大数据技术深度实践
内容简介
随着技术迭代的不断加速,大数据极大改变了行业领域对信息流动的限制。本期我们聚焦2017年领域内热门技术与应用实践,带领大家深度解析大数据技术难点和发展趋势。厉兵秣马今点将,群雄逐鹿正当时。
本书内容
Heron:Twitter 的新一代流处理引擎原理篇
文 /吕能,吴惠君,符茂松
本文介绍了流计算的背景和重要概念,并详细分析了 Twitter 目前的流计算引擎—— Heron的结构及重要组件,希望能借此为大家提供一些在设计和构建流计算系统时的经验。
流计算又称实时计算,是继以 Map-Reduce 为代表的批处理之后的又一重要计算模型。随着互联网业务的发展以及数据规模的持续扩大,传统的批处理计算难以有效地对数据进行快速低延迟处理并返回结果。由于数据几乎处于不断增长的状态中,及时处理计算大批量数据成为了批处理计算的一大难题。在此背景之下,流计算应运而生。相比于传统的批处理计算,流计算具有低延迟、高响应、持续处理的特点。在数据产生的同时,就可以进行计算并获得结果。更可以通过 Lambda 架构将即时的流计算处理结果与延后的批处理计算结果结合,从而较好地满足低延迟、高正确性的业务需求。
Twitter 由于本身的业务特性,对实时性有着强烈的需求。因此在流计算上投入了大量的资源进行开发。第一代流处理系统 Storm 发布以后得到了广泛的关注和应用。根据 Storm 在实践中遇到的性能、规模、可用性等方面的问题,Twitter 又开发了第二代流处理系统——Heron,并在2016年将它开源。
重要概念定义
在开始了解 Heron 的具体架构和设计之前,我们首先定义一些流计算以及在 Heron 设计中用到的基本概念:
-
Tuple:流计算任务中处理的最小单元数据的抽象。
-
Stream:由无限个 Tuple 组成的连续序列。
-
Spout:从外界数据源获得数据并生成 Tuple 的计算任务。
-
Bolt:处理上游 Spout 或者 Bolt 生成的 Tuple 的计算任务。
-
Topology:一个通过 Stream 将 Spout 和 Bolt 相连的处理 Tuple 的逻辑计算任务。
-
Grouping:流计算中的 Tuple 分发策略。在 Tuple 通过 Stream 传递到下游Bolt 的过程中,Grouping 策略决定了如何将一个 Tuple 路由给一个具体的Bolt 实例。典型的 Grouping 策略有:随机分配、基于 Tuple 内容的分配等。
-
Physical Plan:基于 Topology 定义的逻辑计算任务以及所拥有的计算资源,生成的实际运行时信息的集合。
在以上流处理基本概念的基础上,我们可以构建出流处理的三种不同处理语义:
-
至多一次(At-Most-Once): 尽可能处理数据,但不保证数据一定会被处理。吞吐量大,计算快但是计算结果存在一定的误差。
-
至少一次(At-Least-Once):在外部数据源允许 Replay(重演)的情况下,保证数据至少被处理一次。在出现错误的情况下会重新处理该数据,可能会出现重复处理多次同一数据的情况。保证数据的处理但是延迟升高。
-
仅有一次(Exactly-Once):每一个数据确保被处理且仅被处理一次。结果精确但是所需要的计算资源增多并且还会导致计算效率降低。
从上可知,三种不同的处理模式有各自的优缺点,因此在选择处理模式的时候需要综合考量一个 Topology 对于吞吐量、延迟、结果误差、计算资源的要求,从而做出最优的选择。目前的 Heron 已经实现支持至多一次和至少一次语义,并且正在开发对于仅有一次语义的支持。
Heron 系统概览
保持与 Storm 接口(API)兼容是Heron的设计目标之一。因此,Heron 的数据模型与 Storm 的数据模型基本保持一致。每个提交给 Heron 的 Topology 都是一个由 Spout 和 Bolt 这两类结点(Vertex)组成的,以 Stream 为边(Edge)的有向无环图(Directed acyclic graph)。其中 Spout 结点是 Topology 的数据源,它从外部读取 Topology 所需要处理的数据,常见的如 kafka-spout,然后发送给后续的 Bolt 结点进行处理。Bolt 节点进行实际的数据计算,常见的运算如 Filter、Map 以及 FlatMap 等。
我们可以把 Heron 的 Topology 类比为数据库的逻辑查询计划。这种逻辑上的计划最后都要变成实质上的处理计划才能执行。用户在编写 Topology 时指定每个 Spout 和 Bolt 任务的并行度和 Tuple 在 Topology 中结点间的分发策略(Grouping)。所有用户提供的信息经过打包算法(Pakcing)的计算,这些 Spout 和 Bolt 任务(task)被分配到一批抽象容器中。最后再把这些抽象容器映射到真实的容器中,就可以生成一个物理上可执行的计划(Physical plan),它是所有逻辑信息(拓扑图、并行度、计算任务)和运行时信息(计算任务和容器的对应关系、实际运行地址)的集合。
整体结构
总体上,Heron 的整体架构如图1所示。用户通过命令行工具(Heron-CLI)将 Topology 提交给 Heron Scheduler。再由 Scheduler 对提交的 Topology 进行资源分配以及运行调度。在同一时间,同一个资源平台上可以运行多个相互独立 Topology。
与 Storm 的 Service 架构不同,Heron 是 Library 架构。Storm 在架构设计上是基于服务的,因此需要设立专有的 Storm 集群来运行用户提交的 Topology。在开发、运维以及成本上,都有诸多的不足。而 Heron 则是基于库的,可以运行在任意的共享资源调度平台上。最大化地降低了运维负担以及成本开销。
目前的 Heron 支持 Aurora、YARN、Mesos 以及 EC2,而 Kubernetes 和 Docker 等目前正在开发中。通过可扩展插件 Heron Scheduler,用户可以根据不同的需求及实际情况选择相应的运行平台,从而达到多平台资源管理器的支持。
而被提交运行 Topology 的内部结构如图2所示,不同的计算任务被封装在多个容器中运行。这些由调度器调度的容器可以在同一个物理主机上,也可分布在多个主机上。其中每一个 Topology 的第一个容器(容器0)负责整个 Topology 的管理工作,主要运行一个 Topology Master 进程;其余各个容器负责用户提交的计算逻辑的实现,每个容器中主要运行一个 Stream Manager 进程,一个 Metrics Manager 进程,以及多个 Instance 进程。每个 Instance 都负责运行一个 Spout 或者 Bolt 任务(task)。对于 Topology Master、Stream Manager 以及 Instance 进程的结构及重要功能,我们会在本文的后面章节进行详细的分析。
状态(State)存储和监控
Heron 的 State Manager 是一个抽象的模块,它在具体实现中可以是 ZooKeeper 或者是文件系统。它的主要作用是保存各个 Topology 的各种元信息:Topology 的提交者、提交时间、运行时生成的 Physical Plan 以及 Topology Master 的地址等,从而为 Topology 的自我恢复提供帮助。
每个容器中的 Metrics Manager 负责收集所在容器的运行时状态指标(Metrics),并上传给监控系统。当前 Heron 版本中,简化的监控系统集成在 Topology Master 中。将来这一监控模块将会成为容器0中的一个独立进程。Heron 还提供 Heron-Tracker 和 Heron-UI 这两个工具来查看和监测一个数据中心中运行的所有 Topology。
启动过程
在一个 Topology 中,Topology Master 是整个 Topology 的元信息管理者,它维护着完整的 Topology 元信息。而 Stream Manager 是每个容器的网关,它负责各个 Instance 之间的数据通信,以及和 Topology Master 之间的控制信令。
当用户提交 Topology 之后,Scheduler 便会开始分配资源并运行容器。每个容器中启动一个 Heron Executor 的进程,它区分容器0和其他容器,分别启动 Topology Master 或者 Stream Manager 等进程。在一个普通容器中,Instance 进程启动后会主动向本地容器的 Stream Manager 进行注册。当 Stream Manager 收到所有 Instance 的注册请求后,会向 Topology Master 发送包含了自己的所负责的 Instance 的注册信息。当 Topology Master 收到所有 Stream Manager 的注册信息以后,会生成一个各个 Instance,Stream Manager 的实际运行地址的 Physical Plan 并进行广播分发。收到了 Physical Plan 的各个 Stream Manager 之间就可以根据这一 Physical Plan 互相建立连接形成一个完全图,然后开始处理数据。
Instance 进行具体的 Tuple 数据计算处理。Stream Manager 则不执行具体的计算处理任务,只负责中继转发 Tuple。从数据流网络的角度,可以把 Stream Manager 理解为每个容器的路由器。所有 Instance 之间的 Tuple 传递都是通过 Stream Manager 中继。因此容器内的 Instance 之间通信是一跳(hop)的星形网络。所有的 Stream Manager 都互相连接,形成 Mesh 网络。容器之间的通信也是通过 Stream Manager 中继的,是通过两跳的中继完成的。
核心组件分析
TMaster
TMaster 是 Topology Master 的简写。与很多 Master-Slave 模式分布式系统中的 Master 单点处理控制逻辑的作用相同,TMaster 作为 Master 角色提供了一个全局的接口来了解 Topology 的运行状态。同时,通过将重要的状态信息(Physical Plan)等记录到 ZooKeeper 中,保证了 TMaster 在崩溃恢复之后能继续运行。
实际产品中的 TMaster 在启动的时候,会在 ZooKeeper 的某一约定目录中创建一个 Ephemeral Node 来存储自己的 IP 地址以及端口,让 Stream Manager 能发现自己。Heron 使用 Ephemeral Node 的原因包括:
- 避免了一个 Topology 出现多个 TMaster 的情况。这样就使得这个 Topology 的所有进程都能认定同一个 TMaster;
- 同一 Topology 内部的进程能够通过 ZooKeeper 来发现 TMaster 所在的位置,从而与其建立连接。
TMaster 主要有以下三个功能:
- 构建、分发并维护 Topology 的 Physical Plan;
- 收集各个 Stream Manager 的心跳,确认 Stream Manager 的存活;
- 收集和分发 Topology 部分重要的运行时状态指标(Metrics)。
由于 Topology 的 Physical Plan 只有在运行时才能确定,因此 TMaster 就成为了构建、分发以及维护 Physical Plan 的最佳选择。在 TMaster 完成启动和向 ZooKeeper 注册之后,会等待所有的 Stream Manager 与自己建立连接。在 Stream Manager 与 TMaster 建立连接之后,Stream Manager 会报告自己的实际 IP 地址、端口以及自己所负责的 Instance 地址与端口。TMaster 在收到所有 Stream Manager 报告的地址信息之后就能构建出 Physical Plan 并进行广播分发。所有的 Stream Manager 都会收到由 TMaster 构建的 Physical Plan,并且根据其中的信息与其余的 Stream Manager 建立两两连接。只有当所有的连接都建立完成之后,Topology 才会真正开始进行数据的运算和处理。当某一个 Stream Manager 丢失并重连之后,TMaster 会检测其运行地址及端口是否发生了改变;若改变,则会及时地更新 Physical Plan 并广播分发,使 Stream Manager 能够建立正确的连接,从而保证整个 Topology 的正确运行。
TMaster 会接受 Stream Manager 定时发送的心跳信息并且维护各个 Stream Manager 的最近一次心跳时间戳。心跳首先能够帮助 TMaster 确认 Stream Manager 的存活,其次可以帮助其决定是否更新一个 Stream Manager 的连接并且更新 Physical Plan。
TMaster 还会接受由 Metrics Manager 发送的一部分重要 Metrics 并且向 Heron-Tracker 提供这些 Metrics。Heron-Tracker 可以通过这些 Metrics 来确定 Topology 的运行情况并使得 Heron-UI 能够基于这些重要的 Metrics 来进行监控检测。典型的 Metrics 有:分发 Tuple 的次数,计算 Tuple 的次数以及处于 backpressure 状态的时间等。
非常值得注意的一点是,TMaster 本身并不参与任何实际的数据处理。因此它也不会接受和分发任何的 Tuple。这一设计使得 TMaster 本身逻辑清晰,也非常轻量,同时也为以后功能的拓展留下了巨大的空间。
Stream Manager 和反压(Back pressure)机制
Stmgr 是 Stream Manager 的简写。Stmgr 管理着 Tuple 的路由,并负责中继 Tuple。当 Stmgr 拿到 Physical Plan 以后就能根据其中的信息知道与其余的 Stmgr 建立连接形成 Mesh 网络,从而进行数据中继以及 Backpressure 控制。Tuple 传递路径可以通过图3来说明,图3中容器1的 Instance D(1D)要发送一个 Tuple 给容器4中的 Instance C(4C),这个 Tuple 经过的路径为:容器1的1D,容器1的 Stmgr,容器4的 Stmgr,容器4的4C。又比如从3A到3B的 Tuple 经过的路径为:3A,容器3的 Stmgr,3B。与 Internet 的路由机制对比,Heron 的路由非常简单,这得益于 Stmgr 之间两两相连,使得所有的 Instance 之间的距离不超过2跳。
Acking
Stmgr 除了路由中继 Tuple 的功能以外,它还负责确认(Acking)Tuple 已经被处理。Acking 的概念在 Heron 的前身 Storm 中已经存在。Acking 机制的目的是为了实现 At-Least-Once 的语义。原理上,当一个 Bolt 实例处理完一个 Tuple 以后,这个 Bolt 实例发送一个特殊的 Acking Tuple 给这个 bolt 的上游 Bolt 实例或者 Spout 实例,向上游结点确认 Tuple 已经处理完成。这个过程层层向上游结点推进,直到 Spout 结点。实现上,当 Acking Tuple 经过 Stmgr 时候由异或(xor)操作标记 Tuple,由异或操作的特性得知是否处理完成。当一个 Spout 实例在一定时间内还没有收集到 Acking Tuple,那么它将重发对应的数据 Tuple。Heron 的 Acking 机制的实现与它的前任 Storm 一致。
Back Pressure
Heron 引入了反压(Back Pressure)机制,来动态调整 Tuple 的处理速度以避免系统过载。一般来说,解决系统过载问题有三种策略:1. 放任不管;2. 丢弃过载数据;3. 请求减少负载。Heron 采用了第三种策略,通过 Backpressure 机制来进行过载恢复,保证系统不会在过载的情况下崩溃。
Backpressure 机制触发过程如下:当某一个 Bolt Instance 处理速度跟不上 Tuple 的输入速度时,会造成负责向该 Instance 转发 Tuple 的 Stmgr 缓存不断堆积。当缓存大小超过一个上限值(Hight Water Mark)时,该 Stmgr 会停止从本地的 Spout 中读取 Tuple 并向 Topology 中的其他所有 Stmgr 发送一个“开始 Backpressure”的信息。而其余的 Stmgr 在接收到这一消息时也会停止从他们所负责的 Spout Instance 处读取并转发 Tuple。至此,整个 Topology 就不再从外界读入 Tuple 而只处理堆积在内部的未处理 Tuple。而处理的速度则由最慢的 Instance 来决定。在经过一定时间的处理以后,当缓存的大小减低到一个下限值(Low Water Mark)时,最开始发送“开始 Backpressure”的 Stmgr 会再次发送“停止 Backpressure”的信息,从而使得所有的 Stmgr 重新开始从 Spout Instance 读取分发数据。而由于 Spout 通常是从具有允许重演(Replay)的消息队列中读取数据,因此即使冻结了也不会导致数据的丢失。
注意在 Backpressure 的过程中两个重要的数值:上限值(High Water Mark)和下限值(Low Water Mark)。只有当缓存区的大小超过上限值时才会触发 Backpressure,然后一直持续到缓存区的大小减低到下限值时。这一设计有效地避免了一个 Topology 不停地在 Backpressure 状态和正常状态之间震荡变化的情况发展,一定程度上保证了 Topology 的稳定。
Instance
Instance 是整个 Heron 处理引擎的核心部分之一。Topology 中不论是 Spout 类型结点还是 Bolt 类型结点,都是由 Instance 来实现的。不同于 Storm 的 Worker 设计,在当前的 Heron 中每一个 Instance 都是一个独立的 JVM 进程,通过 Stmgr 进行数据的分发接受,完成用户定义的计算任务。独立进程的设计带来了一系列的优点:便于调试、调优、资源隔离以及容错恢复等。同时,由于数据的分发传送任务已经交由 Stmgr 来处理,Instance 可以用任何编程语言来进行实现,从而支持各种语言平台。
Instance 采用双线程的设计,如图4所示。一个 Instance 的进程包含 Gateway 以及 Task Execution 这两个线程。Gateway 线程主要控制着 Instance 与本地 Stmgr 和 Metrics Manager 之间的数据交换。通过 TCP 连接,Gateway 线程:1. 接受由 Stmgr 分发的待处理 Tuple;2. 发送经 Task Execution 处理的 Tuple 给 Stmgr;3. 转发由 Task Execution 线程产生的 Metrics 给 Metrics Manager。不论是 Spout 还是 Bolt,Gateway 线程完成的任务都相同。
Task Execution 线程的职责是执行用户定义的计算任务。对于 Spout 和 Bolt,Task Execution 线程会相应地去执行 open()和 prepare()方法来初始化其状态。如果运行的 Instance 是一个 Bolt 实例,那么 Task Execution 线程会执行 execute()方法来处理接收到的 Tuple;如果是 Spout,则会重复执行 nextTuple()方法来从外部数据源不停地获取数据,生成 Tuple,并发送给下游的 Instance 进行处理。经过处理的 Tuple 会被发送至 Gateway 线程进行下一步的分发。同时在执行的过程中,Task Execution 线程会生成各种 Metrics(tuple 处理数量,tuple 处理延迟等)并发送给 Metrics Manager 进行状态监控。
Gateway 线程和 Task Execution 线程之间通过三个单向的队列来进行通信,分别是数据进入队列、数据发送队列以及 Metrics 发送队列。Gateway 线程通过数据进入队列向 Task Execution 线程传入 Tuple;Task Execution 通过数据发送队列将处理完的 Tuple 发送给 Gateway 线程;Task Execution 线程通过 Metrics 发送队列将收集的 Metric 发送给 Gateway 线程。
总结
在本文中,我们介绍了流计算的背景和重要概念,并且详细分析了 Twitter 目前的流计算引擎—— Heron 的结构及重要组件。希望能借此为大家提供一些在设计和构建流计算系统时的经验,也欢迎大家向我们提供建议和帮助。如果大家对 Heron 的开发和改进感兴趣,可以在 Github 上进行查看。