网络流控的概念与背景
1.为什么需要网络流控
如果 Receive Buffer 是有界的,这时候新到达的数据就只能被丢弃掉了。
如果 Receive Buffer 是无界的,Receive Buffer 会持续的扩张,最终会导致 Consumer 的内存耗尽。
2.网络流控的实现:静态限速
我们在Producer端实现一个静态限流,Producer经过限流器流量降低到和Consumer端相同,这样的话 Producer 端的发送速率跟 Consumer 端的处理速率就可以匹配起来了,就不会导致上述问题。但是这个解决方案有两点限制:
1.事先无法预估 Consumer 到底能承受多大的速率;
2.Consumer 的承受能力通常会动态地波动。
3.网络流控的实现:动态反馈/自动反压
我们需要Consumer及时的给Producer做一个feedback,告知Producer能够承受的速率是多少(flink的反压就采用动态反馈)。
动态反馈分为两种:
1.负反馈:接受速率小于发送速率时发生,告知 Producer 降低发送速率;
2正反馈:发送速率小于接收速率时发生,告知 Producer 可以把发送速率提上来。
Flink反压机制
TCP流控机制
TCP 包的格式结构,有 Sequence number 这样一个机制给每个数据包做一个编号,还有 ACK number 这样一个机制来确保 TCP 的数据传输是可靠的,除此之外还有一个很重要的部分就是 Window Size,接收端在回复消息的时候会通过 Window Size 告诉发送端还可以发送多少数据。
TCP流控:滑动窗口
接收端根据消费速率确定一个窗口大小,每次通信时会返回发送端一个可用的窗口大小,发送端根据窗口大小发送数据。
当消费端消费出现问题,window=0的时候,发送端会定期的发送 1 个字节的探测消息,这时候接收端就会把 window 的大小进行反馈。当接收端的消费恢复了之后,接收到探测消息就可以将 window 反馈给发送端端了从而恢复整个流程。TCP 就是通过这样一个滑动窗口的机制实现 feedback。
Flink TCP-based反压机制(before V1.5)
当Producer速率大于Consumer速率的时候,一段时间后 InputChannel 的 Buffer 被用尽,于是他会向 Local BufferPool 申请新的 Buffer ,Local BufferPool 用尽向 Network BufferPool申请,当Network BufferPool也用尽的时候,这时 Netty AutoRead 就会被禁掉,Netty 就不会从 Socket 的 Buffer 中读取数据了。过不多久Socket的buffer也会被用尽,这是window=0发送给发送端。这时候socket停止发送。
很快发送端的 Socket 的 Buffer 也被用尽,Netty 检测到 Socket 无法写了之后就会停止向 Socket 写数据。所有的数据就会阻塞在 Netty 的 Buffer 当中,很快Netty的buffer也不能在写数据了,数据就会积压到ResultSubPartition中。和接收端一样ResultSubPartition会不断的向 Local BufferPool 和 Network BufferPool 申请内存。
Local BufferPool 和 Network BufferPool 都用尽后整个 Operator 就会停止写数据,达到跨 TaskManager 的反压。
Flink Credit-based 反压机制(since V1.5)
这个机制简单的理解起来就是在 Flink 层面实现类似 TCP 流控的反压机制来解决上述的弊端,Credit 可以类比为 TCP 的 Window 机制。
每一次 ResultSubPartition 向 InputChannel 发送消息的时候都会发送一个 backlog size 告诉下游准备发送多少消息,下游就会去计算有多少的 Buffer 去接收消息,算完之后如果有充足的 Buffer 就会返还给上游一个 Credit 告知他可以发送消息。
当上流速率大于下游速率的时候,下游的 TaskManager 的 Buffer 已经到达了申请上限,这时候下游就会向上游返回 Credit = 0,ResultSubPartition 接收到之后就不会向 Netty 去传输数据,上游 TaskManager 的 Buffer 也很快耗尽,达到反压的效果,这样在 ResultSubPartition 层就能感知到反压,不用通过 Socket 和 Netty 一层层地向上反馈,降低了反压生效的延迟。同时也不会将 Socket 去阻塞,解决了由于一个 Task 反压导致 TaskManager 和 TaskManager 之间的 Socket 阻塞的问题。
Flink反压机制演变
flink1.5之前使用的是TCP的反压机制,但是反压实现依赖于底层TCP的反压,流程较长,延迟比较高。且TaskManager中一个任务线程数据积压就会造成TCP缓冲区数据积压,阻塞Socket,使其余的Task也都无法传输数据,同时barrier也无法传输到下流,造成下流做checkpoint的延时增大。
因此,1.5之后flink的反压改为了信任度机制,在应用层通过信任度模拟TCP的流量控制,实现反压。
生产环境反压识别和排查
识别反压
1、Flink 1.9 UI / Flink 1.11 旧 UI
如下图所示,你可以直接在 Flink Web 中,针对任何一个 Task 做反压检测。该检测机制是需要在 Flink Web 上手动触发,触发后 TaskManager 使用 Thread.getStackTrace 来抽样检测 Task Thread 是否处于等待 NetworkBuffer 中。根据抽样比例,来判断反压状态是处于 OK, LOW, HIGH 三个状态。Ratio 是代表抽样 n 次,遇到等待 NetworkBuffer 次数的比例。
可以从 Sink->Source 反着依次进行检查,遇到的第一个处于 HIGH 状态的 Task 可能(并不绝对) 就是触发反压的根本原因,由于该 Task 处于反压状态,它会将该状态不断向上传递,直到 Source。
当然这种检测方式,存在它本身的缺点:
- 即时触发,并不能观察历史情况。
- 如果遇到并发较多的 Task 时,可能需要等很久才能检测出来。
- 影响作业正常运行状态。
2、Flink Network Metric
从上面我们知道,处于中间的 Task 都会有一个 InputQueue 和 OutputQueue,我们可以通过使用 InputQueueUsage 和 OutputQueueUsage 比例来判断。
Task Status | OutputQueueUsage < 1.0 | OutputQueueUsage == 1.0 |
InputQueueUsage < 1.0 | 正常 | 处于反压,其根本原因可能是该 Task 下游处理能力不足导致,持续下去,该 Task 将会向上游传递反压 |
InputQueueUsage == 1.0 | 处于反压,持续下去,该 Task 会向上游传递反压,而且该 Task 可能是反压的源头 | 处于反压,原因可能是被下游阻塞 |
所以当作业处于反压下,我们需要从 Sink -> Source 逐步找到第一个 InputQueueUsage High,OutputQueueUsage Low 的 Task。优先查看下游的问题,这个更有可能是根本问题。inputPoolUsage > 0 的最下游的task。加上tag: host=*, 看是否某一台机器的高,如果是,重点检查这台机器和这上面的taskmanager, 比如,检查这台机器的负载
常见反压原因
资源不足
Container CPU 不足,导致计算能力不足,出现反压。点击 Dtop Metric 观察单 Container CPU 使用和 Application CPU 使用情况。
由于 cgroup 策略给单个 Container 预留了 20% 的 Buffer,故可以按照 100% 来区分高负载和低负载。
Container CPU < 100% * tm_cores | Container CPU > 100% * tm_cores | |
Application CPU < 80% * total_cores | 否 | 负载不均 |
Application CPU > 80% * total_cores | na(不会出现) | 是 |
当遇到资源不足时,可以选择缩小单 tm_slots,增加 tm_num 个数。
如果是负载不均衡的话,请参照下面来排查。
负载不均
当出现负载不均衡时,需要分析两个原因:(1)是否数据倾斜;(2)是否 Task 调度不均衡。
(1)数据倾斜请查看 Flink Metric 中的各个 Operater 的 QPS 的 Max 和 AVG 值,差别太大的话,则代表数据倾斜。如下图所示,该 bolt 的 qps max 和 avg 差距 10 倍,则说明产生了数据倾斜。
(2)Task 是否调度不均衡
如果 Task 并发度与 Container 并发度不能成正比,则代表不同 Container 分配的 Task 个数不同。
GC 压力
Flink Dashboard 上,默认有 YGC, FGC 两个指标,请查看相关指标,一般我们只需要关注 FULL GC 指标。
Task Latency 较高
用户自己写的某些 Task 执行时间较长,如果 Task 之间有 IO 操作,需要检查 IO Latency 是否过于高。该 Latency 可以查看 Flink Metric Dashboard,上面记录了每个 Task 的执行时间,从这里很清楚看到 map latency 是 10s。遇到这种情况,要么优化 task 处理能力,要么增加并发。
系统资源
遇到单机整体负载较高(常见于 Bigbang 队列和共享 Share 队列),该情况一般会产生局部 Partition 延迟。
遇到单机网络异常,一般会导致 Network Stack 变慢,导致延迟。
遇到这种问题的话,一般用户很难自己排查,请联系 Flink Oncall。
单线程瓶颈
Java Flink / SQL 任务一般情况下单 Task 是由单线程来执行的,单线程跑满也只能跑 1 个 CPU。
所以你需要观察单 Container 分配 Task 个数,以及该 Container CPU 使用情况,如果 Container CPU 使用情况达到该 Container 分配 Task 个数的 Cores,则会出现单线程瓶颈,你需要增加并发。