一、背压机制实现总结
- Flink对于背压的处理是通过在任务传递之间设置有界容量的数据缓冲区, 当整个管道中有一个下游任务速度变慢,会导致缓存区数据变满,上游任务获取不到可用的缓冲区,自然而然地被阻塞和降速, 这就实现了背压。
不同taskManager通信通过Netty, Netty的 Buffer 是无界的,但可以设置 Netty 的高水位
- 如果同一 Task 的不同 SubTask 被安排到同一个TaskManager,则它们与其他 TaskManager 的网络连接将被多路复用并共享一个TCP信道以减少资源使用 。这样会导致其中一个子任务流程受阻,其余子任务也会被连累受阻。为此flink1.5后通过Credit的反压策略,即通过反馈Credit(即缓存区的数量)的方式,巧妙的跳过共享通道,将数据堆积在子任务缓存区中。
二、背压的定义
背压是指系统在临时负载峰值期间接收数据的速率 高于其可以处理的速率的情况
三、如何处理背压
3.1 Flink任务流程天然的提供了应对背压能力
如果产生背压,理想的应对方法是flink整个任务流程会限制源以调整速度匹配管道中最慢的部分,达到稳态
源会被限制速度,与管道其他部分匹配,避免数据堆积。
根据介绍: Flink中的流在算子之间传输数据,就像连接线程的阻塞队列一样起到缓冲和限速的作用。当下游算子处理速度较慢时,上游算子会被限速。这种机制天然地为Flink提供了背压能力。
但是,为什么当下游算子处理速度较慢时,上游算子会被限速?
3.1.1 从flink任务执行流程看:
一个flink的任务流程如下:
-
记录"A"进入Flink,并由Task 1处理。
-
记录被序列化到一个缓冲区。
-
这个缓冲区被发给Task 2,然后Task 2从缓冲区读取记录。
flink通过在任务传递之间设置有界容量的数据缓冲区, 实现了流量控制和背压。如果下游任务的处理速度较慢,上游任务会被阻塞并降低速度,防止缓冲区溢出。这种机制天然地为Flink提供了背压能力。
当下游任务处理速度较慢时,上游任务获取不到可用的缓冲区,自然而然地被阻塞和降速,这就实现了背压。
Task 1在输出端有一个相关的缓冲池,Task 2在输入端有一个。如果有可用缓冲区对"A"进行序列化,我们就对其进行序列化并分发这个缓冲区。
3.1.2 flink任务执行的三种情况
- 远程交换 :如果Task 1和Task 2运行在不同的工作节点上,那么缓冲区可以在被发送到网络上时就回收(TCP通道)。在接收端Task 2,数据从网络复制到输入缓冲池的一个缓冲区 。如果没有可用缓冲区,就中断从TCP连接的读取。输出端Task 1通过一个简单的水位线机制永远不会在网络上放太多数据。如果网络上有超过阈值的数据在等待传输,我们在输入数据进网络之前等待,直到数据量低于一个阈值。这保证了网络上永远不会有过多的数据。如果接收端由于没有可用缓冲区而不消费新数据,这会放慢发送者的速度。
TCP 的 Socket 通信有动态反馈的流控机制,会把容量为0的消息反馈给上游发送端,所以上游的 Socket 就不会往下游再发送数据 。
Netty 的 Buffer 是无界的,但可以设置 Netty 的高水位,即:设置一个 Netty 中 Buffer 的上限。所以每次 ResultSubPartition 向 Netty 中写数据时,都会检测 Netty 是否已经到达高水位,如果达到高水位就不会再往 Netty 中写数据,防止 Netty 的 Buffer 无限制的增长。
- 本地交换 :如果Task 1和Task 2都在同一个工作节点(TaskManager)上运行,缓冲区可以直接交给下一个任务,而不需要通过TCP通道传输,本地交换背压处理机制与远程交换一致 。一旦Task 2消费完毕,缓冲区就会被回收。如果Task 2比Task 1慢,那么回收缓冲区的速度会低于Task 1填充它们的速度,这时所有的缓冲区都会被填满,从而导致Task 1获取不到可用的缓冲区而放慢速度。
- Task内部流程 :与上两种情况相同。 假如 Task A 的下游所有 Buffer 都占满了,那么 Task A 的 Record Writer 会被 block,Task A 的 Record Reader、Operator、Record Writer 都属于同一个线程,所以 Task A 的 Record Reader 也会被 block。
3.1.3 总结
在固定大小的缓冲池之间简单流动缓冲区,使Flink能够具有一个健壮的背压机制,任务永远不会比可消费的速度更快地产生数据。我们描述的在两个任务之间发送数据的机制,自然地推广到了复杂的管道,保证了整个管道中的背压传播。
通过这种缓冲区流动和回收的机制,整个Flink流水线形成了一个闭环的流量控制系统。下游任务处理慢时会限制上游任务,背压遵循着数据流的反方向,自发地在整个任务流程中传播,使得上游任务永远不会产生过多的待消费数据而导致过载。这使Flink能够在保证吞吐量的同时优雅地应对负载的波动。
- 如果接收端Task 2消费数据的速度变慢,缓冲池中缓冲区占满,输出端Task 1输出数据的速度会因为获取不到缓存区或水位线机制而变慢
- 如果输出端Task 1输出数据的速度变慢,缓冲池中无可用缓冲区,接收端Task 2消费数据的速度会因为无数据可用而变慢
3.2 Credit的反压策略
3.2.1 flink通过有界缓存的背压策略存在的问题
如下图所示,我们的任务有4个 SubTask,SubTask A 是 SubTask B的上游,即 SubTask A 给 SubTask B 发送数据。Job 运行在两个 TaskManager中, TaskManager 1 运行着 SubTask A.1 和 SubTask A.2, TaskManager 2 运行着 SubTask B.3 和 SubTask B.4。
现在假如由于CPU共享或者内存紧张或者磁盘IO瓶颈造成 SubTask B.4 遇到瓶颈、处理速率有所下降,但是上游源源不断地生产数据,所以导致 SubTask A.2 与 SubTask B.4 这一条链路产生反压。
不同 Job 之间的每个远程网络连接将在 Flink 的网络堆栈中获得自己的TCP通道。 但是,如果同一 Task 的不同 SubTask 被安排到同一个TaskManager,则它们与其他 TaskManager 的网络连接将被多路复用并共享一个TCP信道以减少资源使用。
例如,图中的 A.1 -> B.3、A.1 -> B.4、A.2 -> B.3、A.2 -> B.4 这四条将会多路复用共享一个 TCP 信道。
现在 SubTask B.3 并没有压力,但是当上图中 SubTask A.2 与 SubTask B.4 产生反压时 ,会把 TaskManager1 端该任务对应 Socket 的 Send Buffer 和 TaskManager2 端该任务对应 Socket 的 Receive Buffer 占满,共享的多路复用的 TCP 通道已经被占满了 ,会导致 SubTask A.1 和 SubTask A.2 要发送给 SubTask B.3 的数据也被阻塞了,从而导致本来没有压力的 SubTask B.3 现在接收不到数据了。
3.2.2 Credit的反压策略原理
flink1.5之后采用了Credit的反压策略
Credit的反压机制作用于 Flink 的应用层,即在 上游任务输出端ResultSubPartition 和 下游任务输入端InputChannel 这一层引入了反压机制。
每次上游 SubTask A.2 给下游 SubTask B.4 发送数据时,会把 Buffer 中的数据和上游 ResultSubPartition 堆积的数据量 Backlog size发给下游,下游会接收上游发来的数据,并向上游反馈目前下游现在的 Credit 值,Credit 值表示目前下游可以接收上游的 Buffer 量,1 个Buffer 等价于 1 个 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 通信。
当上游接受到下游反馈的 credit = 0,然后上游就不会发送数据到 Netty,巧妙的避免了在公用的 Netty 和 TCP 这一层的数据堆积。当然,上游仍会会定期地仅发送 backlog size 给下游,直到下游反馈 credit > 0 时,上游就会继续发送真正的数据到下游了。
四、具体实现源码
4.1 参考源码(不知道具体版本)
当可用的buffer数 <(挤压的数据量 + 已经分配给信任Credit的buffer量) 时,就会向Pool中继续请求buffer,这里请求不到也会一直while形成柱塞反压
直到有足够的然后通过notifyCreditAvailable()方法发送Credit。
4.2 flink1.12 源码
具体方法类RemoteInputChannel.java
4.2.1.获取上游响应的backlog,并判断是否有足够的缓存
java
/**
* Receives the backlog from the producer's buffer response. If the number of available buffers
* is less than backlog + initialCredit, it will request floating buffers from the buffer
* manager, and then notify unannounced credits to the producer.
* todo 当可用的buffer数 <(挤压的数据量 + 已经分配给信任Credit的buffer量) 时,就会向Pool中继续请求buffer,这里请求不到也会一直while形成反压
* @param backlog The number of unsent buffers in the producer's sub partition.
*/
void onSenderBacklog(int backlog) throws IOException {
int numRequestedBuffers = bufferManager.requestFloatingBuffers(backlog + initialCredit);
if (numRequestedBuffers > 0 && unannouncedCredit.getAndAdd(numRequestedBuffers) == 0) {
notifyCreditAvailable();
}
}
4.2.2.请求浮动缓存区,如果请求不到,注册监听器
java
/**
* Requests floating buffers from the buffer pool based on the given required amount, and
* returns the actual requested amount. If the required amount is not fully satisfied, it will
* register as a listener.
* 根据给定的所需数量从缓冲池中请求浮动缓冲区,并返回实际请求的数量。如果没有完全满足所需的数量,它将注册为侦听器。
* todo
*/
int requestFloatingBuffers(int numRequired) {
int numRequestedBuffers = 0;
synchronized (bufferQueue) {
// Similar to notifyBufferAvailable(), make sure that we never add a buffer after
// channel
// released all buffers via releaseAllResources().
if (inputChannel.isReleased()) {
return numRequestedBuffers;
}
numRequiredBuffers = numRequired;
// 监听直到有足够的buffer
while (bufferQueue.getAvailableBufferSize() < numRequiredBuffers
&& !isWaitingForFloatingBuffers) {
BufferPool bufferPool = inputChannel.inputGate.getBufferPool();
Buffer buffer = bufferPool.requestBuffer();
if (buffer != null) {
bufferQueue.addFloatingBuffer(buffer);
numRequestedBuffers++;
} else if (bufferPool.addBufferListener(this)) {
isWaitingForFloatingBuffers = true;
break;
}
}
}
return numRequestedBuffers;
}
- 首先,如果
inputChannel
已经释放,则直接返回,不再请求任何缓冲区。 - 将需要的缓冲区数量赋值给
numRequiredBuffers
变量。 - 然后进入一个循环,在循环中尝试从
BufferPool
获取缓冲区,并将获取到的缓冲区添加到bufferQueue
中。循环条件是bufferQueue
中可用缓冲区数量小于所需数量,并且当前不在等待浮动缓冲区的状态。 - 在循环中,首先从
inputChannel.inputGate.getBufferPool()
获取一个缓冲区。如果获取成功,则将缓冲区添加到bufferQueue
中,并增加numRequestedBuffers
计数器。 - 如果从
BufferPool
中获取缓冲区失败,则调用bufferPool.addBufferListener(this)
方法,将当前对象注册为缓冲区监听器。如果注册成功,则设置isWaitingForFloatingBuffers
标志为true,并退出循环。 - 最后,返回实际获取到的浮动缓冲区数量
numRequestedBuffers
。
4.2.3 监听器通知
当BufferPool
有新的浮动缓冲区可用时,它会调用已注册监听器的notifyBufferAvailable
方法,传入可用的Buffer
对象。
java
/**
* The buffer pool notifies this listener of an available floating buffer. If the listener is
* released or currently does not need extra buffers, the buffer should be returned to the
* buffer pool. Otherwise, the buffer will be added into the <tt>bufferQueue</tt>.
* 缓冲池通知此侦听器一个可用的浮动缓冲区。如果侦听器已释放或当前不需要额外的缓冲区,则应将缓冲区返回到缓冲池。否则,缓冲区将被添加到<tt>缓冲队列</tt>中。
* todo 监听器通知
* @param buffer Buffer that becomes available in buffer pool.
* @return NotificationResult indicates whether this channel accepts the buffer and is waiting
* for more floating buffers.
*/
@Override
public BufferListener.NotificationResult notifyBufferAvailable(Buffer buffer) {
BufferListener.NotificationResult notificationResult =
BufferListener.NotificationResult.BUFFER_NOT_USED;
// Assuming two remote channels with respective buffer managers as listeners inside
// LocalBufferPool.
// While canceler thread calling ch1#releaseAllResources, it might trigger
// bm2#notifyBufferAvaialble.
// Concurrently if task thread is recycling exclusive buffer, it might trigger
// bm1#notifyBufferAvailable.
// Then these two threads will both occupy the respective bufferQueue lock and wait for
// other side's
// bufferQueue lock to cause deadlock. So we check the isReleased state out of synchronized
// to resolve it.
if (inputChannel.isReleased()) {
return notificationResult;
}
try {
synchronized (bufferQueue) {
checkState(
isWaitingForFloatingBuffers,
"This channel should be waiting for floating buffers.");
// Important: make sure that we never add a buffer after releaseAllResources()
// released all buffers. Following scenarios exist:
// 1) releaseAllBuffers() already released buffers inside bufferQueue
// -> while isReleased is set correctly in InputChannel
// 2) releaseAllBuffers() did not yet release buffers from bufferQueue
// -> we may or may not have set isReleased yet but will always wait for the
// lock on bufferQueue to release buffers
if (inputChannel.isReleased()
|| bufferQueue.getAvailableBufferSize() >= numRequiredBuffers) {
isWaitingForFloatingBuffers = false;
return notificationResult;
}
bufferQueue.addFloatingBuffer(buffer);
bufferQueue.notifyAll();
if (bufferQueue.getAvailableBufferSize() == numRequiredBuffers) {
isWaitingForFloatingBuffers = false;
notificationResult = BufferListener.NotificationResult.BUFFER_USED_NO_NEED_MORE;
} else {
notificationResult = BufferListener.NotificationResult.BUFFER_USED_NEED_MORE;
}
}
if (notificationResult != NotificationResult.BUFFER_NOT_USED) {
inputChannel.notifyBufferAvailable(1);
}
} catch (Throwable t) {
inputChannel.setError(t);
}
return notificationResult;
}
- 检查channel状态,确保channel未释放且正在等待浮动缓冲区。
- 将新的浮动缓冲区
buffer
添加到bufferQueue
中。 - 通知等待在
bufferQueue
上的线程,即调用bufferQueue.notifyAll()
。 - 根据当前可用缓冲区数量是否满足需求,设置
isWaitingForFloatingBuffers
标志和返回NotificationResult
。 - 如果已获得足够的缓冲区,通过
inputChannel.notifyBufferAvailable(1)
进一步通知上层。
五、如何定位背压
可以在Web界面,从Sink到Source这样反向逐个Task排查,找到第一个出现反压的Task,一般上Task出现反压会出现如下现象:
当 Web 页面切换到某个 Task 的 BackPressure 页面时,才会对这个Task触发反压检测。BackPressure界面会周期性的对Task线程栈信息采样,通过线程被阻塞在请求Buffer的频率来判断节点是否处于反压状态(反压就是因为Buffer不够用了,也就是内存不够用了,所以Task暂时性的阻塞住了)。默认情况下,这个频率在 0.1 以下显示为 OK,0.1 至 0.5 显示 LOW,而超过 0.5 显示为 HIGH。
通过反压状态可以大致锁定反压可能存在的算子,但具体反压是由于当前Task自身处理速度慢还是由于下游Task处理慢导致的,需要通过metric监控进一步判断。因为反压存在两种可能性:
- 当前Task发送的速度跟不上其接受后产生数据的速度。比如一条输入flatmap或者collect多次处理成多条输出这种情况,导致当前Task发送端申请不到足够的内存。
- 当前Task处理数据的速度比较慢,比如每条数据都要进行算法调用之类的,而上游Task处理数据较快,从而导致上游发送端申请不到足够的内存。
六、背压处理实验
该图显示了生产者(黄色)和消费者(绿色)任务的平均吞吐量占最大吞吐量的百分比随时间的变化。为了测量平均吞吐量,我们每5秒测量一次任务处理的记录数。
- 首先,我们让生产者任务以最大速度的60%运行(通过Thread.sleep()调用模拟减速)。在没有人为减速的情况下,消费者任务以相同的速度处理数据,。
- 然后我们将消费者任务减速到最大速度的30%。这时,背压效应就发挥作用了,我们看到生产者也自然地降低到30%的满速。
- 接着我们停止对消费者的人为减速,两个任务都恢复到最大吞吐量。
- 我们再次将消费者减速到最大速度的30%,管道立即作出反应,生产者也降低到30%的满速。
- 最后,我们再次停止减速,两个任务继续以100%的满速运行。
总的来说,我们看到在管道中,生产者和消费者的吞吐量互相跟随,这正是流管道所期望的行为。
通过这个实验,我们看到Flink基于其天然的数据流缓冲和控制机制,能够在整个管道中传播背压,使生产者和消费者的吞吐量自动匹配较慢的一方,防止过载同时避免数据丢失。这种简单而健壮的背压处理机制是Flink作为流式处理引擎的一大优势。
七、参考文档
How Apache Flink™ handles backpressure
netty高低水位流控(yet) - silyvin - 博客园