8、Flink背压机制

1 网络流控

1.1 网络流控的作用

1.2 网络流控的实现-静态限速

1.3 网络流控的实现:动态反馈/自动反压

2 TCP流控机制

2.1 TCP包结构

2.2 TCP滑动窗口

3 Flink TCP-based反压机制(before V1.5)

3.1 示例:WindowWordCount

3.2 编译阶段:生成 JobGraph

3.3 运行阶段:调度 ExecutionGraph

3.4 问题拆解:反压传播两个阶段

3.5 跨TaskManager数据传输

3.6 跨TaskManager反压过程

3.7 TaskManager 内反压过程

4.Flink Credit-based反压机制(since V1.5)

4.1 TCP-based反压的弊端

4.2 Credit-based反压过程

5 总结与思考

5.1 总结

5.2 思考

网络流控


1.1 网络流控的作用

https://ververica.cn/wp-content/uploads/2019/12/1-幻灯片04-1024x576.png

如上是一张网络流控的图,Producer的吞吐率是2MB/sConsumer1MB/s,此时在网络通信时Producer的速度是比Consumer要快的,有1MB/s的速度差。假定两端都有一个BufferProducer端有一个发送用的Send BufferConsumer端有一个接收用的Receive Buffer,在网络端的吞吐率是2MB/s5sReceive Buffer可能就撑不住了,这时候会面临两种情况:

  • 如果Receive Buffer是有界的,这时候新到达的数据只能被丢弃掉了。
  • 如果Receive Buffer是无界的,Receive Buffer会持续的扩张,最终会导致Consumer的内存耗尽

1.2 网络流控的实现-静态限速

https://ververica.cn/wp-content/uploads/2019/12/2-幻灯片05-1024x576.png

为了解决这个问题,就需要从网络流控上来解决上下游速度差的问题,传统的做法可以在Producer端实现一个类似Rate Limiter这样的静态限流Producer的发送速率是2MB/s,经过限流这一层后,往Send Buffer去传数据的时候就会降到1MB/s 了,这样的话Producer端的发送速率跟Consumer端的处理速率就可以匹配起来了,就不会导致上述问题。但是这个解决方案有两点限制:

  • 事先无法预估Consumer到底能承受多大的速率。
  • Consumer的承受能力通常会动态波动

1.3 网络流控的实现:动态反馈/自动反压

https://ververica.cn/wp-content/uploads/2019/12/3-幻灯片06-1024x576.png

针对静态限速的问题演进到了动态反馈(自动反压)的机制,这需要Consumer能够及时的给Producer做一个feedback,即告知Producer能够承受的速率是多少。动态反馈分为两种:

  • 负反馈:接受速率小于发送速率时发生,告知 Producer 降低发送速率
  • 正反馈:发送速率小于接收速率时发生,告知 Producer 可以把发送速率提上来

案例一:Storm反压实现

https://ververica.cn/wp-content/uploads/2019/12/4-幻灯片07-1024x576.png

上图就是Storm里实现的反压机制,可以看到在每一个Bolt中都会有一个监测反压的线程(Backpressure Thread),这个线程一旦检测到Bolt里的接收队列(recv queue)出现了严重阻塞就会把这个情况写到ZooKeeper里,ZooKeeper会一直被Spout监听,监听到有反压的情况就会停止发送,通过这样的方式匹配上下游的发送接收速率。                                           

案例二:Spark Streaming反压实现

https://ververica.cn/wp-content/uploads/2019/12/5-幻灯片08-1024x576.png

Spark Streaming里也有做类似这样的feedback机制,上图Fecher会实时的从BufferProcessing这样的节点收集一些指标然后通过Controller把速度接收的情况再反馈到Receiver,实现速率的匹配。

疑问:为什么Flinkbefore V1.5)里没有用类似的方式实现feedback机制?

首先在解决这个疑问之前我们需要先了解一下Flink的网络传输架构。

https://ververica.cn/wp-content/uploads/2019/12/6-幻灯片10-1024x576.png

上面体现了Flink在做网络传输的时候基本的数据的流向,发送端在发送网络数据前要经历自己内部的一个流程,会有一个自己的Network Buffer,在底层用Netty去做通信,Netty这一层又有属于自己的ChannelOutbound Buffer,因为最终是要通过Socket做网络请求的发送,所以在Socket也有自己的Send Buffer,同样在接收端也有对应的三级BufferTCP是自带流量控制的,实际上Flink(before V1.5)就是通过TCP的流控机制来实现feedback的

2 TCP流控机制


2.1 TCP包结构

https://ververica.cn/wp-content/uploads/2019/12/7-幻灯片13-1024x576.png

根据上图简单回顾一下TCP包的格式结构。首先,有Sequence number这样一个机制给每个数据包做一个编号,还有ACK number这样一个机制来确保TCP的数据传输是可靠的,除此之外还有一个很重要的部分就是Window Size,接收端在回复消息的时候会通过Window Size告诉发送端还可以发送多少数据。

2.2 TCP滑动窗口

https://ververica.cn/wp-content/uploads/2019/12/8-幻灯片14-1024x576.png

TCP的流控是基于滑动窗口的机制,现在有一个Socket的发送端和一个Socket的接收端,发送端的速率是接收端的3倍,这样会发生什么样的一个情况呢?假定初始的时候发送的window大小是3,然后接收端的window大小是固定的,就是接收端的Buffer大小为5

https://ververica.cn/wp-content/uploads/2019/12/9-幻灯片15-1024x576.png

首先,发送端会一次性发3packets,将123发送给接收端,接收端接收后会将这3packets放到Buffer里。

https://ververica.cn/wp-content/uploads/2019/12/10-幻灯片16-1024x576.png

接收端一次消费1packet,这时候1就已经被消费了,然后看到接收端的滑动窗口会往前滑动一格,这时候23还在Buffer当中,而456是空出来的,所以接收端会给发送端发送ACK =4,代表发送端可以从4开始发送,同时会将window设置为3Buffer的大小5减去已经存下的23),发送端接收到回应后也会将他的滑动窗口向前移动到456

https://ververica.cn/wp-content/uploads/2019/12/11-幻灯片17-1024x576.png

这时候发送端将456发送,接收端也能成功的接收到Buffer中去。

https://ververica.cn/wp-content/uploads/2019/12/12-幻灯片18-1024x576.png

到这一阶段后,接收端就消费到2了,同样其窗口也会向前滑动一个,这时候Buffer就只剩一个了,于是向发送端发送ACK=7window =1。发送端收到之后滑动窗口也向前移,但是这个时候就不能移动3格了,虽然发送端的速度允许发3packets但是window传值已经告知只能接收一个,所以他的滑动窗口就只能往前移一格到7,这样就达到了限流的效果,发送端的发送速度从3降到1

https://ververica.cn/wp-content/uploads/2019/12/13-幻灯片19-1024x576.png

https://ververica.cn/wp-content/uploads/2019/12/14-幻灯片20-1024x576.png

再看一下这种情况,这时候发送端将7发送后,接收端接收到,但是由于接收端的消费出现问题,一直没有从Buffer中去取,这时候接收端向发送端发送ACK=8window=0 由于这个时候window = 0,发送端是不能发送任何数据,也就会使发送端的发送速度降为0。这个时候发送端不发送任何数据了,接收端也不进行任何的反馈了,那么如何知道消费端又开始消费了呢?

https://ververica.cn/wp-content/uploads/2019/12/15-幻灯片21-1024x576.png

https://ververica.cn/wp-content/uploads/2019/12/16-幻灯片22-1024x576.png

https://ververica.cn/wp-content/uploads/2019/12/17-幻灯片23-1024x576.png

TCP当中有一个ZeroWindowProbe的机制,发送端会定期的发送1个字节的探测消息,这时候接收端就会把 window的大小进行反馈。当接收端的消费恢复了之后,接收到探测消息就可以将window反馈给发送端端了从而恢复整个流程。TCP就是通过这样一个滑动窗口的机制实现 feedback

3 Flink TCP-based反压机制(before V1.5


3.1 示例:WindowWordCount

https://ververica.cn/wp-content/uploads/2019/12/18-幻灯片25-1024x576.png

大体的逻辑就是从Socket里去接收数据,每5s去进行一次WordCount,将这个代码提交后就进入到了编译阶段。

3.2 编译阶段:生成 JobGraph

https://ververica.cn/wp-content/uploads/2019/12/19-幻灯片26-1024x576.png

此时还没有向集群去提交任务,在Client端会将StreamGraph生成JobGraphJobGraph就是做为向集群提交的最基本的单元。在生成JobGrap的时候会做一些优化,将一些没有Shuffle机制的节点进行合并。有了JobGraph后就会向集群进行提交,进入运行阶段。

3.3 运行阶段:调度 ExecutionGraph

https://ververica.cn/wp-content/uploads/2019/12/20-幻灯片27-1-1024x576.png

JobGraph提交到集群后会生成ExecutionGraph,这时候就已经具备基本的执行任务的雏形了,把每个任务拆解成了不同的SubTask,上图ExecutionGraph中的Intermediate Result Partition就是用于发送数据的模块,最终会将ExecutionGraph交给JobManager的调度器,将整个ExecutionGraph调度起来。从物理执行图可以看到每个Task在接收数据时都会通过一个InputGate负责接收数据,再往前有这样一个ResultPartition负责发送数据,在ResultPartition又会去做分区跟下游的Task保持一致,就形成了ResultSubPartitionInputChannel的对应关系。这就是从逻辑层上来看的网络传输的通道,基于这么一个概念可以将反压的问题进行拆解。

3.4 问题拆解:反压传播两个阶段

https://ververica.cn/wp-content/uploads/2019/12/21-幻灯片28-1024x576.png

反压的传播实际上是分为两个阶段的,对应着上面的执行图,一共涉及3 TaskManager,在每个TaskManager里面都有相应的Task在执行,还有负责接收数据的InputGate,发送数据的ResultPartition,这就是一个最基本的数据传输的通道。在这时候假设最下游的TaskSink)出现了问题,处理速度降了下来这时候是如何将这个压力反向传播回去呢?这时候就分为两种情况:

  • 跨TaskManager,反压如何从InputGate传播到ResultPartition
  • TaskManager内,反压如何从ResultPartition传播到 InputGate

3.5 TaskManager数据传输

https://ververica.cn/wp-content/uploads/2019/12/22-幻灯片29-1024x576.png

前面提到,发送数据需要ResultPartition,在每个ResultPartition里面会有分区ResultSubPartition,中间还会有一些关于内存管理的Buffer

对于一个TaskManager来说会有一个统一的Network BufferPool被所有的Task共享,在初始化时会从Off-heap Memory中申请内存,申请到内存的后续内存管理就是同步Network BufferPool来进行的,不需要依赖JVM GC的机制去释放。有了Network BufferPool之后可以为每一个ResultSubPartition创建Local BufferPool

如上图左边的TaskManagerRecord Writer写了<12>这个两个数据进来,因为ResultSubPartition初始化的时候为空,没有Buffer用来接收,就会向Local BufferPool申请内存,这时Local BufferPool也没有足够的内存于是将请求转到Network BufferPool,最终将申请到的Buffer按原链路返还给ResultSubPartition<12>这个两个数据就可以被写入了。之后会将ResultSubPartitionBuffer拷贝到NettyBuffer当中,最终拷贝到SocketBuffer将消息发送出去。然后接收端按照类似的机制去处理将消息消费掉。

接下来模拟上下游处理速度不匹配的场景,发送端的速率为2,接收端的速率为1,看一下反压的过程是怎样的。

3.6 TaskManager反压过程

https://ververica.cn/wp-content/uploads/2019/12/23-幻灯片30-1024x576.png

因为速度不匹配就会导致一段时间后InputChannelBuffer被用尽,于是会向Local BufferPool申请新的Buffer,这时候可以看到Local BufferPool中的一个Buffer就会被标记为Used

https://ververica.cn/wp-content/uploads/2019/12/24-幻灯片31-1024x576.png

发送端还在持续以不匹配的速度发送数据,然后就会导致InputChannelLocal BufferPool申请Buffer的时候发现没有可用的Buffer了,这时候就只能向Network BufferPool去申请,当然每个Local BufferPool都有最大的可用的Buffer,防止一个Local BufferPool把Network BufferPool耗尽。这时候看到Network BufferPool还是有可用的Buffer可以向其申请。

https://ververica.cn/wp-content/uploads/2019/12/25-幻灯片32-1024x576.png

一段时间后,发现Network BufferPool没有可用的Buffer,或是Local BufferPool的最大可用Buffer到了上限无法向Network BufferPool申请,没有办法去读取新的数据,这时Netty AutoRead就会被禁掉,Netty就不会从Socket的Buffer中读取数据了

https://ververica.cn/wp-content/uploads/2019/12/26-幻灯片33-1024x576.png

显然,再过不久SocketBuffer也被用尽,这时就会将Window= 0发送给发送端(前文提到的TCP滑动窗口的机制)。这时发送端的Socket就会停止发送。

https://ververica.cn/wp-content/uploads/2019/12/27-幻灯片34-1024x576.png

很快发送端的SocketBuffer也被用尽,Netty检测到Socket无法写了之后就会停止向Socket写数据。

https://ververica.cn/wp-content/uploads/2019/12/28-幻灯片35-1024x576.png

Netty停止写了之后,所有的数据就会阻塞在NettyBuffer当中了,但是NettyBuffer是无界的,可以通过Netty的水位机制中的high watermark控制他的上界。当超过了netty的high watermark,Netty就会将其channel置为不可写,ResultSubPartition在写之前都会检测Netty是否可写,发现不可写就会停止向Netty写数据。

https://ververica.cn/wp-content/uploads/2019/12/29-幻灯片36-1024x576.png

这时候所有的压力都来到了ResultSubPartition,和接收端一样他会不断向Local BufferPoolNetwork BufferPool申请内存。

https://ververica.cn/wp-content/uploads/2019/12/30-幻灯片38-1024x576.png

Local BufferPoolNetwork BufferPool都用尽后整个Operator就会停止写数据,达到跨TaskManager的反压。

3.7 TaskManager 内反压过程

了解了跨TaskManager反压过程后再来看TaskManager内反压过程就更好理解了,下游的TaskManager反压导致本TaskManagerResultSubPartition无法继续写入数据,于是Record Writer的写也被阻塞住了,因为Operator需要有输入才能有计算后的输出,输入跟输出都是在同一线程执行,Record Writer阻塞了,Record Reader也停止从 InputChannel读数据,这时上游的TaskManager还在不断地发送数据,最终将这个TaskManager的Buffer耗尽。具体流程可以参考下图,这就是TaskManager内的反压过程。

https://ververica.cn/wp-content/uploads/2019/12/31-幻灯片39-1024x576.png

https://ververica.cn/wp-content/uploads/2019/12/32-幻灯片40-1024x576.png

https://ververica.cn/wp-content/uploads/2019/12/33-幻灯片41-1024x576.png

https://ververica.cn/wp-content/uploads/2019/12/34-幻灯片42-1024x576.png

4.Flink Credit-based反压机制(since V1.5


4.1 TCP-based反压的弊端

https://ververica.cn/wp-content/uploads/2019/12/35-幻灯片44-1024x576.png

在介绍Credit-based反压机制之前,先分析下TCP反压有哪些弊端:

  • 在一个TaskManager中可能要执行多个Task,如果多个Task的数据最终都要传输到下游同一TaskManager就会复用同一个Socket进行传输,这个时候如果单个Task产生反压,就会导致复用的Socket阻塞,其余的Task也无法使用传输,checkpoint barrier也无法发出导致下游执行checkpoint的延迟增大。
  • 依赖最底层的TCP去做流控,会导致反压传播路径太长,导致生效的延迟比较大。

4.2 Credit-based反压过程

这个机制简单理解就是在Flink层面实现类似TCP流控的反压机制来解决上述的弊端,Credit可以类比为TCPWindow机制。

https://ververica.cn/wp-content/uploads/2019/12/36-幻灯片46-1024x576.png

如图所示在Flink层面实现反压机制,就是每一次ResultSubPartition向InputChannel发送消息的时候都会发送一个backlog size告诉下游准备发送多少消息,下游就会去计算有多少的Buffer去接收消息,算完之后如果有充足的Buffer就会返还给上游一个Credit告知他可以发送消息(图上两个ResultSubPartitionInputChannel之间是虚线是因为最终还是要通过NettySocket去通信),下面我们看一个具体示例。

https://ververica.cn/wp-content/uploads/2019/12/37-幻灯片47-2-1024x576.png

假设我们上下游的速度不匹配,上游发送速率为2,下游接收速率为1,可以看到图上在ResultSubPartition中累积了两条消息,1011backlog就为2,这时就会将发送的数据<8,9>backlog = 2一同发送给下游。下游收到了之后就会去计算是否有2Buffer去接收,可以看到InputChannel中已经不足了这时就会从Local BufferPoolNetwork BufferPool申请,好在这个时候Buffer还是可以申请到的。

https://ververica.cn/wp-content/uploads/2019/12/38-幻灯片48-2-1024x576.png

过了一段时间后由于上游的发送速率要大于下游的接受速率,下游的TaskManagerBuffer已经到达了申请上限,这时候下游就会向上游返回Credit =0ResultSubPartition收到之后就不会向Netty去传输数据,上游TaskManagerBuffer也很快耗尽,达到反压的效果,这样在ResultSubPartition层就能感知到反压,不用通过Socket和Netty一层层地向上反馈,降低了反压生效的延迟同时也不会将Socket去阻塞,解决了由于一个Task反压导致TaskManager和TaskManager之间的Socket阻塞的问题

总结与思考


5.1 总结

  • 网络流控是为了在上下游速度不匹配的情况下,防止下游出现过载
  • 网络流控有静态限速和动态反压两种手段
  • Flink 1.5 之前是基于TCP流控+bounded buffer实现反压
  • Flink 1.5 之后实现了自己托管的credit – based流控机制,在应用层模拟TCP的流控机制

5.2 思考

1、有了动态反压,静态限速是不是完全没有作用了?

https://ververica.cn/wp-content/uploads/2019/12/39-幻灯片52-1-1024x576.png

实际上动态反压不是万能的,我们流计算的结果最终是要输出到一个外部的存储(Storage),外部数据存储到Sink端的反压是不一定会触发的,这要取决于外部存储的实现,像Kafka这样是实现了限流限速的消息中间件可以通过协议将反压反馈给Sink端,但是像ES无法将反压进行传播反馈给Sink端,这种情况下为了防止外部存储在大的数据量下被打爆,我们就可以通过静态限速的方式在Source端去做限流。所以说动态反压并不能完全替代静态限速的,需要根据合适的场景去选择处理方案。

2、作业处理性能上不去导致source端严重积压怎么办?

首先,确定sink端的外部系统是否正常;

正常的情况下,看有无背压;

如果有背压,确定一下背压出现在哪个算子上,然后适当的扩大下游算子的并行度然后观察作业背压是否缓解或者消除。此外,还可以增大Network Buffer的大小,使得task在传输数据时获得更多的buffer缓解背压。

3、背压导致的后果有哪些?

背压会导致ckp barrier无法正常下发,导致ckp超时失败;

背压会影响任务的处理性能。

4、其他不错的blog:

浅析背压(Back Pressure)机制及其在 Spark & Flink中的实现

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值