分布式技术的发展,深刻地改变了我们编程的模式和思考软件的模式。值 2019 岁末,PingCAP 联合 InfoQ 共同策划出品“分布式系统前沿技术 ”专题, 邀请众多技术团队共同参与,一起探索这个古老领域的新生机。本文出自微众银行大数据平台负责人邸帅。
在当前的复杂分布式架构环境下,服务治理已经大行其道。但目光往下一层,从上层 APP、Service,到底层计算引擎这一层面,却还是各个引擎各自为政,Client-Server 模式紧耦合满天飞的情况。如何做好“计算治理”,让复杂环境下各种类型的大量计算任务,都能更简洁、灵活、有序、可控的提交执行,和保障成功返回结果?计算中间件 Linkis 就是上述问题的最佳实践。
分布式架构,指的是系统的组件分布在通过网络相连的不同计算机上,组件之间通过网络传递消息进行通信和协调,协同完成某一目标。一般来说有水平(集群化)和垂直(功能模块切分)两个拆分方向,以解决高内聚低耦合、高并发、高可用等方面问题。
多个分布式架构的系统,组成分布式系统群,就形成了一个相对复杂的分布式架构环境。通常包含多种上层应用服务,多种底层基础计算存储引擎。如下图 1 所示:
图 1
就像《微服务设计》一书中提到的,如同城市规划师在面对一座庞大、复杂且不断变化的城市时,所需要做的规划、设计和治理一样,庞大复杂的软件系统环境中的各种区域、元素、角色和关系,也需要整治和管理,以使其以一种更简洁、优雅、有序、可控的方式协同运作,而不是变成一团乱麻。
在当前的复杂分布式架构环境下,大量 APP、Service 间的通信、协调和管理,已经有了从 SOA(Service-Oriented Architecture)到微服务的成熟理念,及从 ESB 到 Service Mesh 的众多实践,来实现其从服务注册发现、配置管理、网关路由,到流控熔断、日志监控等一系列完整的服务治理功能。服务治理框架的“中间件”层设计,可以很好的实现服务间的解耦、异构屏蔽和互操作,并提供路由、流控、状态管理、监控等治理特性的共性提炼和复用,增强整个架构的灵活性、管控能力、可扩展性和可维护性。
但目光往下一层,你会发现在从 APP、Service,到后台引擎这一层面,却还是各个引擎各自为政,Client-Server 模式紧耦合满天飞的情况。在大量的上层应用,和大量的底层引擎之间,缺乏一层通用的“中间件”框架设计。类似下图 2 的网状。
图 2
计算治理,关注的正是上层应用和底层计算(存储)引擎之间,从 Client 到 Server 的连接层范围,所存在的紧耦合、灵活性和管控能力欠缺、缺乏复用能力、可扩展性、可维护性差等问题。要让复杂分布式架构环境下各种类型的计算任务,都能更简洁、灵活、有序、可控的提交执行,和成功返回结果。如下图 3 所示:
图 3
更详细的来看计算治理的问题,可以分为如下治(architecture,架构层面)和理(insight,细化特性)两个层面。
计算治理之治(architecture)-架构层面问题
紧耦合问题,上层应用和底层计算存储引擎间的 CS 连接模式。
所有 APP & Service 和底层计算存储引擎,都是通过 Client-Server 模式相连,处于紧耦合状态。以 Analytics Engine 的 Spark 为例,如下图 4:
图 4
这种状态会带来如下问题:
引擎 client 的任何改动(如版本升级),将直接影响每一个嵌入了该 client 的上层应用;当应用系统数量众多、规模庞大时,一次改动的成本会很高。
直连模式,导致上层应用缺乏,对跨底层计算存储引擎实例级别的,路由选择、负载均衡等能力;或者说依赖于特定底层引擎提供的特定连接方式实现,有的引擎有一些,有的没有。
随着时间推移,不断有新的上层应用和新的底层引擎加入进来,整体架构和调用关系将愈发复杂,可扩展性、可靠性和可维护性降低。
重复造轮子问题,每个上层应用工具系统都要重复解决计算治理问题。
每个上层应用都要重复的去集成各种 client,创建和管理 client 到引擎的连接及其状态,包括底层引擎元数据的获取与管理。在并发使用的用户逐渐变多、并发计算任务量逐渐变大时,每个上层应用还要重复的去解决多个用户间在 client 端的资源争用、权限隔离,计算任务的超时管理、失败重试等等计算治理问题。
图 5
想象你有 10 个并发任务数过百的上层应用,不管是基于 Web 的 IDE 开发环境、可视化 BI 系统,还是报表系统、工作流调度系统等,每个接入 3 个底层计算引擎。上述的计算治理问题,你可能得逐一重复的去解决 10*3=30 遍,而这正是当前在各个公司不断发生的现实情况,其造成的人力浪费不可小觑。
扩展难问题,上层应用新增对接底层计算引擎,维护成本高,改动大。
在 CS 的紧耦合模式下,上层应用每新增对接一个底层计算引擎,都需要有较大改动。
以对接 Spark 为例,在上层应用系统中的每一台需要提交 Spark 作业的机器,都需要部署和维护好 Java 和 Scala 运行时环境和变量,下载和部署 Spark Client 包,且配置并维护 Spark 相关的环境变量。如果要使用 Spark on YARN 模式,那么你还需要在每一台需要提交 Spark 作业的机器上,去部署和维护 Hadoop 相关的 jar 包和环境变量。再如果你的 Hadoop 集群需要启用 Kerberos 的,那么很不幸,你还需要在上述的每台机器去维护和调试 keytab、principal 等一堆 Kerberos 相关配置。
图 6
这还仅仅是对接 Spark 一个底层引擎。随着上层应用系统和底层引擎的数量增多,需要维护的关系会是个笛卡尔积式的增长,光 Client 和配置的部署维护,就会成为一件很令人头疼的事情。
应用孤岛问题,跨不同应用工具、不同计算任务间的互通问题。
多个相互有关联的上层应用,向后台引擎提交执行的不同计算任务之间,往往是有所关联和共性的,比如需要共享一些用户定义的运行时环境变量、函数、程序包、数据文件等。当前情况往往是一个个应用系统就像一座座孤岛,相关信息和资源无法直接共享,需要手动在不同应用系统里重复定义和维护。
典型例子是在数据批处理程序开发过程中,用户在数据探索开发 IDE 系统中定义的一系列变量、函数,到了数据可视化系统里往往又要重新定义一遍;IDE 系统运行生成的数据文件位置和名称,不能直接方便的传递给可视化系统;依赖的程序包也需要从 IDE 系统下载、重新上传到可视化系统;到了工作流调度系统,这个过程还要再重复一遍。不同上层应用间,计算任务的运行依赖缺乏互通、复用能力。
图 7
计算治理之理(insight)- 细化特性问题
除了上述的架构层面问题,要想让复杂分布式架构环境下,各种类型的计算任务,都能更简洁、灵活、有序、可控的提交执行,和成功返回结果,计算治理还需关注高并发,高可用,多租户隔离,资源管控,安全增强,计算策略等等细化特性问题。这些问题都比较直白易懂,这里就不一一展开论述了。
核心功能模块与流程
计算中间件 Linkis,是微众银行专门设计用来解决上述紧耦合、重复造轮子、扩展难、应用孤岛等计算治理问题的。当前主要解决的是复杂分布式架构的典型场景-数据平台环境下的计算治理问题。
Linkis 作为计算中间件,在上层应用和底层引擎之间,构建了一层中间层。能够帮助上层应用,通过其对外提供的标准化接口(如 HTTP, JDBC, Java …),快速的连接到多种底层计算存储引擎(如 Spark、Hive、TiSpark、MySQL、Python 等),提交执行各种类型的计算任务,并实现跨上层应用间的计算任务运行时上下文和依赖的互通和共享。且通过提供多租户、高并发、任务分发和管理策略、资源管控等特性支持,使得各种计算任务更灵活、可靠、可控的提交执行,成功返回结果,大大降低了上层应用在计算治理层的开发和运维成本、与整个环境的架构复杂度,填补了通用计算治理软件的空白。(图 8、9)
图 8
图 9
要更详细的了解计算任务通过 Linkis 的提交执行过程,我们先来看看 Linkis 核心的“计算治理服务”部分的内部架构和流程。如下图 10:
图 10
计算治理服务 :计算中间件的核心计算框架,主要负责作业调度和生命周期管理、计算资源管理,以及引擎连接器的生命周期管理。
公共增强服务:通用公共服务,提供基础公共功能,可服务于 Linkis 各种服务及上层应用系统。
其中计算治理服务的主要模块如下:
入口服务 Entrance,负责接收作业请求,转发作业请求给对应的 Engine,并实现异步队列、高并发、高可用、多租户隔离。
应用管理服务 AppManager,负责管理所有的 EngineConnManager 和 EngineConn,并提供 EngineConnManager 级和 EngineConn 级标签能力;加载新引擎插件,向 RM 申请资源, 要求 EM 根据资源创建 EngineConn;基于标签功能,为作业分配可用 EngineConn。
资源管理服务 ResourceManager,接收资源申请,分配资源,提供系统级、用户级资源管控能力,并为 EngineConnManager 级和 EngineConn 提供负载管控。
引擎连接器管理服务 EngineConn Manager,负责启动 EngineConn,管理 EngineConn 的生命周期,并定时向 RM 上报资源和负载情况。
引擎连接器 EngineConn,负责与底层引擎交互,解析和转换用户作业,提交计算任务给底层引擎,并实时监听底层引擎执行情况,回推相关日志、进度和状态给 Entrance。
如图 10 所示,一个作业的提交执行主要分为以下 11 步:
1. 上层应用向计算中间件提交作业,微服务网关 SpringCloud Gateway 接收作业并转发给 Entrance。
2. Entrance 消费作业,为作业向 AppManager 申请可用 EngineConn。
3. 如果不存在可复用的 Engine,AppManager 尝试向 ResourceManager 申请资源,为作业启动一个新 EngineConn。
4. 申请到资源,要求 EngineConnManager 依照资源启动新 EngineConn
5. EngineConnManager 启动新 EngineConn,并主动回推新 EngineConn 信息。
6. AppManager 将新 EngineConn 分配给 Entrance,Entrance 将 EngineConn 分配给用户作业,作业开始执行,将计算任务提交给 EngineConn。
7. EngineConn 将计算任务提交给底层计算引擎。
8. EngineConn 实时监听底层引擎执行情况,回推相关日志、进度和状态给 Entrance,Entrance 通过 WebSocket,主动回推 EngineConn 传过来的日志、进度和状态给上层应用系统。
9. EngineConn 执行完成后,回推计算任务的状态和结果集信息,Entrance 将作业和结果集信息更新到 JobHistory,并通知上层应用系统。
10. 上层应用系统访问 JobHistory,拿到作业和结果集信息。
11. 上层应用系统访问 Storage,请求作业结果集。
计算任务管理策略支持
在复杂分布式环境下,一个计算任务往往不单会是简单的提交执行和返回结果,还可能需要面对提交失败、执行失败、hang 住等问题,且在大量并发场景下还需通过计算任务的调度分发,解决租户间互相影响、负载均衡等问题。
Linkis 通过对计算任务的标签化,实现了在任务调度、分发、路由等方面计算任务管理策略的支持,并可按需配置超时、自动重试,及灰度、多活等策略支持。如下图 11。
图 11
基于 Spring Cloud 微服务框架
说完了业务架构,我们现在来聊聊技术架构。在计算治理层环境下,很多类型的计算任务具有生命周期较短的特征,如一个 Spark job 可能几十秒到几分钟就执行完,EngineConn(EnginConnector)会是大量动态启停的状态。前端用户和 Linkis 中其他管理角色的服务,需要能够及时动态发现相关服务实例的状态变化,并获取最新的服务实例访问地址信息。同时需要考虑,各模块间的通信、路由、协调,及各模块的横向扩展、负载均衡、高可用等能力。
基于以上需求,Linkis 实际是基于 Spring Cloud 微服务框架技术,将上述的每一个模块/角色,都封装成了一个微服务,构建了多个微服务组,整合形成了 Linkis 的完整计算中间件能力。如下图 12:
图 12
从多租户管理角度,上述服务可区分为租户相关服务,和租户无关服务两种类型。租户相关服务,是指一些任务逻辑处理负荷重、资源消耗高,或需要根据具体租户、用户、物理机器等,做隔离划分、避免相互影响的服务,如 Entrance、 EnginConn(EnginConnector) Manager、EnginConn;其他如 App Manger、Resource Manager、Context Service 等服务,都是租户无关的。
Eureka承担了微服务动态注册与发现中心,及所有租户无关服务的负载均衡、故障转移功能。
Eureka 有个局限,就是在其客户端,对后端微服务实例的发现与状态刷新机制,是客户端主动轮询刷新,最快可设 1 秒 1 次(实际要几秒才能完成刷新)。这样在 Linkis 这种需要快速刷新大量后端 EnginConn 等服务的状态的场景下,时效得不到满足,且定时轮询刷新对 Eureka server、对后端微服务实例的成本都很高。
为此我们对 Spring Cloud Ribbon 做了改造,在其中封装了 Eureka client 的微服务实例状态刷新方法,并把它做成满足条件主动请求刷新,而不会再频繁的定期轮询。从而在满足时效的同时,大大降低了状态获取的成本。如下图 13:
图 13
Spring Cloud Gateway承担了外部请求 Linkis 的入口网关的角色,帮助在服务实例不断发生变化的情况下,简化前端用户的调用逻辑,快速方便的获取最新的服务实例访问地址信息。
Spring Cloud Gateway 有个局限,就是一个 WebSocket 客户端只能将请求转发给一个特定的后台服务,无法完成一个 WebSocket 客户端通过网关 API 对接后台多个 WebSocket 微服务,而这在我们的 Entrance HA 等场景需要用到。
为此 Linkis 对 Spring Cloud Gateway 做了相应改造,在 Gateway 中实现了 WebSocket 路由转发器,用于与客户端建立 WebSocket 连接。建立连接成功后,会自动分析客户端的 WebSocket 请求,通过规则判断出请求该转发给哪个后端微服务,然后将 WebSocket 请求转发给对应的后端微服务实例。详见 Github 上 Linkis 的 Wiki 中,“Gateway 的多 WebSocket 请求转发实现”一文。
图 14
Spring Cloud OpenFeign 提供的 HTTP 请求调用接口化、解析模板化能力,帮助 Linkis 构建了底层 RPC 通信框架。
但基于 Feign 的微服务之间 HTTP 接口的调用,只能满足简单的 A 微服务实例根据简单的规则随机选择 B 微服务之中的某个服务实例,而这个 B 微服务实例如果想异步回传信息给调用方,是无法实现的。同时,由于 Feign 只支持简单的服务选取规则,无法做到将请求转发给指定的微服务实例,无法做到将一个请求广播给接收方微服务的所有实例。
Linkis 基于 Feign 实现了一套自己的底层 RPC 通信方案,集成到了所有 Linkis 的微服务之中。一个微服务既可以作为请求调用方,也可以作为请求接收方。作为请求调用方时,将通过 Sender 请求目标接收方微服务的 Receiver;作为请求接收方时,将提供 Receiver 用来处理请求接收方 Sender 发送过来的请求,以便完成同步响应或异步响应。如下图示意。详见 GitHub 上 Linkis 的 Wiki 中,“Linkis RPC 架构介绍”一文。
图 15
至此,Linkis 对上层应用和底层引擎的解耦原理,其核心架构与流程设计,及基于 Spring Cloud 微服务框架实现的,各模块微服务化动态管理、通信路由、横向扩展能力介绍完毕。
Linkis 作为计算中间件,在上层应用和底层引擎之间,构建了一层中间层。上层应用所有计算任务,先通过 HTTP、WebSocket、Java 等接口方式提交给 Linkis,再由 Linkis 转交给底层引擎。原有的上层应用以 CS 模式直连底层引擎的紧耦合得以解除,因此实现了解耦。如下图 16 所示:
图 16
通过解耦,底层引擎的变动有了 Linkis 这层中间件缓冲,如引擎 client 的版本升级,无需再对每一个对接的上层应用做逐个改动,可在 Linkis 层统一完成。并能在 Linkis 层,实现对上层应用更加透明和友好的升级策略,如灰度切换、多活等策略支持。且即使后继接入更多上层应用和底层引擎,整个环境复杂度也不会有大的变化,大大降低了开发运维工作负担。
上层应用复用 Linkis 示例(Scriptis)
有了 Linkis,上层应用可以基于 Linkis,快速实现对多种后台计算存储引擎的对接支持,及变量、函数等自定义与管理、资源管控、多租户、智能诊断等计算治理特性。
优点
以微众银行与 Linkis 同时开源的,交互式数据开发探索工具 Scriptis 为例,Scriptis 的开发人员只需关注 Web UI、多种数据开发语言支持、脚本编辑功能等纯前端功能实现,Linkis 包办了其从存储读写、计算任务提交执行、作业状态日志更新、资源管控等等几乎所有后台功能。基于 Linkis 的大量计算治理层能力的复用,大大降低了 Scriptis 项目的开发成本,使得 Scritpis 目前只需要有限的前端人员,即可完成维护和版本迭代工作。
如下图 17,Scriptis 项目 99.5% 的代码,都是前端的 JS、CSS 代码。后台基本完全复用 Linkis。
图 17
模块化可插拔的计算引擎接入设计,新引擎接入简单快速。
对于典型交互式模式计算引擎(提交任务,执行,返回结果),用户只需要 buildApplication 和 executeLine 这 2 个方法,就可以完成一个新的计算引擎接入 Linkis,代码量极少。示例如下。
(1) AppManager 部分:用户必须实现的接口是 ApplicationBuilder,用来封装新引擎连接器实例启动命令。
1. //用户必须实现的方法: 用于封装新引擎连接器实例启动命令
2. def buildApplication(protocol:Protocol):ApplicationRequest
(2) EngineConn部分:用户只需实现executeLine方法,向新引擎提交执行计算任务:
1. //用户必须实现的方法:用于调用底层引擎提交执行计算任务
2. def executeLine(context: EngineConnContext,code: String): ExecuteResponse
引擎相关其他功能/方法都已有默认实现,无定制化需求可直接复用。
通过 Linkis 提供的上下文服务,和存储、物料库服务,接入的多个上层应用之间,可轻松实现环境变量、函数、程序包、数据文件等,相关信息和资源的共享和复用,打通应用孤岛。
图 18
Context Service 上下文服务介绍
Context Service(CS)为不同上层应用系统,不同计算任务,提供了统一的上下文管理服务,可实现上下文的自定义和共享。在 Linkis 中,CS 需要管理的上下文内容,可分为元数据上下文、数据上下文和资源上下文 3 部分。
图 19
元数据上下文,定义了计算任务中底层引擎元数据的访问和使用规范,主要功能如下:
提供用户的所有元数据信息读写接口(包括 Hive 表元数据、线上库表元数据、其他 NoSQL 如 HBase、Kafka 等元数据)。
计算任务内所需元数据的注册、缓存和管理。
数据上下文,定义了计算任务中数据文件的访问和使用规范。管理数据文件的元数据。
运行时上下文,管理各种用户自定义的变量、函数、代码段、程序包等。
同时 Linkis 也提供了统一的物料管理和存储服务,上层应用可根据需要对接,从而可实现脚本文件、程序包、数据文件等存储层的打通。
Linkis 计算治理细化特性设计与实现介绍,在高并发、高可用、多租户隔离、资源管控、计算任务管理策略等方面,做了大量细化考量和实现,保障计算任务在复杂条件下成功执行。
Linkis 的 Job 基于多级异步设计模式,服务间通过高效的 RPC 和消息队列模式进行快速通信,并可以通过给 Job 打上创建者、用户等多种类型的标签进行任务的转发和隔离来提高 Job 的并发能力。通过 Linkis 可以做到 1 个入口服务(Entrance)同时承接超 1 万+ 在线的 Job 请求。
多级异步的设计架构图如下:
图 20
如上图所示 Job 从 GateWay 到 Entrance 后,Job 从生成到执行,到信息推送经历了多个线程池,每个环节都通过异步的设计模式,每一个线程池中的线程都采用运行一次即结束的方式,降低线程开销。整个 Job 从请求—执行—到信息推送全都异步完成,显著的提高了 Job 的并发能力。
这里针对计算任务最关键的一环 Job 调度层进行说明,海量用户成千上万的并发任务的压力,在 Job 调度层中是如何进行实现的呢?
在请求接收层,请求接收队列中,会缓存前端用户提交过来的成千上万计算任务,并按系统/用户层级划分的调度组,分发到下游 Job 调度池中的各个调度队列;到 Job 调度层,多个调度组对应的调度器,会同时消费对应的调度队列,获取 Job 并提交给 Job 执行池进行执行。过程中大量使用了多线程、多级异步调度执行等技术。示意如下图 21:
图 21
Linkis 还在高可用、多租户隔离、资源管控、计算任务管理策略等方面,做了很多细化考量和实现。篇幅有限,在这里不再详述每个细化特性的实现,可参见 Github 上 Linkis 的 Wiki。后继我们会针对 Linkis 的计算治理-理之路(Insight)的细化特性相关内容,再做专题介绍。
基于如上解耦、复用、快速扩展、连通等架构设计优点,及高并发、高可用、多租户隔离、资源管控等细化特性实现,计算中间件 Linkis 在微众生产环境的应用效果显著。极大的助力了微众银行一站式大数据平台套件 WeDataSphere 的快速构建,且构成了 WeDataSphere 全连通、多租户、资源管控等企业级特性的基石。
Linkis 在微众应用情况如图 22:
图 22
我们已将 Linkis 开源,Github repo 地址: https://github.com/WeBankFinTech/Linkis 。
欢迎对类似计算治理问题感兴趣的同学,参与到计算中间件 Linkis 的社区协作中,共同把 Linkis 建设得更加完善和易用。
作者介绍:邸帅,微众银行大数据平台负责人,主导微众银行 WeDataSphere 大数据平台套件的建设运营与开源,具备丰富的大数据平台开发建设实践经验。
本文是「分布式系统前沿技术」专题文章,目前该专题在持续更新中,欢迎大家保持关注:point_down: