前言
整个反压机制不是单单一个算子去实现的,而是上下游协同操作的,因此,解析源码的时候会拆出每个单独的部分,没办法全面去协调解析,很绕,分为以下几步
- 下游解析上游发送的数据消息并占用缓冲区,等待下游消费者处理 DONE
- 下游消费者处理完,回收缓冲区,更新信用值(缓冲区) DONE
- 下游计算信用值,并发送给上游
- 上游拿到信用值,并根据信用值去发送数据
在上一篇文章Flink-反压-2.源码分析-流程-1 - 掘金,已经解析了1和2步,下面开始解析3和4步
三.下游计算信用值,并发送给上游
上面说到下游会把消费完的缓冲区进行回收,回收过程中,会调用inputChannel.notifyBufferAvailable(1)
,那么我们接下来回到RemotInputChannel中
1.RemotInputChannel干了啥
RemotInputChannel中有两个信用值属性,如下
java
// 初始信用值(每个通道的初始缓冲区数量,由配置 `networkBuffersPerChannel` 决定)
private final int initialCredit;
// 未通知给上游的可用信用值(核心变量,记录下游可接收的缓冲区数量)
private final AtomicInteger unannouncedCredit = new AtomicInteger(0);
(1) notifyBufferAvailable()
notifyBufferAvailable()
负责累加信用值,而notifyCreditAvailable()
负责通知上游
java
/**
* 当缓冲区可用时,增加未通知的信用值,触发上游通知
* (信用值计算的核心入口:缓冲区可用数量 → 信用值增加)
* 调notifyBufferAvailable的时候,正是释放缓冲区后才有的逻辑
*/
@Override
public void notifyBufferAvailable(int numAvailableBuffers) throws IOException {
// 若有可用缓冲区(numAvailableBuffers > 0),且之前无未通知信用值
// 则累加信用值,并调notifyCreditAvailable()通知上游
if (numAvailableBuffers > 0 && unannouncedCredit.getAndAdd(numAvailableBuffers) == 0) {
notifyCreditAvailable();
}
}
(2) notifyCreditAvailable()
调partitionRequestClient.notifyCreditAvailable()
对当前RemoteInputChannel对象进行封装成AddCreditMessage对象,然后发送给上游
java
private void notifyCreditAvailable() throws IOException {
// 检查partitionRequestClient是否连接到上游了(这是网络通信的客户端)
checkPartitionRequestQueueInitialized();
/* 通过网络发送信用消息给上游
* 1.this:就是当前RemoteInputChannel对象
* 2.底层会对当前RemoteInputChannel对象进行封装成AddCreditMessage对象,然后发送给上游
* */
partitionRequestClient.notifyCreditAvailable(this);
}
(3) PartitionRequestClient实现类的notifyCreditAvailable()
PartitionRequestClient是一个接口,其实现类如下
以NettyPartitionRequestClient为例,其实现的notifyCreditAvailable()如下
<1> NettyPartitionRequestClient.notifyCreditAvailable()
功能:把RemoteInputChannel对象封装成AddCreditMessage对象,然后发送给上游
java
@Override
public void notifyCreditAvailable(RemoteInputChannel inputChannel) {
// 把RemoteInputChannel对象封装成AddCreditMessage对象,然后发送给上游
sendToChannel(new AddCreditMessage(inputChannel));
}
<2> NettyPartitionRequestClient.sendToChannel()
功能:将消息提交到 Netty 事件循环线程执行
java
private void sendToChannel(Object message) {
// 将消息提交到 Netty 事件循环线程执行
// 1.tcpChannel.eventLoop().execute 确保消息发送在 Netty 的 I/O 线程中执行,避免并发问题
// 2.fireUserEventTriggered 触发 Netty 管道中的处理器(如 ServerHandler),最终将消息通过 TCP 发送给上游
tcpChannel.eventLoop().execute(() -> tcpChannel.pipeline().fireUserEventTriggered(message));
}
<3> NettyPartitionRequestClient内部类AddCreditMessage
当信用值 > 0 时,构建 Netty 消息,Netty通信的时候会自动调用buildMessage()
java
private static class AddCreditMessage extends ClientOutboundMessage {
/* ClientOutboundMessage是AddCreditMessage父类,其代码如下
abstract class ClientOutboundMessage {
protected final RemoteInputChannel inputChannel;
ClientOutboundMessage(RemoteInputChannel inputChannel) {
this.inputChannel = inputChannel;
}
@Nullable
abstract Object buildMessage();
}*/
private AddCreditMessage(RemoteInputChannel inputChannel) {
super(checkNotNull(inputChannel));
}
@Override
Object buildMessage() {
// RemoteInputChannel的getAndResetUnannouncedCredit()如下,就是先get出unannouncedCredit的值,然后重置unannouncedCredit未0
/*
public int getAndResetUnannouncedCredit() {
return unannouncedCredit.getAndSet(0);
}*/
int credits = inputChannel.getAndResetUnannouncedCredit();
// 仅当信用值 > 0 时,构建 Netty 消息
return credits > 0
? new NettyMessage.AddCredit(credits, inputChannel.getInputChannelId())
: null;
}
}
<4> NettyMessage.AddCredit类
--- 每个算子|上下游都会通过它去解析和发送信用值
write()
:把信用值和通道ID都写入到ByteBuf对象result中,然后通过Netty发送result给目标端readFrom()
:读取传来的信用值和通道id然后构建AddCredit实例
java
static class AddCredit extends NettyMessage {
private static final byte ID = 6;
final int credit;
final InputChannelID receiverId;
AddCredit(int credit, InputChannelID receiverId) {
checkArgument(credit > 0, "The announced credit should be greater than 0");
this.credit = credit;
this.receiverId = receiverId;
}
// 序列化消息,发送给其他人
@Override
void write(ChannelOutboundInvoker out, ChannelPromise promise, ByteBufAllocator allocator)
throws IOException {
ByteBuf result = null;
try {
// 1. 分配缓冲区:根据消息类型(ID)、信用值(int占4字节)、通道ID(固定长度)计算总长度
result =
allocateBuffer(
allocator, ID, Integer.BYTES + InputChannelID.getByteBufLength());
// 2. 序列化写入信用值(int类型)
result.writeInt(credit);
// 3. 序列化写入通道ID(InputChannelID的writeTo方法会将其转为二进制)
receiverId.writeTo(result);
// 4. 通过Netty发送result给目标端
out.write(result, promise);
} catch (Throwable t) {
handleException(result, null, t);
}
}
// 反序列化消息,接收它人的消息
static AddCredit readFrom(ByteBuf buffer) {
// 1. 从二进制缓冲区读取信用值(int类型)
int credit = buffer.readInt();
// 2. 从二进制缓冲区读取通道ID(InputChannelID.fromByteBuf方法反序列化)
InputChannelID receiverId = InputChannelID.fromByteBuf(buffer);
// 3. 构建AddCredit实例
return new AddCredit(credit, receiverId);
}
@Override
public String toString() {
return String.format("AddCredit(%s : %d)", receiverId, credit);
}
}
四.上游拿到信用值,并根据信用值去发送数据
上一步骤发现下游会通过Netty发送信用值和通道ID,那么上游去解析这些数据,肯定也是要在NettyMessage中
1.入口
这里因为涉及的类很多,就简单说一下谁调谁,感兴趣的可以自行按照下面的顺序走一遍看源码即可
java
下游算子初始化
↓
创建 RemoteInputChannel(下游输入通道)
↓
调用 requestSubpartitions()(下游主动请求上游数据)
↓
NettyPartitionRequestClient 构建 NettyMessage.PartitionRequest 消息
↓
【编码】NettyMessageEncoder 将 PartitionRequest 序列化为字节流(ByteBuf)
↓
通过 TCP 连接发送字节流(下游 → 上游)
↓
上游 Netty 服务端接收字节流(TCP 层)
↓
Netty EventLoop 线程触发 ChannelPipeline 处理
↓
【解码】NettyMessageDecoder 将字节流(ByteBuf)反序列化为 NettyMessage.PartitionRequest 对象
↓
PartitionRequestServerHandler.channelRead0() 处理 PartitionRequest 消息
↓
创建 CreditBasedSequenceNumberingViewReader(上游读取器,关联数据分区和信用值)
↓
下游发送 NettyMessage.AddCredit 消息(携带信用值)
↓
【编码】NettyMessageEncoder 将 AddCredit 序列化为字节流
↓
通过 TCP 连接发送字节流(下游 → 上游)
↓
上游 Netty 服务端接收字节流
↓
Netty EventLoop 触发 ChannelPipeline 处理
↓
【解码】NettyMessageDecoder 将字节流反序列化为 NettyMessage.AddCredit 对象
↓
PartitionRequestServerHandler.channelRead0() 处理 AddCredit 消息:
调用 outboundQueue.addCreditOrResumeConsumption() → 触发 reader.addCredit(credits)
↓
enqueueAvailableReader(reader) 将读取器加入可用队列
↓
writeAndFlushNextMessageIfPossible() 从队列获取读取器,发送数据缓冲区
↓
【编码】NettyMessageEncoder 将 BufferResponse 序列化为字节流(上游 → 下游)
↓
下游接收字节流,经 NettyMessageDecoder 解码为 BufferResponse 对象
↓
下游 RemoteInputChannel 处理缓冲区数据,完成一次数据传输
2.PartitionRequestServerHandler.channelRead0()
该方法负责解析各类NettyMessage,然后调不同的方法
重点说AddCredit的消息
- 将信用值和通道ID转发给outboundQueue(类是
PartitionRequestQueue
),并且里面调了reader.addCredit()
去添加信用值 - 其实就是后续在
CreditBasedSequenceNumberingViewReader中调addCredit()
java
// 这里负责解析各类NettyMessage,然后调不同的方法
@Override
protected void channelRead0(ChannelHandlerContext ctx, NettyMessage msg) throws Exception {
try {
Class<?> msgClazz = msg.getClass();
。。。
else if (msgClazz == AddCredit.class) { // 处理信用值消息(关键)
AddCredit request = (AddCredit) msg;
// 将信用值和通道ID转发给 outboundQueue,并且里面调了reader.addCredit()去添加信用值
// 其实就是后续在CreditBasedSequenceNumberingViewReader中调addCredit()
outboundQueue.addCreditOrResumeConsumption(
request.receiverId, reader -> reader.addCredit(request.credit));
} else if (msgClazz == ResumeConsumption.class) {
ResumeConsumption request = (ResumeConsumption) msg;
outboundQueue.addCreditOrResumeConsumption(
request.receiverId, NetworkSequenceViewReader::resumeConsumption);
}
。。。
}
(1) 调用的PartitionRequestQueue类
<1> 调用的addCreditOrResumeConsumption()
流程如下
- 根据通道id获取对应的
NetworkSequenceViewReader
实现类对象 --CreditBasedSequenceNumberingViewReader
- 执行传入的操作如reader.addCredit(request.credit)
- 将reader加入可用队列,触发数据发送逻辑 -- 下面详细讲解
java
/* 调用者的方法如下
outboundQueue.addCreditOrResumeConsumption(
request.receiverId, reader -> reader.addCredit(request.credit));
*/
void addCreditOrResumeConsumption(
InputChannelID receiverId, Consumer<NetworkSequenceViewReader> operation)
throws Exception {
if (fatalError) {
return;
}
// 1.根据通道id获取对应的NetworkSequenceViewReader实现类对象 -- CreditBasedSequenceNumberingViewReader
NetworkSequenceViewReader reader = obtainReader(receiverId);
// 2.执行传入的操作(reader.addCredit(request.credit))
operation.accept(reader);
// 3.将读取器加入可用队列,触发数据发送逻辑
enqueueAvailableReader(reader);
}
<2> 调用的enqueueAvailableReader()
这个方法会调用reader.getAvailabilityAndBacklog()
、announceBacklog()
、writeAndFlushNextMessageIfPossible()
java
private void enqueueAvailableReader(final NetworkSequenceViewReader reader) throws Exception {
// 1. 避免重复处理已注册的读取器
if (reader.isRegisteredAsAvailable()) {
return;
}
// 2. 判断读取器是否具备发送条件
ResultSubpartitionView.AvailabilityWithBacklog availabilityWithBacklog =
reader.getAvailabilityAndBacklog(); // 调CreditBasedSequenceNumberingViewReader.getAvailabilityAndBacklog()
// 如果判断不可发送数据,那么进行下面的处理
if (!availabilityWithBacklog.isAvailable()) {
// 2.1 获取反压情况
int backlog = availabilityWithBacklog.getBacklog();
// 2.2 通知下游节点,上游此时的反压情况
if (backlog > 0 && reader.needAnnounceBacklog()) {
announceBacklog(reader, backlog);
}
return;
}
// Queue an available reader for consumption. If the queue is empty,
// we try trigger the actual write. Otherwise this will be handled by
// the writeAndFlushNextMessageIfPossible calls.
// 3. 将可用读取器加入发送队列
boolean triggerWrite = availableReaders.isEmpty(); // 标记队列是否为空
registerAvailableReader(reader); // 将读取器加入队列,并标记为"已注册"
// 4. 触发数据发送
if (triggerWrite) {
writeAndFlushNextMessageIfPossible(ctx.channel());
}
}
<3> 调用的announceBacklog()
功能:负责构建家有消息并发送给下游,然后监听是否发送成功,若失败则调onChannelFutureFailure()
java
private void announceBacklog(NetworkSequenceViewReader reader, int backlog) {
checkArgument(backlog > 0, "Backlog must be positive.");
// 构建积压消息
NettyMessage.BacklogAnnouncement announcement =
new NettyMessage.BacklogAnnouncement(backlog, reader.getReceiverId());
// 发送积压消息并监听发送结果
ctx.channel()
.writeAndFlush(announcement)
.addListener(
(ChannelFutureListener)
future -> {
if (!future.isSuccess()) { // 若发送失败,则调用onChannelFutureFailure()处理
onChannelFutureFailure(future);
}
});
}
<4> 调用的writeAndFlushNextMessageIfPossible()
-- 发送数据的核心逻辑
发送数据,分为以下几步
- 检查是否存在致命错误或网络背压(channel不可写),若存在则停止发送
- 循环从可用读取器队列中获取下一个可发送数据的读取器
- 若队列中无可用读取器, 说明暂时无数据可发送,直接返回
- 获取下一个待发送缓冲区所属的子分区ID
- 从读取器获取下一个缓冲区,同时消耗1个信用值(若为数据缓冲区)--调
reader.getNextBuffer();
- 处理"无数据可发送"的情况
- 处理"有数据可发送"的情况
- 检查读取器是否还有更多数据可立即发送:就是检查nextDataType是否为
DataType.NONE
- 构建包含缓冲区数据和元信息的网络消息
- 异步发送数据并注册监听器
- 检查读取器是否还有更多数据可立即发送:就是检查nextDataType是否为
java
/**
* 尝试从可用读取器队列中获取下一个缓冲区并通过网络通道发送,
* 同时根据信用值和数据可用性动态调整读取器的调度状态。
*
* @param channel 上下游算子之间的TCP连接通道,用于实际数据传输
* @throws IOException 发送过程中出现I/O异常时抛出
*/
private void writeAndFlushNextMessageIfPossible(final Channel channel) throws IOException {
// 1. 检查是否存在致命错误或网络背压(channel不可写),若存在则停止发送
if (fatalError || !channel.isWritable()) {
return;
}
BufferAndAvailability next = null;
int nextSubpartitionId = -1;
try {
// 2. 循环从可用读取器队列中获取下一个可发送数据的读取器
while (true) {
NetworkSequenceViewReader reader = pollAvailableReader();
// 若队列中无可用读取器, 说明暂时无数据可发送,直接返回
if (reader == null) {
return;
}
// 3. 获取下一个待发送缓冲区所属的子分区ID(用于多路复用)
nextSubpartitionId = reader.peekNextBufferSubpartitionId();
// 4. 核心操作:从读取器获取下一个缓冲区,同时消耗1个信用值(若为数据缓冲区)
// 此处会给nextDataType赋值:若信用值不足且下一个不是事件,则返回DataType.NONE
next = reader.getNextBuffer();
// 5. 处理"无数据可发送"的情况:可能是读取器已释放或出现错误
if (next == null) {
if (!reader.isReleased()) {
// 读取器未释放但暂时无数据,继续尝试下一个读取器
continue;
}
// 读取器已释放,检查是否存在异常原因并向下游发送错误响应
Throwable cause = reader.getFailureCause();
if (cause != null) {
ErrorResponse msg = new ErrorResponse(cause, reader.getReceiverId());
ctx.writeAndFlush(msg);
}
}
// 6. 处理"有数据可发送"的情况:构建网络消息并发送
else {
// 关键逻辑:检查读取器是否还有更多数据可立即发送
// 此处的判断依赖于getNextBuffer()中计算的nextDataType,
// 6.1该值已在获取缓冲区时结合信用值和事件类型进行了判断
if (next.moreAvailable()) {
// 若还有数据且信用值足够(或下一个是事件),
// 则将读取器重新加入可用队列,以便继续调度发送后续数据
registerAvailableReader(reader);
}
// 6.2构建包含缓冲区数据和元信息的网络消息
BufferResponse msg = new BufferResponse(
next.buffer(), // 实际数据缓冲区
next.getSequenceNumber(), // 序列号(用于排序和确认)
reader.getReceiverId(), // 目标通道ID(下游算子的输入通道)
nextSubpartitionId, // 子分区ID(用于多路复用)
next.buffer() instanceof FullyFilledBuffer
? ((FullyFilledBuffer) next.buffer()).getPartialBuffers().size()
: 0,
next.buffersInBacklog()); // 剩余积压数据量(用于流量控制)
// 6.3异步发送消息,并注册回调监听器处理发送结果
// 注意:此处发送后立即return,确保每次只发送一个缓冲区,避免过多消息堆积
channel.writeAndFlush(msg).addListener(writeListener);
return;
}
}
} catch (Throwable t) {
// 异常处理:确保缓冲区资源被正确回收,避免内存泄漏
if (next != null) {
next.buffer().recycleBuffer();
}
throw new IOException(t.getMessage(), t);
}
}
(2) NetworkSequenceViewReader
实现类 -- 重要
NetworkSequenceViewReader
是一个接口,其实现类如下
这里以CreditBasedSequenceNumberingViewReader
为例,以信用机制为核心的属性如下
java
// 初始信用值(下游启动时的初始缓冲区数量)
private final int initialCredit;
// 下游可用的信用值(核心变量,控制数据发送的关键)
private int numCreditsAvailable;
<1> 调用的addCredit()
功能:接收下游发送的信用值,累加至可用信用值
java
@Override
public void addCredit(int creditDeltas) {
// 接收下游发送的信用值,累加至可用信用值
numCreditsAvailable += creditDeltas;
}
<2> 上游根据信用值发送数据的核心逻辑getNextBuffer()
该方法的调用者是PartitionRequestQueue的writeAndFlushNextMessageIfPossible()
流程如下
- 从子分区视图获取下一个缓冲区(待发送的数据)
- 若为数据缓冲区(非事件),则消耗 1 个信用值
- 信用值不足时抛异常
- 构造返回结果,包含缓冲区和下一个数据类型
java
// 上游发送数据的核心逻辑,该方法的调用者是PartitionRequestQueue的writeAndFlushNextMessageIfPossible(),会while循环,然后循环里面调getNextBuffer()
@Nullable
@Override
public BufferAndAvailability getNextBuffer() throws IOException {
// 1. 从子分区视图获取下一个缓冲区(待发送的数据)
BufferAndBacklog next = subpartitionView.getNextBuffer();
if (next != null) {
// 2. 若为数据缓冲区(非事件),则消耗 1 个信用值
if (next.buffer().isBuffer() && --numCreditsAvailable < 0) {
throw new IllegalStateException("no credit available"); // 信用值不足时抛异常
}
// 3. 构造返回结果,包含缓冲区和下一个数据类型
final Buffer.DataType nextDataType = getNextDataType(next);
return new BufferAndAvailability(
next.buffer(), nextDataType, next.buffersInBacklog(), next.getSequenceNumber());
} else {
return null; // 无数据可发送
}
}
<3> 调用的getNextDataType()
- 当信用值足够 或 下一个是数据类型是事件 -->表示可发送,返回nextDataType
- 否则,不可发送,返回DataType.NONE
java
private Buffer.DataType getNextDataType(BufferAndBacklog bufferAndBacklog) {
final Buffer.DataType nextDataType = bufferAndBacklog.getNextDataType();
if (numCreditsAvailable > 0 || nextDataType.isEvent()) { // 当信用值足够 或 下一个是数据类型是事件 -->表示可发送
return nextDataType;
}
return Buffer.DataType.NONE; // 否则,不可发送
}