Sherlock.IO 是 eBay 现有的监控平台,每天要处理上百亿条日志、事件和指标。Flink Streaming job 实时处理系统用于处理其中的日志和事件。
图片来自 Pexels
本文将结合监控系统 Flink 的现状,具体讲述 Flink 在监控系统上的实践和应用,希望给同业人员一些借鉴和启发。
监控系统 Flink 的现状
eBay 的监控平台 Sherlock.IO 每天处理着上百亿条日志(log),事件(event)和指标(metric)。
通过构建 Flink Streaming job 实时处理系统,监控团队能够及时将日志和事件的处理结果反馈给用户。
当前,监控团队维护着 8 个 Flink 集群,最大的集群规模达到上千个 TaskManager,总共运行着上百个作业(job),一些作业已经稳定运行了半年以上。
元数据驱动
为了让用户和管理员能够更加快捷地创建 Flink 作业并调整参数,监控团队在 Flink 上搭建了一套元数据微服务(metadata service)。
该服务能够用 Json 来描述一个作业的 DAG,且相同的 DAG 共用同一个作业,能够更加方便地创建作业,无需调用 Flink API。
Sherlock.IO 流处理整体的架构如图 1 所示:
图 1:Sherlock.IO 流处理整体架构
目前,用这套元数据微服务创建的作业仅支持以 Kafka 作为数据源,只要数据接入到 Kafka,用户就可以定义 Capability 来处理逻辑从而通过 Flink Streaming 处理数据。
元数据微服务
元数据微服务框架如图 2 所示,最上层是元数据微服务提供的 Restful API, 用户通过调用 API 来描述和提交作业。
图 2:元数据微服务框架
描述作业的元数据包含三个部分:
- Capability
- Policy
- Resource
Flink 适配器(Adaptor)连接了 Flink Streaming API 和元数据微服务 API,且会根据元数据微服务描述的作业调用 Flink Streaming API 来创建作业,从而屏蔽 Flink Stream API。
因此,用户不用了解 Flink Streaming API 就可以创建 Flink 作业。未来如果需要迁移到其他的流处理框架,只要增加一个适配器,就可以将现有的作业迁移到新的流处理框架上。
①Capability
Capability 定义了作业的 DAG 以及每个算子(Operator)所用的 Class,图 3 是事件处理(eventProcess)Capability,它最终会生成如图 4 的 DAG:
图 3:eventESSink Capability
事件处理 Capability 先从 Kafka 读出数据,再写到 Elasticsearch 中。
该 Capability 将该作业命名为“eventProcess”,并定义其并行度为“5”,其算子为“EventEsIndexSinkCapability”, 其数据流为“Source→Sink”。
②Policy
每个命名空间(Namespace)需要定义一个或多个 Policy,每个 Policy 指定了相应的 Capability,即指定了用哪一套 DAG 来运行这个 Policy。
Policy 还定义了这个作业的相关配置,例如从哪个 Kafka topic 中读取数据,写到 ElasticSearch 的哪个索引(Index)中,中间是否要跳过某些算子等等。
其次,Policy 还能作为一个简易的过滤器(Filter),可以通过配置 Jexl 表达式过滤掉一些不需要的数据,提高作业的吞吐量。
另外,我们还实现了 Zookeeper 定时更新的机制,使得 Policy 修改后不再需要重启作业,只要是在更新时间间隔内,该命名空间的 Policy 修改就会被自动应用到作业上。
图 5 是命名空间为 paas 的 Policy 示例:
图 5:paas alertESSink Policy
③Resource
Resource 定义了某个命名空间所需要的资源,比如 Flink 集群, Kafka broker,ES 集群等等。
我们有多个 Flink 集群和 ES 集群,通过 Resource 配置,作业可以知道某个命名空间的日志应该写到哪个 ES 集群,并可以判断该命名空间的数据应该从哪个 Kafka 集群读取。
共享作业
为了减少作业数量,我们可以让相同的 DAG 复用同一个作业。我们先给不同的 Policy 指定相同的 Capability,在该 Capability 资源足够的情况下,这些 Policy 就会被调度到同一个作业上。
以 SQL 的 Capability 为例,每个 Policy 的 SQL 语句不尽相同,如果为每个 Policy 都创建一个作业, Job Manager 的开销就会很大,且不好管理。
因此,我们可以为 SQL Capability 配置 20 个 Slot,每个 Policy 占用一个 Slot。那么该 Capability 生成的作业就可以运行 20 个 Policy。
作业运行时,从 Source 读进来的数据会被打上相应 Policy 的标签,并执行该 Policy 定义的 SQL 语句,从而实现不同 Policy 共享同一个作业,大大减少了作业的数量。
用共享作业还有一个好处:如果多个命名空间的数据在一个 Kafka topic 里,那么只要读一遍数据即可,不用每个命名空间都读一次 topic 再过滤,这样就大大提高了处理的效率。
Flink 作业的优化和监控
了解元数据驱动后,让我们来看看可以通过哪些方法实现 Flink 作业的优化和监控。
Heartbeat
在 Flink 集群的运维过程中,我们很难监控作业的运行情况。即使开启了检查点(checkpoint),我们也无法确定是否丢失数据或丢失了多少数据。因此,我们为每个作业注入了 Heartbeat 以监控其运行情况。
Heartbeat 就像 Flink 中用来监控延迟的“LatencyMarker”一样,它会流过每个作业的管道。
但与 LatencyMarker 不同的是,当 Heartbeat 遇到 DAG 的分支时,它会分裂并流向每个分支,而不像 LatencyMarker 那样随机流向某一个分支。
另一个不同点在于 Heartbeat 不是由 Flink 自身产生,而是由元数据微服务定时产生,而后由每个作业消费。
如上文中图 4 所示,每个作业在启动的时候会默认加一个 Heartbeat 的数据源。
Heartbeat 流入每个作业后,会随数据流一起经过每个节点,在每个节点上打上当前节点的标签,然后跳过该节点的处理逻辑流向下个节点。
直到 Heartbeat 流到最后一个节点时,它会以指标(Metric)的形式发送到 Sherlock.IO(eBay 监控平台)。
该指标包含了 Heartbeat 产生的时间,流入作业的时间以及到达每个节点的时间。
通过这个指标,我们可以判断该作业在读取 Kafka 时是否延时,以及一条数据被整个管道处理所用的时间和每个节点处理数据所用的时间,进而判断该作业的性能瓶颈。
由于 Heartbeat 是定时发送的,因此每个作业收到的 Heartbeat 个数应该一致。若最后发出的指标个数与期望不一致,则可以进一步判断是否有数据丢失。
图 6 描述了某 Flink 作业中的数据流以及 Heartbeat 的运行状态:
图 6:Heartbeat 在作业中的运行过程
可用性
有了 Heartbeat,我们就可以用来定义集群的可用性。首先,我们需要先定义在什么情况下属于不可用的:
①Flink 作业重启
当内存不足(OutofMemory)或代码运行错误时,作业就可能会意外重启。我们认为重启过程中造成的数据丢失是不可用的情况之一。因此我们的目标之一是让 Flink 作业能够长时间稳定运行。
②Flink 作业中止
有时因为基础设施的问题导致物理机或者容器没启动起来,或是在 Flink 作业发生重启时由于 Slot 不够而无法启动,或者是因为 Flink 作业的重启次数已经超过了最大重启次数(rest.retry.max-attempts),Flink 作业就会中止。
此时需要人工干预才能将作业重新启动起来。我们认为 Flink 作业中止时,也是不可用的情况之一。
③Flink 作业在运行中不再处理数据
发生这种情况,一般是因为遇到了反压(BackPressure)。造成反压的原因有很多种,比如上游的流量过大,或者是中间某个算子的处理能力不够,或者是下游存储节点遇到性能瓶颈等等。
虽然短时间内的反压不会造成数据丢失,但它会影响数据的实时性,最明显的变化是延迟这个指标会变大。
我们认为反压发生时是不可用的情况之一。针对以上三种情况,我们都可以用 Heartbeat 来监控,并计算可用性。
比如第一种情况,如果作业重启时发生了数据丢失,那么相应的那段管道的 Heartbeat 也会丢失,从而我们可以监测出是否有数据丢失以及粗粒度地估算数据丢了多少。
对于第二种情况,当作业中止时,HeartBeat 也不会被处理,因此可以很快发现作业停止运行并让 on-call 及时干预。
第三种情况当反压发生时,HeartBeat 也会被阻塞在发生反压的上游,因此 on-call 也可以很快地发现反压发生并进行人工干预。
综上,Heartbeat 可以很快监测出 Flink 作业的运行情况。那么,如何评估可用性呢?
由于 Heartbeat 是定时发生的,默认情况下我们设置每 10 秒发一次。1 分钟内我们期望每个作业的每条管道能够发出 6 个带有作业信息的 Heartbeat,那么每天就可以收到 8640 个 Heartbeat。
因此,一个作业的可用性可以定义为:
Flink 作业隔离
Slot 是 Flink 运行作业的最小单位[1],每个 TaskManager 可以分配一个至多个 Slot(一般分配的个数为该 TaskManager 的 CPU 数)。
根据 Flink 作业的并行度,一个作业可以分配到多个 TaskManager 上,而一个 TaskManager 也可能运行着多个作业。
然而,一个 TaskManager 就是一个 JVM,当多个作业分配到一个 TaskManager 上时,就会有抢夺资源的情况发生。
例如,我一个 TaskManager 分配了 3 个 Slot(3 个 CPU)和 8G 堆内存。
当 JobManager 调度作业的时候,有可能将 3 个不同作业的线程调度到该 TaskManager 上,那么这 3 个作业就会同时抢夺 CPU 和内存的资源。当其中一个作业特别耗 CPU 或内存的时候,就会影响其他两个作业。
在这种情况下,我们通过配置 Flink 可以实现作业的隔离,如图 7 所示:
图 7:Flink 作业隔离前后的调度图
通过以上配置,可以限定每个 TaskManager 独占 CPU 和内存的资源,且不会多个作业抢占,实现作业之间的隔离。
反压
我们运维 Flink 集群的时候发现,出现最多的问题就是反压。在 3.2 中提到过,发生反压的原因有很多种,但无论什么原因,数据最终都会被积压在发生反压上游的算子的本地缓冲区(localBuffer)中。
我们知道,每一个 TaskManager 有一个本地缓冲池, 每一个算子数据进来后会把数据填充到本地缓冲池中,数据从这个算子出去后会回收这块内存。
当被反压后,数据发不出去,本地缓冲池内存就无法释放,导致一直请求缓冲区(requestBuffer)。
由于 Heartbeat 只能监控出是否发生了反压,但无法定位到是哪个算子出了问题。
因此我们定时地将每个算子的 StackTrace 打印出来,当发生反压时,通过 StackTrace 就可以知道是哪个算子的瓶颈。
如图8所示,我们可以清晰地看到发生反压的 Flink 作业及其所在的 Taskmanager。再通过 Thread Dump,我们就可以定位到代码的问题。
图 8:发生反压的StackTrace
其他监控手段
Flink 本身提供了很多有用的指标[2]来监控 Flink 作业的运行情况,在此基础上我们还加了一些业务上的指标。除此之外,我们还使用了以下工具监控 Flink 作业。
①History server
Flink 的 History server[3]可以查询已完成作业的状态和指标。比如一个作业的重启次数、它运行的时间。
我们常常用它找出运行不正常的作业。比如,我们可以通过 History server 的 Attempt 指标知道每个作业重启的次数,从而快速去现场找到重启的原因,避免下次再发生。
②监控作业和集群
虽然 Flink 有 HA 的模式,但在极端情况下,例如整个集群出现问题时,需要 on-call 即时发觉并人工干预。
我们在元数据微服务中保存了最后一次提交作业成功的元数据,它记录了在每个 Flink 集群上应该运行哪些作业。
守护线程(Daemon thread)会每分钟去比较这个元数据和 Flink 上运行的作业,若发现 JobManager 连不通或者有作业运行不一致则立刻发出告警(Alert)通知 on-call。
实例
下面介绍几个已经运行在监控系统上的 Flink 流处理系统的应用:
Event Alerting
当前监控团队是基于 Flink Streaming 做事件告警(Event alerting),我们定义了一个告警算子 EventAlertingCapability,该 Capability 可以处理每个 Policy 自定义的规则。
如图 9 定义的一条性能监控规则:
该规则的含义是当性能检测器的应用为“r1rover”, 主机以“r1rover”开头,且数值大于 90 时,就触发告警。且生成的告警会发送到指定的 Kafka topic 中供下游继续处理。
图 9:Single-Threshold1 Policy
Eventzon
Eventzon 就像 eBay 的事件中心,它收集了从各个应用,框架,基础架构发过来的事件,最后通过监控团队的 Flink Streaming 实时生成告警。
由于各个事件的数据源不同,它们的元数据也不同,因此无法用一条统一的规则来描述它。
我们专门定义了一套作业来处理 Eventzon 的事件,它包含了多个 Capability,比如 Filter Capability,用来过滤非法的或者不符合条件的事件;又比如 Deduplicate Capability,可以用来去除重复的事件。
Eventzon 的所有事件经过一整套作业后,会生成有效的告警,并根据通知机制通过 E-mail、Slack 或 Pagerduty 发给相关团队。
Netmon
Netmon 的全称为 Network Monitoring, 即网络监控,它可以用来监控整个 eBay 网络设备的健康状态。它的数据源来自 eBay 的交换机,路由器等网络设备的日志。
Netmon 的作用是根据这些日志找出一些特定的信息,往往是一些错误的日志,以此来生成告警。
eBay 的每一台设备都要“登记造册”,每台设备将日志发过来后,我们通过 EnrichCapability 从“册子”中查询这台设备的信息,并把相关信息比如 IP 地址,所在的数据中心,所在的机架等填充到日志信息中作为事件保存。
当设备产生一些特定的错误日志时, 它会被相应的规则匹配然后生成告警,该告警会被 EventProcess Capability 保存到 Elasticsearch 中实时显示到 Netmon 的监控平台(dashboard)上。
有时因为网络抖动导致一些短暂的错误发生,但系统过一会儿就会自动恢复。
当上述情况发生时,Netmon 会有相应的规则将发生在网络抖动时生成的告警标记为“已解决”(Resolved)。
对于一些必须人工干预的告警,运维人员可以通过网络监控平台(Netmon dashboard)手动点击“已解决”,完成该告警的生命周期。
总结与展望
eBay 的监控团队希望能根据用户提供的指标、事件和日志以及相应的告警规则实时告警用户。
Flink Streaming 能够提供低延时的处理从而能够达到我们低延时的要求,并且它适合比较复杂的处理逻辑。
然而在运维 Flink 的过程中,我们也发现了由于作业重启等原因导致误报少报告警的情况发生,从而误导客户。因此今后我们会在 Flink 的稳定性和高可用性上投入更多。
我们也希望在监控指标、日志上能够集成一些复杂的 AI 算法,从而能够生成更加有效精确的告警,成为运维人员的一把利器。
参考文献:
[1]https://ci.apache.org/projects/flink/flink-docs-release-1.7/concepts/runtime.html#task-slots-and-resources
[2]https://ci.apache.org/projects/flink/flink-docs-release-1.7/monitoring/metrics.html
[3]https://ci.apache.org/projects/flink/flink-docs-release-1.4/monitoring/historyserver.html