天天看点

【flink】Flink 中的木桶效应:单个 subtask 卡死导致整个任务卡死1.概述1、结论2、原理分析3、问题定位过程4、 关于 Flink 物理分区策略的思考Flink 欠缺的一种负载均衡策略5、总结

【flink】Flink 中的木桶效应:单个 subtask 卡死导致整个任务卡死1.概述1、结论2、原理分析3、问题定位过程4、 关于 Flink 物理分区策略的思考Flink 欠缺的一种负载均衡策略5、总结

1.概述

转载:Flink 中的木桶效应:单个 subtask 卡死导致整个任务卡死

工作或者面试中一般都要求面试者有较强的独立解决问题的能力,解决问题的前提是:我们对相应组件的原理非常清楚。本文先讲述原理,再结合实战分析一个线上任务的异常案例。

本文分以下几个部分:

第一部分直接给出结论

第二部分会分析原理:Flink 中单个 subtask 卡死,为什么会导致整个任务卡死?

第三部分:线上业务如果出现类似问题如何定位?换言之,线上出现哪些现象可以说明是单个 subtask 导致整个任务卡住了。会通过案例结合 Metric jstack 等定位问题的根本原因。

第四部分对 Flink 现有物理分区策略的思考

第五部分总结

1、结论

keyBy 或 rebalance 下游的算子,如果单个 subtask 完全卡死,会把整个 Flink 任务卡死

通过反压可以确定哪个 Task 出现性能瓶颈

通过 inPoolUsage 指标可以确定下游 Task 的哪个 Subtask 出现性能瓶颈

Flink 现有的物理分区策略全是静态的负载均衡策略,没有动态根据负载能力进行负载均衡的策略

2、原理分析

2.1 分析一个简单的 Flink 任务

如下图所示,任务由 Source → map → KeyBy → Sink 四个算子组成。其中 keyBy 和 Sink 算子之间存在 shuffle,图中相同颜色的箭头表示到达 Sink 中相同的 subtask。其中 subtask A0 和 A1 都要给 TaskB 的 3 个 subtask 发送数据。

【flink】Flink 中的木桶效应:单个 subtask 卡死导致整个任务卡死1.概述1、结论2、原理分析3、问题定位过程4、 关于 Flink 物理分区策略的思考Flink 欠缺的一种负载均衡策略5、总结

任务执行图

2.2 任务运行过程中,具体的数据传输过程

如下图所示,上游每个 Subtask 中会有 3 个 resultSubPartition,连接下游算子的 3 个 subtask。下游每个 subtask 会有 2 个 InputChannel,连接上游算子的 2 个 subtask。在正常运行过程中如果没有反压,所有的 buffer pool 是用不完的。就像下图一样,所有的 InputChannel 并没有占满,公共的 buffer pool 中也几乎没有数据。

【flink】Flink 中的木桶效应:单个 subtask 卡死导致整个任务卡死1.概述1、结论2、原理分析3、问题定位过程4、 关于 Flink 物理分区策略的思考Flink 欠缺的一种负载均衡策略5、总结

正常的数据传输

2.3 Subtask B0 卡死后数据传输发生的现象

假设由于某些原因 Subtask B0 长时间地处理非常慢甚至卡死,其他的 Subtask 都正常,会出现下图中的现象。

【flink】Flink 中的木桶效应:单个 subtask 卡死导致整个任务卡死1.概述1、结论2、原理分析3、问题定位过程4、 关于 Flink 物理分区策略的思考Flink 欠缺的一种负载均衡策略5、总结

下游其中一个 subtask 反压严重

2.3.1 现象描述

1、Subtask B0 内的 A0 和 A1 两个 InputChannel 会被占满

2、Subtask B0 公共的 BufferPool 中可申请到的空间也被占满

3、Subtask A0 和 A1 的 B0 ResultSubPartition 被占满

4、Subtask A0 和 A1 公共的 BufferPool 中可申请到的空间也被占满

5、Subtask B1 和 B2 的所有 InputChannel 和 BufferPool 都是空的

6、Subtask A0 和 A1 的 B1、B2 ResultSubPartition 都是空的

2.3.2 现象解释 ☆☆☆☆☆

Subtask B0 卡死了,不再处理数据或者处理的超级慢。上游如果一直给 Subtask B0 发送数据,必然会导致 Subtask B0 的所有 InputChannel 占满,最后导致公共的 BufferPool 中可申请到的空间也被占满。也就是现象中的 1、2 两点。

虽然 Subtask B0 的所有 Buffer 占满后,Subtask A0 和 A1 仍然在生产数据,此时必然不能发送数据到 B0,所以就会把 Subtask A0 和 A1 中 Subtask B0 对应的 buffer 给占满(也就是 Flink 中反压传递的过程),最后再把 Subtask A0 和 A1 公共的 BufferPool 中可申请到的空间也占满。也就是现象中的 3、4 两点。

其中 1、2、3、4 这四点比较容易理解,关键是 5、6 两点,即:Subtask B0 卡死会什么导致 Subtask B1 和 B2 完全没有数据了?

Subtask B1 和 B2 在整个上下游的 buffer 都是空的,理论来讲只要有空余的 buffer,就可以用来传输数据。但实际上并没有将 Subtask A0 和 A1 的数据传输给 Subtask B1 和 B2。

「这里的根本原因是:

Subtask A0 和 A1 的主线程完全卡死压根不会生产数据了。

既然不会生产数据了,那么 Subtask A0 和 A1 的下游就算 buffer 空着,也是没有意义的。所以就出现 5、6 的现象。

重点解释:为什么 Subtask A0 和 A1 的主线程会卡死?A0 和 A1 是一样的,下面单独分析 A0。Subtask A0 处理数据流程图如下所示:

【flink】Flink 中的木桶效应:单个 subtask 卡死导致整个任务卡死1.概述1、结论2、原理分析3、问题定位过程4、 关于 Flink 物理分区策略的思考Flink 欠缺的一种负载均衡策略5、总结

Subtask A0 处理数据流程

Subtask A0 的主线程会从上游读取数据消费,按照数据的 KeyBy 规则,将数据发送到 B0、B1、B2 三个 outputBuffer 中。现在我们可以看到 B0 对应的 buffer 占满了,且 B0 在公共的 BufferPool 中可申请到的空间也被占满。现在主线程在处理数据,假设这条数据根据 KeyBy 分区规则后,应该分配给 Subtask B0 处理,那么主线程必须把数据放到 B0 这个 buffer 中。但是现在 buffer 没有空间了,所以主线程就会卡在申请 buffer 上,直到可以再申请到 buffer(这也是 Flink 反压的实现原理)。

同理 Subtask A1 也会出现这样的问题,如果 Task A 的并行度是 1000,那么 Subtask B0 也会将上游 1000 个 Subtask A 全部卡住。最后导致整个任务全部卡住。

原理弄懂了,下一阶段要搞懂线上出现哪些现象可以说明是单个 subtask 导致整个任务卡住了。线上业务如果出现类似问题如何定位?

2.4 小结

其实不只是 keyBy 场景会出现上述问题,rebalance 场景也会出现上述问题。rebalance 分区策略表示,上游 subtask 以轮询的策略向下游所有 subtask 发送数据,即:subtask A0 会先给 subtask B0 发一条,下一条发给 B1,下一条再发给 B2,再发给 B0 依次类推:B0、B1、B2、B0、B1、B2、B0、B1、B2。。。

一旦 B0 卡死,最终主线程肯定因为 B0 把 Subtask A 内的 buffer 用完了,导致主线程卡住。

「所以总结成一句话就是:

keyBy 或 rebalance 下游的算子,如果单个 subtask 完全卡死,会把整个 Flink 任务卡死

。」

3、问题定位过程

3.1 业务场景

业务反馈一个写 ES 的任务跑一会就没输出了,完全卡死,一条输出都没有。80 并发完全正常,可以正常输出,调大并发到 100 以后,运行一会就没有输出了。

DAG 图如下所示,上下游算子之间的数据分区策略是 rebalance。

【flink】Flink 中的木桶效应:单个 subtask 卡死导致整个任务卡死1.概述1、结论2、原理分析3、问题定位过程4、 关于 Flink 物理分区策略的思考Flink 欠缺的一种负载均衡策略5、总结

任务 DAG

3.2 思考及定位过程

听到业务方的反馈,看到作业 DAG 图,开始定位问题,笔者并没有想到第二部分那么多的原理分析,因为大部分的任务卡住并不是因为单个 Subtask 卡住导致整个任务卡住。所以下面的定位过程完全是以一个旁观者的角度触发,也是笔者当时定位问题的一个完整过程。笔者作为平台方,也是完全不清楚业务逻辑的,只是从 Flink 的角度来定位问题。

3.2.1 从 DAG 上来看任务有两个 Task,到底是上游 Task 有问题还是下游 Task 有问题

如何定位上游 Task 还是下游有问题很简单:看一下上游 Task 是否有反压,如果下游 Task 卡死或者消费慢,上游 Task 肯定反压比较严重。所以判定依据:

  • 如果上游 Task 反压严重,则表示下游 Task 有问题
  • 如果上游 Task 没有反压,大概率是上游 Task 有问题

查看后,发现上游 Task 的所有 Subtask 反压都非常严重,所以断定下游 Task 有问题。

3.2.2 下游 Task 发生了什么?在干嘛?

要想知道下游 Task (ES Sink)在干嘛,很简单:查看现场,随便选一个 Subtask 打个 jstack,看看当前进程在做什么。

下游 Task 总共 100 个并行度,随便找了一个 Subtask 打 jstack,发现当前 Subtask 处理数据的主线程卡在 poll 数据。即:ES Sink 的当前 Subtask 不输出数据竟然是因为上游不发送数据了。为了确认当前 Subtask 接收不到上游算子发送的数据,又看了当前 Subtask 的 Metric:inPoolUsage。inPoolUsage 表示当前算子输入缓冲区的使用率,inPoolUsage 持续为 0 证实了当前 Subtask 确实接收不到上游发送的数据。

读者在这里是不是开始怀疑了,是不是上游出问题导致整个任务的下游都接收不到数据?

答:不可能。如果上游 Task 出问题,所有下游 Subtask 都是正常的,都在接收上游发送数据,那么上游算子的 buffer 肯定是空的,怎么可能出现反压。所以上游算子反压严重必然是下游算子处理性能不行。

到这里,经过上述一步步推导,才开始想本文第二部分那些原理分析:是不是下游 Task 有某几个 Subtask 卡住了,导致整个任务卡住了。问题来了:怎么找出下游 Task 那几个可能卡住的 Subtask?

3.2.3 怎么找出下游 Task 那几个可能卡住的 Subtask?

本文第二部分分析过,如果下游 Task 某几个 Subtask 卡死,那么这几个 Subtask 的 inputBuffer 会被占满,且其他的 Subtask inputBuffer 全为空。所以我们只需要找出下游哪几个 Subtask 的 inputBuffer 占满了,也就是出现卡顿的 Subtask。

此时需要 Flink 强大的 Metric,Flink 的 Metric 可以看到下游 Task 所有 Subtask 的 inPoolUsage,Flink 的 Web UI 可以看到 Metric 项。下游 Task 有 100 个并行度,即:对应 100 个 Subtask。最笨的方式是分别查看 100 个 Subtask 的 inPoolUsage 指标。

高效的方式是构建好整个 Flink 的 Metric 系统,通过 Metric Report 将各种指标收集到外部存储系统,用 Grafana 或其他可视化工具展示。此时根据 Top N 的方式去查询即可。按照 subtask_id 分组,对 inPoolUsage 排降序,找出 inPoolUsage 最高的几个 subtask_id。

利用上述方案发现只有一个 subtask 的 inPoolUsage 为 1,其余的 subtask 的 inPoolUsage 都为 0。此时就可以得出结论了:确实是因为单个 Subtask 导致整个任务卡住了。

3.2.4 解决方案

定位到具体的 Subtask,jstack 发现该 Subtask 的主线程卡在了 ES Sink 的某一处代码。具体 ES 的问题这里不分析了,是 ES 客户端的 bug 导致卡死。查看了 ES 的相关 issue 将代码合入后问题最终解决。

3.2.5 小结

第三部分主要是从 Flink 的角度出发,使用一种通用的方法论来定位到任务被卡死的真正原因。定位问题需要有全面的理论支撑结合强大的 Metric 系统辅助定位问题。

4、 关于 Flink 物理分区策略的思考

Flink 支持的物理分区策略

Flink 的物理分区策略支持多种,包括:partitionCustom、shuffle、rebalance、rescale、broadcast。具体参考:https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/stream/operators/#physical-partitioning。

其实前四种分区策略都可以认为是一种负载均衡策略,上游算子 n 个并行度,下游算子 m 个并行度,如何将上游 n 个 Subtask 的数据打散到下游 m 个 Subtask 呢?

partitionCustom 表示自定义分区策略,根据用户自定义的分区策略发送数据

shuffle 表示随机的策略实现负载均衡。

rebalance 表示轮询策略。

rescale 是对 rebalance 策略的优化。引用官网 rescale 图示,相比 rebalance 而言,使用 rescale 策略时,上游 Subtask 只会给下游某几个 Subtask 发送数据。大大减少数据传输时边的个数。

  • 如下图所示,Source 有两个 Subtask,Map 有 6 个 Subtask,则一个 Source 的 Subtask 固定给 3 个 Map Subtask 发送数据。
  • 如果是 rebalance,每个 Source 都会给所有的 Map Subtask 发送数据。
【flink】Flink 中的木桶效应:单个 subtask 卡死导致整个任务卡死1.概述1、结论2、原理分析3、问题定位过程4、 关于 Flink 物理分区策略的思考Flink 欠缺的一种负载均衡策略5、总结

Flink 欠缺的一种负载均衡策略

上述几种物理分区策略都是静态的,而不是动态的。如下图所示是 rebalance shuffle 图示,上游 Task A 的所有 Subtask 要发送数据给下游 Task B 的所有 Subtask。假设 Subtask B0 没有卡死,但是由于资源竞争等原因,Subtask B0 的吞吐比 B1 和 B2 要差。但是 rebalance 是严格的轮询策略,所以上游给 Subtask B0、B1、B2 发送的数据量完全一致。最后 B0 就会拖慢整个任务的吞吐量,B1 和 B2 也不能发挥出自己真正的性能。

【flink】Flink 中的木桶效应:单个 subtask 卡死导致整个任务卡死1.概述1、结论2、原理分析3、问题定位过程4、 关于 Flink 物理分区策略的思考Flink 欠缺的一种负载均衡策略5、总结

rebalance shuffle

对于这种问题,常用的负载均衡策略并不是使用随机或者轮询策略,而是上游发送数据时会检测下游的负载能力,根据不同的负载能力,给下游发送不同的数据量。假设下游 Subtask B1 和 B2 吞吐量高于 B0,那么上游 Subtask A 会多给 B1 和 B2 发送一些数据,少给 B0 发送一些数据。

该策略可以解决 rebalance 策略导致的木桶效应。但该策略不能解决 KeyBy 的场景,因为 KeyBy 策略严格决定了每条数据要发送到下游哪个 Subtask。

5、总结

再次回顾第一部分的结论:

keyBy 或 rebalance 下游的算子,如果单个 subtask 完全卡死,会把整个 Flink 任务卡死

通过反压可以确定哪个 Task 出现性能瓶颈

通过 inPoolUsage 指标可以确定下游 Task 的哪个 Subtask 出现性能瓶颈

Flink 现有的物理分区策略全是静态的负载均衡策略,没有动态根据负载能力进行负载均衡的策略

基于 Apache Flink 的实时监控告警系统关于数据中台的深度思考与总结(干干货)日志收集Agent,阴暗潮湿的地底世界

2020 继续踏踏实实的做好自己

继续阅读