本文是我在实时数据计算系统的设计、开发、运维生涯的一部分经验总结。主要介绍一些设计思路和常见问题的解决方案,不关注具体计算框架的使用。
本人主要致力于监控系统数据计算方向,主要业务场景有:监控数据的ETL、数据汇聚、分析、异常检测等。
由于监控系统对时效性有较高需求,所以我们的计算系统更偏向实时系统,根据业务场景的不同,延迟从数百毫秒到分钟不等。下文提到的计算架构更多是指整个计算处理通路,主要以监控场景下的系统设计为例,相信与其他场景下的架构也有相通之处。
文章从以下几个方面展开:
首先,在第1节,我们简述不同数据规模和场景下,监控系统计算架构的可选方案。在公司、业务发展的不同阶段,主要矛盾不同,能够投入人力物力资源不同,需要选择合适的架构方案。
实时计算系统的设计有一个核心问题:如何同时满足高时效性和数据准确性?在现实环境中,高时效性和准确性很难同时达到,第2节中介绍了Watermark机制以实现两者的平衡。
第3节介绍百度监控系统的实时计算系统架构,描述了其基本组成、思路和实现中一些常见问题的解决方案。
最后,简单讨论了实时计算系统可用性建设。
监控系统计算架构选型
对于包含数百到千级别节点的小集群,数据规模有限,所有采集、存储、计算逻辑都可以集成在一个模块中实现,这种多为领域专用系统。监控系统中,典型的有Prometheus,其采用单服务节点架构,提供简单的HA模式进行容错,这种架构的优点是部署使用简单。受限于单机资源,适合部署自治的多个实例,每个实例监控不同服务。大规模集群情况下,要实现全局的监控,需要合并多个监控实例的数据,在配置分发、可用性、容错、自动化部署管理等方面都需要更多的工作。从开发角度考虑,由于功能耦合严重,不利于开发升级。
比起领域专用系统,还有一种架构是使用通用性更强的OLAP系统来实现实时或者近实时的计算功能,如TSDB、ElasticSearch等系统都有一定的聚合计算能力。这些分布式系统都有水平扩展能力和容错能力,但难以实现复杂业务计算,同时延迟不可控,复杂查询或大批量数据查询的延迟可能会达到分钟级别。
更多的情况下我们采用存储计算分离的方案,以便存储和计算的各自演进和平台化。通常由一个提供精细查询能力的存储服务与一个计算模块组成。计算模块根据计算规则从存储系统中拉取数据并进行计算,这个架构的瓶颈在于存储系统能够支持的查询规模。根据我们的经验,基于精心设计的内存数据库集群,能够承受百万级别的并发查询。这种架构下计算任务多为周期性调度,当查询性能下降时会造成任务的堆积。这个模型不方便处理延迟数据,需要一定机制预测数据完整时间,调度任务进行重算,任务调度的复杂度高。基于索引查询的计算系统的延迟取决于计算轮询的周期,适用于聚合类的涉及时间窗口的运算操作。
在更高数据量和计算规则的情况下,流式计算是一个自然的选择,降低了写存储、索引、查询的消耗,计算延迟大幅降低。
当数据量进一步上升,需要的网络吞吐、计算能力骤增,后端的算力难以跟上数据量的增长。这时候可以将计算能力分散到全链路,将计算提前到数据产生端。在监控系统中,通过在采集端进行预计算和ETL操作,提取或整合有用信息,对于实时日志采集,能大幅度降低传输到后端的数据量。放到更大的视角上,这种将算力下放到数据源的思想,是目前大热的边缘计算的一个主要思路。
近年来,Serverless计算架构得到了更多的重视。这个模型将计算抽象为事件以及触发的计算逻辑,计算逻辑实际由框架调度、分配资源执行。用户只需要编写计算逻辑,而不用关心可用性、可扩展、负载均衡等后端代码,极大的降低了开发成本。通过按需调度,自动扩缩容,业务运维方不再关心容量规划等问题。私以为当前常见计算框架的Serverless化是一个值得尝试的方向。目前的Serverless框架还存在很多问题,例如调度初始化虚机、容器的成本高,缺乏状态存储等,比较适用于无状态的计算。
一般来说根据场景需求,通常对不同的架构会组合使用。例如百度监控系统内部是以流式计算与近线计算相结合的方式提供服务的,近线计算从时序数据库(TSDB)中拉取数据进行计算。对于Trace、在线数据分析等具有比较复杂查询需求但是相对比较低频的操作,更适合基于索引查询的架构。
准确性与时效性
对于实时系统,我们对时效性有更严格的需求,但是通常高时效性伴随着低准确度,二者不可兼得。在分布式环境下,天然存在长尾的延迟数据,这可能是原始数据自身的延迟,也可能是由采集点异常、网络延迟、网络抖动、数据通路中负载等造成的延迟。数据源越多,分散的越广,这个长尾效应就会越严重,等待数据完整所需要的时间也越长。我们需要在最终数据的准确性和时效性间做折中。
不同场景对两者的需求不一致,通常来说报警、自动止损等操作需要最高的时效性,能够容忍一定的精度缺失,在审计、订单等数据上我们更多的追求准确性,时效性可以适当放宽。解决这个折衷的常用机制是Watermark。
Watermark是在数据流中增加标志信息,用以指示一个窗口内的数据已经“完全”到达,可以进行计算。
示例:
假设我们要统计10s内到达的事件数目,以事件时间(Event Time,即事件携带的时间,多为事件产生时间)作为时间基准。如下图所示,横线为Wall Time时间线,单位为s。圆球表示事件,圆球里面的数字为事件时间,其虚线指向产生时间,圆球正对的Wall Time时间线上的点为被计算系统处理的时间(Process Time),两个时间之间的差值为实际延迟。每个事件都或多或少存在延迟,其中数字为45的圆球延迟最大。对于事件时间[40, 50]这个汇聚窗口,假设我们将Watermark线画在53处,则我们认为数据在53之前已经完全到达,已经接收到的那些数据可以参与汇聚,Watermark之后到来的事件则忽略。
具体怎么确定Watermark通常取决于需求,对于数据点数量级比较稳定的场景,可以设置一个到达的数据点的比例,在某一个判断周期内,只要到达的数据点比例满足阈值则可添加Watermark。主流开源计算框架对Watermark的实际定义不尽相同,具体使用参考对应计算框架的定义。
私以为Watermark机制隐含的一个重要思想是将数据准确性的定义交还给用户,让用户决定。产品或者架构上的功能,存在多种方案的情况下,选择最泛化的那个方案,暴露出参数然后让用户来选择,不要自己替用户做决定。当然为了简化实现成本和用户使用成本,可以设置固定的一些选项,并选择一个需求最大的作为默认值。
通常Watermark之后的过期数据点会被丢弃。经常的,除了满足高时效性需求外,我们也需要在之后保证数据的最终准确性,即在一定时间段之后的数据是准确的。常用思路是部署两套计算系统,流式计算用以实现低延迟但是准确性低的需求,批量计算用于补偿计算数据的准确性,这就是Lambda架构。最典型的使用Lambda架构的场景是从日志中统计PV/UV,通常是一个流式采集系统和流式计算框架进行实时的PV/UV计算,同时有一套离线系统,定期拉取原始日志,通过批量计算系统统计准确的PV/UV数值。通常这种架构的缺点是两套系统的资源消耗,开发运维成本高。
当前主流计算框架如Spark和Flink对流式和批量计算进行了统一抽象。可以一定程度降低两套系统的开发运维成本,降低了不同框架的开发运维成本,两次计算的的资源消耗依旧存在。
对于满足交换率和结合率的算子,如常见的统计方法(MAX/MIN/SUM/COUNT),在存储侧支持相同运算的情况下,可以将两次运算合并成一次。我们内部称这个方案为多版本,即数据生产一部分就汇聚一部分,每次汇聚产生一个数据版本,由存储在写入时合并,或者在查询时合并。本质上,这是将汇聚的功能迁移了一部分至存储。
百度监控实时计算系统架构
百度监控系统的实时计算系统承担了监控系统数据处理栈的主要计算功能,每天处理数千亿条消息。本节在实际系统的基础上,进行了一定的抽象,介绍一个较为通用的系统架构。
如图所示,架构主要包含以下组件:
接入模块:包括数据拉取和数据接收,前者主动拉取数据,后者接收由上游模块推送的数据。
分发模块:根据配置的计算规则,过滤订阅的数据,并根据调度策略、集群状态将规则对应的数据分配到一个或多个处理单元进行计算。
处理单元:包括一个物理计算模块和对应的输入输出消息队列。物理计算模块执行实际的业务计算逻辑,不同处理单元间可以是同构的也可以是异构的,根据不同的业务场景和用户需求,可以使用不同的技术栈。
控制模块:接收用户提交的计算规则和管理操作,分配调度资源,产生对其他模块的控制信息。
数据推送模块:拉取计算结果,根据计算规则将数据分发到下游模块。
每个物理计算模块都对应一个输入和输出消息队列,以将数据接入、据输出与计算层隔离,增加一个新的第三方系统的交互不会影响计算功能。升级变更物理框架不会影响其他组件。
由于大数据处理框架,在其数据规模、节点数目达到一定规模时,其处理性能以及异常恢复速度都会下降。我们将一个固定计算能力以及配套的资源(如消息队列)抽象为一个处理单元,每个处理单元处理一部分的数据,取决于计算功能的物理实现,这个处理单元可以对应一个集群或者一个作业。一个处理单元的具体规模取决于具体的技术选型和硬件条件。确认处理单元的好处是便于容量规划,可以以一个处理单元作为粒度进行扩缩容。如果需要嫌粒度过大,分层次进行扩缩容,先在一个处理单元内部扩展直到极限,之后启动一个新的处理单元。
实现中需要考虑以下几个点:
1、负载均衡
负载均衡发生在系统的每一个层次。
数据接入层与和分发模块之间的采用随机发送的策略以均衡分发模块的压力。
数据拉取和数据推送模块需要动态平衡每个实例上的拉取或推送任务,常用的策略是一致性哈希,以每个任务的实际数据量作为权重。
计算过程是最需要考虑负载均衡的场景,聚合计算通常会遭遇数据倾斜问题,即某些key的数据量远大于其他,这就造成汇聚该Key的任务OOM。下面提供几种常用解决思路:
对于满足交换率和结合率的计算方法,如MAX/MIN/SUM/COUNT等,可以添加多层预聚合,降低最终聚合的数据量。预聚合层次间可以随机方式,最终汇聚之前按Key哈希。
负载均衡或者说任务调度对应Bin Packing等一系列等价的最优化问题。这些问题是NP-Hard的,可以通过近似算法来逼近,典型的有First Fit算法。实现时一般需要自定义计算框架的分区逻辑,如Spark可以通过自定义Partitioner来实现。
2、控制节点扇入扇出规模
无论是具备状态副本的分布式存储系统、基于DAG的分布式计算系统还是Stateless的接入集群,当集群规模持续增大时,节点间交互会显著增大,最差的情况下全连接,扩容节点会有指数级的连接增长。这会严重影响系统对水平扩容能力,扩容带来的收益跟不上单机资源消耗的增长。
对于分布式系统,通过控制集群或者作业规模可以实现一定程度的控制,对于接入模块,可以限制下游连接到上限。
可用性
对于可用性要求高的服务可以多集群热备的方式,在上述架构中,可以通过运行多个并行的处理单元处理相同的数据流来实现。也可以部署整个集群,通过采集端多写的方式复制数据流。最后需要根据输出结果的延迟、准确度等判断策略选择一个计算结果输出。
服务无损升级,可以通过启动一个新的计算单元,并行处理数据,待数据“预热”后,进行切换。
在部署时,接入模块尽可能的靠近数据源,保证每个地域一套。系统多地域部署,每个地域内部模块间尽量自治。接入端发送数据时优先发送本地域,异常时尝试其他地域。模块间交互可以打QoS(服务质量)标签增加网络优先级以降低网络丢包。
监控上,除了基础资源、流量等监控外,最重要的是全通路时延监控,常见方案是构造业务流量,统计在系统中的延迟。这个延迟指标通常是多维度的,根据部署和业务使用情况可能需要关注不同地域,不同业务,不同处理通路的延迟。这些延迟指标,可以指示系统进行流量调度或者资源的重分配。
总 结
本文简单介绍了百度的分布式监控计算系统架构的演进和当前的实时计算架构,并提供了部分常见问题点解决思路。架构是不断在演进的,我们的系统仅仅是“够用”,还有更多的工作需要开展,如架构的轻量化,统一易用的计算表示层,计算的自动优化等。
由于个人水平有限,如果行文中有错误,或者有需要进一步探讨的,请在留言中指出。