网络流控的概念与背景

分析一个简单的 Flink 流任务,下图是一个简单的 Flink 流任务执行图:任务首先从 Kafka 中读取数据、 map 算子对数据进行转换、keyBy 按照指定 key 对数据进行分区(相同 key 的数据经过 keyBy 后分到同一个 subtask 实例中),keyBy 后对数据接着进行 map 转换,然后使用 Sink 将数据输出到外部存储。

Flink 网络流控与反压机制 - 图1

Flink任务简单示例图

众所周知,在大数据处理中,无论是批处理还是流处理,单点处理的性能总是有限的,我们的单个 Job 一般会运行在多个节点上,多个节点共同配合来提升整个系统的处理性能。图中,任务被切分成 4 个可独立执行的 subtask( A0、A1、B0、B1),在数据处理过程中,就会存在 shuffle(数据传输)的过程。例如,subtask A0 处理完的数据经过 keyBy 后发送到 subtask B0、B1 所在节点去处理。

Flink 网络流控与反压机制 - 图2

流控-存在的问题

首先我们可以看下这张最精简的网络流控的图,Producer 的吞吐率是 2MB/s,Consumer 是 1MB/s,这个时候我们就会发现在网络通信的时候我们的 Producer 的速度是比 Consumer 要快的,有 1MB/s 的这样的速度差,假定我们两端都有一个 Buffer,Producer 端有一个发送用的 Send Buffer,Consumer 端有一个接收用的 Receive Buffer,在网络端的吞吐率是 2MB/s,过了 5s 后我们的 Receive Buffer 可能就撑不住了,这时候会面临两种情况:
  • 如果 Receive Buffer 是有界的,这时候新到达的数据就只能被丢弃掉了。
  • 如果 Receive Buffer 是无界的,Receive Buffer 会持续的扩张,最终会导致 Consumer 的内存耗尽,OOM,程序挂掉

Flink 网络流控与反压机制 - 图3

流控-静态限速

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

Flink 网络流控与反压机制 - 图4

流控-动态反馈

针对静态限速的问题我们就演进到了动态反馈(自动反压)的机制,我们需要 Consumer 能够及时的给 Producer 做一个 feedback,即告知 Producer 能够承受的速率是多少。动态反馈分为两种:
  • 负反馈:接受速率小于发送速率时发生,告知 Producer 降低发送速率
  • 正反馈:发送速率小于接收速率时发生,告知 Producer 可以把发送速率提上来

如下图所示,假如我们的 Job 分为 Task A、B、C,Task A 是 Source Task、Task B 处理数据、Task C 是 Sink Task。假如 Task C 由于各种原因吞吐量降低,会将负载信息反馈给 Task B,Task B 会降低向 Task C 发送数据的速率,此时如果 Task B 如果还是一直从 Task A 读取数据,那么按照同样的道理,数据会把 Task B 的 Send Buffer 和 Receive Buffer 撑爆,又会出现上面描述的问题。所以,当 Task B 的 Send Buffer 和 Receive Buffer 被用完后,Task B 会用同样的原理将负载信息反馈给 Task A,Task A 收到 Task B 的负载信息后,会降低给 Task B 发送数据的速率,以此类推。

如果下游 Task C 的负载能力提升后,会及时反馈给 Task B,于是 Task B 会提升往 Task C 发送数据的速率,Task B 又将负载提升的信息反馈给 Task A,Task A 就会提升从 Source 端读取数据的速率,从而提升整个系统的负载能力。

Flink 网络流控与反压机制 - 图5

简单的 3 个 Task 图示

上面这个流程,就是 Flink 动态限流(反压机制)的简单描述。我们可以看到 Flink 的反压其实是从下游往上游传播的,一直往上传播到 Source Task 后,Source Task 最终会降低从 Source 端读取数据的速率。

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

Flink V1.5 之前版本为什么没有 feedback 机制

Flink V1.5 以前版本之所以没有 feedback 机制,是因为 TCP 天然具备 feedback 流控机制,Flink 基于它来实现反压。

下图是 Flink 的 TaskManager 之间网络传输的数据流向:

Flink 网络流控与反压机制 - 图6

Flink 网络传输的数据流向

图中,我们可以看到 TaskManager A 给 TaskManager B 发送数据,TaskManager A 做为 Producer,TaskManager B 做为 Consumer。Producer 端的 Operator 实例会产生数据,最后通过网络发送给 Consumer 端的 Operator 实例。

Producer 端 Operator 实例生产的数据首先缓存到 TaskManager 内部的 Network Buffer。Network 依赖 Netty 来做通信,Producer 端的 Netty 内部有 ChannelOutbound Buffer,Consumer 端的 Netty 内部有 ChannelInbound Buffer。Netty 最终还是要通过 Socket 发送网络请求,Socket 这一层也会有 Buffer,Producer 端有 Send Buffer,Consumer 端有 Receive Buffer。

总结一下,现在有两个 TaskManager A、B,TaskManager A 中 Producer Operator 处理完的数据由 TaskManager B 中 Consumer Operator 处理。那么 Producer Operator 处理完的数据是怎么到达 Consumer Operator 的?首先 Producer Operator 从自己的上游或者外部数据源读取到数据后,对一条条的数据进行处理,处理完的数据首先输出到 Producer Operator 对应的 Network Buffer 中。Buffer 写满或者超时后,就会触发将 Network Buffer 中的数据拷贝到 Producer 端 Netty 的 ChannelOutbound Buffer,之后又把数据拷贝到 Socket 的 Send Buffer 中,这里有一个从用户态拷贝到内核态的过程,最后通过 Socket 发送网络请求,把 Send Buffer 中的数据发送到 Consumer 端的 Receive Buffer。数据到达 Consumer 端后,再依次从 Socket 的 Receive Buffer 拷贝到 Netty 的 ChannelInbound Buffer,再拷贝到 Consumer Operator 的 Network Buffer,最后 Consumer Operator 就可以读到数据进行处理了。

这就是两个 TaskManager 之间的数据传输过程,我们可以看到发送方和接收方各有三层的 Buffer。

TCP 流控机制

TCP Packet

Flink 网络流控与反压机制 - 图7

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

TCP流控: 滑动窗口

Flink 网络流控与反压机制 - 图8

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

Flink 网络流控与反压机制 - 图9

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

Flink 网络流控与反压机制 - 图10

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

Flink 网络流控与反压机制 - 图11

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

Flink 网络流控与反压机制 - 图12

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

Flink 网络流控与反压机制 - 图13

Flink 网络流控与反压机制 - 图14

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

Flink 网络流控与反压机制 - 图15

Flink 网络流控与反压机制 - 图16

Flink 网络流控与反压机制 - 图17

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

Job 提交执行流程

示例: WindowWordCount

Flink 网络流控与反压机制 - 图18

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

编译阶段: 生成 JobGraph

Flink 网络流控与反压机制 - 图19

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

运行阶段: 调度 ExecutionGraph

Flink 网络流控与反压机制 - 图20

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

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

Flink 网络流控与反压机制 - 图21

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

下面将以简单的 3 个 Task 的反压进行说明

Flink 网络流控与反压机制 - 图22

简单的 3 个 Task 反压图示
  • 跨 TaskManager
    • 当下游 Task C 的 Receive Buffer 满了,如何告诉上游 Task B 应该降低数据发送速率
    • 当下游 Task C 的 Receive Buffer 空了,如何告诉上游 Task B 应该提升数据发送速率
  • TaskManager 内
    • 当 Task B 的 Send Buffer 满了,如何告诉 Task B 内部的 Receive Buffer 下游 Send Buffer 满了、下游处理性能不行了?因为要让 Task B 的 Receive Buffer 感受到压力,才能把下游的压力传递到 Task A
    • 当 Task B 的 Send Buffer 空了,如何告诉 Task B 内部的 Receive Buffer 下游 Send Buffer 空了,下游处理性能很强,上游加快处理数据吧

跨 TaskManager 数据传输

如下图所示,我们先介绍跨 TaskManager 网络传输使用的 buffer

Flink 网络流控与反压机制 - 图23

跨 TaskManager 数据传输

前面提到,发送数据需要 ResultPartition,在每个 ResultPartition 里面会有分区 ResultSubPartition,中间还会有一些关于内存管理的 Buffer。 对于一个 TaskManager 来说会有一个统一的 <font style="color:rgb(48, 48, 48);">Network BufferPool</font>TaskManager 内所有的 Task 共享,在初始化时会从 <font style="color:rgb(48, 48, 48);">Off-heap Memory</font> 中申请内存,后续的内存管理由 Network BufferPool 进行管理,不需要依赖 <font style="color:rgb(48, 48, 48);">JVM GC</font> 的机制去释放。有了 Network BufferPool 之后可以为每一个 ResultSubPartition 创建 Local BufferPool 。 如上图左边的 TaskManager 的 Record Writer 写了 <1,2> 这个两个数据进来,因为 <font style="color:rgb(48, 48, 48);">ResultSubPartition</font> 初始化的时候为空,没有 Buffer 用来接收,就会向 <font style="color:rgb(48, 48, 48);">Local BufferPool</font> 申请 Buffer,这时 <font style="color:rgb(48, 48, 48);">Local BufferPool</font> 也没有足够的 Buffer 于是将请求转到 <font style="color:rgb(48, 48, 48);">Network BufferPool</font>,最终将申请到的 Buffer 按原链路返还给 <font style="color:rgb(48, 48, 48);">ResultSubPartition</font>,<1,2> 这个两个数据就可以被写入了。之后会将 <font style="color:rgb(48, 48, 48);">ResultSubPartition</font> 的 Buffer 拷贝到 Netty 的 Buffer 当中,最终拷贝到 Socket 的 Buffer 将消息发送出去。然后接收端按照类似的机制去处理将消息消费掉。
接下来我们来模拟上下游处理速度不匹配的场景,发送端的速率为 2,接收端的速率为 1,看一下反压的过程是怎样的。

跨 TaskManager 反压过程

Flink 网络流控与反压机制 - 图24

跨 TaskManager 数据反压

因为速度不匹配就会导致一段时间后 <font style="color:rgb(48, 48, 48);">InputChannel</font> 的 Buffer 被用尽,于是他会向<font style="color:rgb(48, 48, 48);"> Local BufferPool</font> 申请新的 Buffer ,这时候可以看到 <font style="color:rgb(48, 48, 48);">Local BufferPool</font> 中的一个 Buffer 就会被标记为 Used。

Flink 网络流控与反压机制 - 图25

跨 TaskManager 数据反压

发送端还在持续以不匹配的速度发送数据,然后就会导致 <font style="color:rgb(48, 48, 48);">InputChannel</font><font style="color:rgb(48, 48, 48);">Local BufferPool</font> 申请 Buffer 的时候发现没有可用的 Buffer 了,这时候就只能向 <font style="color:rgb(48, 48, 48);">Network BufferPool</font> 去申请,当然每个 <font style="color:rgb(48, 48, 48);">Local BufferPool</font> 都有最大的可用的 Buffer,防止一个 <font style="color:rgb(48, 48, 48);">Local BufferPool</font><font style="color:rgb(48, 48, 48);">Network BufferPool</font> 耗尽。这时候看到 <font style="color:rgb(48, 48, 48);">Network BufferPool</font> 还是有可用的 Buffer 可以向其申请。

Flink 网络流控与反压机制 - 图26

跨 TaskManager 数据反压

一段时间后,发现 <font style="color:rgb(48, 48, 48);">Network BufferPool</font> 没有可用的 Buffer,或是 <font style="color:rgb(48, 48, 48);">Local BufferPool</font> 的最大可用 Buffer 到了上限无法向 <font style="color:rgb(48, 48, 48);">Network BufferPool</font> 申请,没有办法去读取新的数据,这时 Netty <font style="color:rgb(48, 48, 48);">AutoRead</font> 就会被禁掉,Netty 就不会从 Socket 的 Buffer 中读取数据了。

Flink 网络流控与反压机制 - 图27

跨 TaskManager 数据反压

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

Flink 网络流控与反压机制 - 图28

跨 TaskManager 数据反压

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

Flink 网络流控与反压机制 - 图29

跨 TaskManager 数据反压

Netty 停止写了之后,所有的数据就会阻塞在 Netty 的 Buffer 当中了,但是 Netty 的 Buffer 是无界的,可以通过 Netty 的水位机制中的 high watermark 控制他的上界。当超过了 high watermark,Netty 就会将其 channel 置为不可写,<font style="color:rgb(48, 48, 48);">ResultSubPartition</font> 在写之前都会检测 Netty 是否可写,发现不可写就会停止向 Netty 写数据。

Flink 网络流控与反压机制 - 图30

Flink 网络流控与反压机制 - 图31

跨 TaskManager 数据反压

这时候所有的压力都来到了 <font style="color:rgb(48, 48, 48);">ResultSubPartition</font>,和接收端一样他会不断的向 <font style="color:rgb(48, 48, 48);">Local BufferPool </font><font style="color:rgb(48, 48, 48);">Network BufferPool</font> 申请内存。

Flink 网络流控与反压机制 - 图32

跨 TaskManager 数据反压

<font style="color:rgb(48, 48, 48);">Local BufferPool</font><font style="color:rgb(48, 48, 48);">Network BufferPool</font> 都用尽后整个 Operator 就会停止写数据,达到跨 TaskManager 的反压。

TaskManager 内反压过程

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

Flink 网络流控与反压机制 - 图33


Flink 网络流控与反压机制 - 图34


Flink 网络流控与反压机制 - 图35


Flink 网络流控与反压机制 - 图36

Task 内数据反压

整个 Flink 的反压是从下游往上游传播的,一直传播到 Source Task,Source Task 有压力后,会降低从外部组件中读取数据的速率,例如:Source Task 会降低从 Kafka 中读取数据的速率,来降低整个 Flink Job 中缓存的数据,从而降低负载。

Flink TCP-based 反压机制( since V1.5 版本)

Flink V1.5 之前版本的反压策略存在的问题

Flink 网络流控与反压机制 - 图37

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

Credit 的反压策略实现原理

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

如下图所示,反压机制作用于 Flink 的应用层,即在 ResultSubPartitionInputChannel 这一层引入了反压机制。每次上游 SubTask A.2 给下游 SubTask B.4 发送数据时,会把 Buffer 中的数据和上游 ResultSubPartition 堆积的数据量 Backlog size 发给下游,下游会接收上游发来的数据,并向上游反馈目前下游现在的 Credit 值,Credit 值表示目前下游可以接收上游的 Buffer 量,1 个Buffer 等价于 1 个 Credit 。

Flink 网络流控与反压机制 - 图38

Credit 数据传输

例如,上游 SubTask A.2 发送完数据后,还有 5 个 Buffer 被积压,那么会把发送数据和 Backlog size = 5 一块发送给下游 SubTask B.4,下游接受到数据后,知道上游积压了 5 个Buffer,于是向 Buffer Pool 申请 Buffer,由于容量有限,下游 InputChannel 目前仅有 2 个 Buffer 空间,所以,SubTask B.4 会向上游 SubTask A.2 反馈 Channel Credit = 2。然后上游下一次最多只给下游发送 2 个 Buffer 的数据,这样每次上游发送的数据都是下游 InputChannel 的 Buffer 可以承受的数据量,所以通过这种反馈策略,保证了不会在公用的 Netty 和 TCP 这一层数据堆积而影响其他 SubTask 通信。ResultSubPartition 会把 buffer 和backlog size 同时发送给下游,下游向上游反馈 credit

Credit-based 反压过程

Flink 网络流控与反压机制 - 图39

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

Flink 网络流控与反压机制 - 图40

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

Flink 网络流控与反压机制 - 图41

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

总结与思考

总结

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

思考

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

Flink 网络流控与反压机制 - 图42

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

参考文档

一文搞懂 Flink 网络流控与反压机制

Apache Flink 进阶(七):网络流控及反压剖析