腾讯Oceanus实时计算平台架构设计---学习总结

一、背景

实时计算应用主要分为以下四类:

(1)ETL:ETL应该是目前实时计算最普遍的应用场景。例如在TDBank的数据链路中,TDSort读取消息缓存系统Tube中的消息,通过流数据处理系统将消息队列中的数据进行实时分拣,并落地到HDFS接口机集群,并将最终分拣后的数据由加载到TDW中。
(2) 监控系统: 监控系统需要能够对产品和服务进行多维度的监控,对指标数据进行实时的聚合和分析,并支持方便灵活的报警规则设置。
(3)实时BI:及时运营策略。
(4)在线学习:实时计算目前的用户行为进行推荐、广告和搜索等。
目前腾讯的实时计算的规模已经十分庞大。数据平台部实时计算团队每天需要处理超过了17万亿条数据,其中每秒接入的数据峰值达到了2.1亿条,每天3PB的数据量。

二、实时收集数据

腾讯在大数据处理方面主要有两个平台:一个是负责离线数据处理的TDW(Tencent distributed Data Warehouse,腾讯分布式数据仓库),主要进行如产品日周月报表、小时/天粒度的数据分析、数据挖掘等数据应用;另一个是负责实时计算的TRC(Tencent Real-time Computing,腾讯实时计算平台),负责提供秒/分钟级的实时计算。

TDBank系统架构

数据接入层

适配各种各样的数据源,获取到各种形式的业务数据,比如日志数据文件、TCP/UDP消息、数据库记录和HTTP Request等,TDBank提供不同的插件Agent来支持。

数据存储中心

存取速度是一个关键问题,我们使用“文件顺序写+磁盘Raid”来达到高效的数据持久化。另外一方面会面对“Volume”的问题,面对大量的数据,需要使用分布式的集群作为数据缓存,同时使用zookeeper对集群进行协调。

腾讯自主研发的消息中间件Tube,毫秒级送达,提供7天缓存,消息可多次重复订阅等核心功能,另外,还支持消息主动推送。目前Tube每日新增数据条数解决1万亿,大小超过200T。

数据分拣中心

数据分拣中心Sort。采用插件化的形式来支持多种形式的数据预处理过程。对于离线系统来说,一个重要的功能是将实时采集到的数据进行分类存储,需要按照某些维度(比如某个key值+时间等维度)进行分类存储;同时存储文件的粒度(大小/时间)也是需要定制的,使离线系统能以指定的的粒度来进行离线计算。对于在线系统来说,常见的预处理过程如数据过滤、数据采样和数据转换等。

支持多种存储引擎。使用HDFS作为离线文件的存储载体,同时,对HDFS做了改造,解决了namenode单点问题,因此,提供的是可靠的数据存储服务。另外,也支持HBase、PostgreSQL、MySQL等等多种存储引擎,适应不同的业务场景。

TDSort结构图

TDSort是运行在Storm上的一个应用。Storm是Twitter开源的一个实时计算系统,Storm本身是分布式的,具有好的容错性和很高的性能。TDSort从Tube订阅数据进行处理;使用Zookeeper存储TDSort相关的配置,利用Zookeeper的watch通知机制实现对Strom的Worker的业务层管理。然后将对应数据单元的数据写入目的地,将其统计信息写入DB。

三、Oceanus数据平台架构


实时计算团队从2017年开始围绕Flink打造了Oceanus (http://data.qq.com),一个集开发、测试、部署和运维于一体的一站式可视化实时计算平台。Oceanus集成了应用管理、计算引擎和资源管理等功能,提供了三种不同的应用开发方式,包括画布,SQL和Jar,来满足不同用户的开发需求,同时通过日志、监控、运维等周边服务打通了应用的整个生命周期。

Oceanus还研发了Oceanus-ML来提高在线学习任务的开发效率。

用户可以通过Oceanus配置作业所需要的CPU和内存资源,并指定作业需要部署的集群。当用户完成配置之后,Oceanus会向Gaia申请对应的资源并将作业提交到Gaia上运行。

画布

大部分Oceanus的用户可以使用画布方便的构建他们的实时计算应用。Oceanus提供了常见的流计算算子。在开发实时计算应用时,用户将需要的算子拖拽到画布上,配置这些算子的属性并将这些算子连接,这样就构建好了一个流计算应用。这种构建方式十分简单,不需要用户了解底层实现的细节,也不需要掌握SQL等语言的语法,使得用户能够专注于业务逻辑。

SQL和Jar

为了用户能够在使用SQL和Jar进行开发时也能方便的进行作业配置,Oceanus会首先对用户提交的SQL脚本和JAR包进行解析和编译,生成作业执行的JobGraph,并可视化在页面上。

动态调整task的数量

输入和输出的TPS也是在作业运行中的关键指标。通常来说,一个task的输出TPS和输入TPS之间的比例并不会随着并发度的变化而变化。我们利用这个性质来确定作业运行时的并发度。当确定作业并发度时,我们首先将所有task的并发度设置为1并启动作业。此时这个作业显然是无法处理上游的数据的,因此大部分task的单机处理能力会被打满,其输入和输出TPS可以达到最大值。根据需要的TPS和单机最大TPS,我们可以估算出每个task的并发度,并重新启动。之后根据前面提到的输入输出队列的使用率,我们对作业并发度进行一定的调整来去除作业中的性能瓶颈。一般通过几次调整之后,我们就可以得到较为理想的作业并发度配置。

TaskExecutor的线程信息进行了采集

在即将发布的新版Oceanus中,我们还对TaskExecutor的线程信息进行了采集。这些线程信息能够很好地帮助用户定位发生的问题。例如checkpoint可能会由于多种多样的原因而超时。当用户实现的source function在被IO或者网络堵塞时并没有释放checkpoint锁,那么正在执行的checkpoint可能就会由于无法及时获取锁而超时。用户也有可能实现了一个堵塞的checkpoint函数,由于较慢的HDFS写入或者其他原因而导致checkpoint超时。通过观察线程信息,我们就可以容易的知道checkpoint超时的原因。

这些采集的线程信息也能对程序的性能优化提供很多帮助。一般而言,当一个task线程的cpu使用率达到100%时,就说明这个task的执行并没有受到加锁,I/O或者网络等操作的影响。在上图中,我们展示了一个Word Count程序的Task Executor的线程信息。在Word Count程序,我们有一个source task在持续不断的发送word,还有一个map task对出现的word进行计数。可以看到,在Task Executor中,map线程的cpu使用率几乎达到了100%,这说明其的执行是没有太大问题的。而source线程的cpu使用率仅仅只有80%,这说明其的性能受到了影响。观察线程堆栈,我们可以发现source线程时常会堵塞在数据的发送上。这是很好理解的,因为每产生一个word,map线程都需要比source线程执行更多的指令。也就是说,map线程的数据处理能力比source线程的生产能力要低。为了提高这个Word Count程序的性能,我们就需要保证map线程的数目比source线程的数目多一点。

四、GAIA架构

Gaia架构

Gaia其实是基于Hadoop的YARN改造的一个Docker的调度系统。

首先它相比社区的YARN有哪些特点呢?社区的YARN可能在RM、NM都已经实现了无单点的设计,可以热升级。在此基础之上,我们自研了一个AM,负责所有Docker类作业的调度。然后我们对AM以及Docker也进行了一定的改造,让它支持无单点的一个设计。右边的图中我们可以看到每个Slave节点上除了有NM之外,还有一个Docker进程,负责拉起所有的Docker作业,我们也实现了Docker的热升级。除了对Master节点进行无单点改造之外,Gaia也为用户的APP提供了本地重试和跨机重试两种容灾方式。

五、Flink内核改进

(1)作业管理相关:我们对Flink的作业管理的改进主要以提高作业执行的可靠性为主,包括对分布式环境下的leader选举的重构和无需作业重启的Job Master恢复机制等。同时,我们也正在研究和开发细粒度恢复机制来减少发生故障时需要重启的task数目。
(2)资源调度相关:我们对Flink的资源调度,特别是在Yarn集群上的资源调度进行了重构,以提供更好的资源使用率。同时,我们也正在研究如何使用分布式和异步的资源调度框架来提高超大并发度的作业的资源调度效率。
(3)可用性相关:我们在Flink中提供了多个算子,包括local keyby, incremental windows, dim join等。这些算子能够很好的提高用户开发程序的效率和程序执行的性能。

分布式Leader选举

目前Flink依赖Zookeeper进行leader选举,并将当选的leader的信息保存在Zookeeper上以实现服务发现。但在复杂的集群环境中,Flink当前的实现并不能很好的保证leader选举和发布的正确性。

如上图左侧所示,当JM1获得leader之后,其需要在Zookeeper发布其地址以供其他节点来发现自己。但如果在其发布地址之前,JM1发生了Full GC,那么集群就可以陷入混乱之中。其长时间的GC可能会导致其丢失leader以及和Yarn之间的心跳连接。此时一个新的master节点, JM2, 可能会被Yarn拉起。JM2在获得leader之后会将其地址发布在集群中。当如果此时JM1从Full GC中恢复过来,并继续执行之前的代码,将其地址发布在集群中,那么JM1的地址将会覆盖JM2的地址导致集群混乱。

如上图右侧,另一个由于leader选举导致的常见问题是checkpoint的并发访问。当一个master丢失leader节点之后,其需要立即停止其所有正在进行的工作并退出。但是如果此时旧master的Checkpoint Coordinator正在完成checkpoint,那么退出方法将无法获取到锁而执行。此时,在已经丢失了leader的情况下,旧master仍然有机会完成一个新的checkpoint。而此时,新master却会从一个较旧的checkpoint进行恢复。目前Flink使用了许多tricky的方法来保证多个master节点对checkpoint的并发访问不会导致作业无法从故障中恢复,但这些方法也导致我们目前无法对失败的checkpoint进行有效的脏数据清理。


为了上述问题,我们对Flink的leader选举和发布进行了重构。我们要求每个master节点在竞争leader时都创建一个EMPHEMERAL和SEQUENTIAL的latch节点。之后所有master节点会检查latch目录下所有的节点,序列号最小的那个节点将会被选举为leader。

Zookeeper的实现保证了创建的latch节点的序列号是递增的。所以如果一个master节点被选为leader之后,只要它的latch节点仍然存在,就意味着它的序列号仍然是所有master节点中最小的,它仍然是集群中的leader。从而我们就可以通过检查一个master的latch节点是否存在来判断这个master是否已经丢失leader。通过将leader地址的发布以及对checkpoint的修改等更新操作和对latch节点的检查放置在一个Zookeeper事务中,我们可以保证只有保有leader的master节点才可以对作业执行状态进行修改。

无需作业重启的master恢复机制

Master节点会由于多种不同的原因而发生故障。目前在master重启时,Flink会重启所有正在执行的task,重新开始执行作业。在Zookeeper连接出现抖动时,集群中所有task都会重启,对HDFS, Zookeeper和YARN这些集群基本组件带来较大的压力,使得集群环境进一步恶化。

为了减少master恢复的开销,我们实现了无需作业重启的master恢复机制。首先,我们使用Zookeeper和心跳等手段来对master的状态进行监控。当master发生故障时,我们立即拉起一个新的master。新master在启动时,并不会像第一次执行时那样申请资源并调度任务,而是会进入到reconcile阶段,等待task的汇报。

在另一边,task executor在丢失了和master节点的连接之后,也不会立即杀死这个master负责的task。相反,它将等待一段时间来发现新master的地址。如果在这段时间内发现了新master的地址,那么task executor将把其执行的task的信息汇报给新master。

新master通过task executor汇报上来的信息来重建其execution graph和slot pool。当所有task完成汇报,并且所有task在master恢复的这段时间内没有出现故障,那么master就可以直接切换作业状态到running,并继续作业的执行。如果有task未能在规定时间内汇报,或者有task在这段时间内发生故障,那么master将切换到failover状态并通过重启恢复执行。

细粒度资源分配


目前Oceanus依赖YARN来进行资源申请和任务调度。但现有Flink在YARN上资源分配的实现有着较大的问题,对作业可靠性带来了一定的风险。

在现在Flink的实现中,每个task executor都有着一定数目的slot。这些slot的数目是在task executor启动时根据配置得到的。当为任务分配资源时,task会按照可用slot的数目分配到空闲的task executor上,一个task占据一个slot。在这个过程中,Flink并不会考虑task实际使用的资源量以及task executor剩余可用的资源量。

这种资源分配的方式是十分危险的,会导致task executor向YARN申请的资源量和实际task使用的资源量不匹配。在集群资源紧张的时候,由于YARN会杀死那些超用资源的container,作业就会进入不断重启的状态之中。

这种资源分配的方式也会导致较严重的资源浪费。在实际中每个算子所需的资源使用量是不同的。有的算子需要较多的CPU资源,而有的算子需要较少的内存资源。由于现在的配置中所有task executor具有相同的slot数目,所有slot都具有相同的资源,因此导致较为严重的资源碎片,无法充分利用集群资源。

为了避免由于资源分配导致的不稳定,我们修改了Flink在YARN上的资源申请协议。我们不再使用静态的slot配置,而是根据task申请动态的创建和销毁slot。首先,我们要求用户能够为每个operator设置其所需的资源量。这样我们就可以根据slot中执行的operator来得到每个slot所需的资源量。当Master节点请求一个slot时,我们遍历所有的task executor并在空余资源量能够满足slot请求的task executor上创建一个新的slot提供给master节点。当这个slot中的task完成执行之后,这个slot也将被删除并将其资源归还给task executor。这种动态的slot申请方式可以使得Flink的资源利用率极大的提高。

动态管理task executor的slot

关于solt:Flink 中的计算资源通过 Task Slot 来定义。每个 task slot 代表了 TaskManager 的一个固定大小的资源子集。例如,一个拥有3个slot的 TaskManager,会将其管理的内存平均分成三分分给各个 slot。将资源 slot 化意味着来自不同job的task不会为了内存而竞争,而是每个task都拥有一定数量的内存储备。需要注意的是,这里不会涉及到CPU的隔离,slot目前仅仅用来隔离task的内存。

Local Keyed Streams

以WordCount程序作为示例。为了统计每个出现word的次数,我们需要将每个word送到对应的aggregator上进行统计。当有部分word出现的次数远远超过其他word时,那么将只有少数的几个aggregator在执行,而其他的aggregator将空闲。当我们增加更多的aggregator时,因为绝大部分word仍然只会被发送到少数那几个aggregator上,程序性能也不会得到任何提高。

为了解决负载倾斜的问题,我们提供了Local Keyby算子,允许用户在task本地对数据流进行划分。划分得到的Local keyed streams和一般的Keyed streams是类似的。用户可以通过RuntimeContext访问keyed state,也可以在数据流上执行窗口操作。利用Local keyed streams,我们就可以在数据发送一端就进行本地的预聚合,统计一定时间段内word在当前task出现的次数。这些预聚合的结果然后被发送给下游,通过合并得到最终的结果。


但Local keyed streams的数据划分和分发和keyed streams不同。在keyed streams中,数据流会划分成多个key group,每个task都会负责一部分key group的处理。每个task之间的key group是没有任何交集的。而由于local keyed streams是在task本地对数据流进行划分,因此每个task上的key group range都是key group全集。即如果数据流总共有3个key group,那么每个task的local key group range都为[1, 3]。

当并发度改变时,这些local key group将按照数据均匀分给新的task。例如当task并发度从3变为2时,那么第一个task将分配到5个local key group,而第二个task将被分配到4个。在这种情况下,同一个task将会被分配到多个具有相同id的local key group。这些具有相同id的local key group将会被合并起来。当合并完成之后,所有task上的local key group range将仍然是[1, 3]。对于Reducing State, Aggregating State以及List State来说,它们的合并是比较简单的。而对于Value State, MapState和Folding State等类型的数据而说,则需要用户提供自定义的合并函数来实现local key group的合并。

由于在一定时间段内发送给下游的数据量不过超过上游的并发度,下游的负载倾斜可以有效缓解。同时由于数据在上游一般没有较为严重的倾斜,程序性能不会由于负载倾斜而严重降低。我们测试了WordCount程序在不同数据倾斜程度下的吞吐。可以看到,在没有使用local keyed streams的情况下,程序性能随着倾斜程度而迅速下降,而使用local keyed streams之后,程序性能几乎不受影响。

动态分配六数据量给下游程序,实现负载均衡

可用性提升


为了方便用户开发画布和SQL程序,我们实现了超过30个的Table API和SQL函数。用户可以利用这些内置函数极大地提高实时计算应用的开发效率。此外,我们也对数据流和外部维表的join进行了大量优化,并补充了Flink还未支持的Top N功能。我们还提供了incremental window功能,允许用户能够在窗口未触发时得到窗口的当前结果。Incremental window在多个应用场景中有着广泛的应用。例如用户可以利用incremental window统计活跃用户数目在一天内的增长情况。

增加了很多新的常用功能

六、使用


参考文献:
https://data.qq.com/article?id=3884
http://dockone.io/article/1555
https://data.qq.com/article?id=951
http://wuchong.me/blog/2016/05/09/flink-internals-understanding-execution-resources/

全部评论

相关推荐

重生2012之我是java程序员:换个稍微正式点的照片吧
点赞 评论 收藏
分享
点赞 1 评论
分享
牛客网
牛客企业服务