Flink-反压-3.源码分析-流程-2

前言

整个反压机制不是单单一个算子去实现的,而是上下游协同操作的,因此,解析源码的时候会拆出每个单独的部分,没办法全面去协调解析,很绕,分为以下几步

  1. 下游解析上游发送的数据消息并占用缓冲区,等待下游消费者处理 DONE
  2. 下游消费者处理完,回收缓冲区,更新信用值(缓冲区) DONE
  3. 下游计算信用值,并发送给上游
  4. 上游拿到信用值,并根据信用值去发送数据

在上一篇文章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的消息

  1. 将信用值和通道ID转发给outboundQueue(类是PartitionRequestQueue),并且里面调了reader.addCredit()去添加信用值
  2. 其实就是后续在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()

流程如下

  1. 根据通道id获取对应的NetworkSequenceViewReader实现类对象 -- CreditBasedSequenceNumberingViewReader
  2. 执行传入的操作如reader.addCredit(request.credit)
  3. 将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() -- 发送数据的核心逻辑

发送数据,分为以下几步

  1. 检查是否存在致命错误或网络背压(channel不可写),若存在则停止发送
  2. 循环从可用读取器队列中获取下一个可发送数据的读取器
    • 若队列中无可用读取器, 说明暂时无数据可发送,直接返回
  3. 获取下一个待发送缓冲区所属的子分区ID
  4. 从读取器获取下一个缓冲区,同时消耗1个信用值(若为数据缓冲区)--调reader.getNextBuffer();
  5. 处理"无数据可发送"的情况
  6. 处理"有数据可发送"的情况
    1. 检查读取器是否还有更多数据可立即发送:就是检查nextDataType是否为DataType.NONE
    2. 构建包含缓冲区数据和元信息的网络消息
    3. 异步发送数据并注册监听器
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. 从子分区视图获取下一个缓冲区(待发送的数据)
  2. 若为数据缓冲区(非事件),则消耗 1 个信用值
    • 信用值不足时抛异常
  3. 构造返回结果,包含缓冲区和下一个数据类型
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; // 否则,不可发送
}
相关推荐
想用offer打牌3 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
KYGALYX4 小时前
服务异步通信
开发语言·后端·微服务·ruby
Hello.Reader5 小时前
Flink ZooKeeper HA 实战原理、必配项、Kerberos、安全与稳定性调优
安全·zookeeper·flink
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
爬山算法5 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
Cobyte6 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
程序员侠客行7 小时前
Mybatis连接池实现及池化模式
java·后端·架构·mybatis
Honmaple7 小时前
QMD (Quarto Markdown) 搭建与使用指南
后端
PP东8 小时前
Flowable学习(二)——Flowable概念学习
java·后端·学习·flowable