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; // 否则,不可发送
}
相关推荐
超浪的晨6 小时前
Java 实现 B/S 架构详解:从基础到实战,彻底掌握浏览器/服务器编程
java·开发语言·后端·学习·个人开发
追逐时光者7 小时前
一款超级经典复古的 Windows 9x 主题风格 Avalonia UI 控件库,满满的回忆杀!
后端·.net
Python涛哥8 小时前
go语言基础教程:【1】基础语法:变量
开发语言·后端·golang
我命由我123458 小时前
PostgreSQL 保留关键字冲突问题:语法错误 在 “user“ 或附近的 LINE 1: CREATE TABLE user
数据库·后端·sql·mysql·postgresql·问题·数据库系统
LUCIAZZZ9 小时前
final修饰符不可变的底层
java·开发语言·spring boot·后端·spring·操作系统
wsj__WSJ9 小时前
Spring Boot 请求参数绑定:全面解析常用注解及最佳实践
java·spring boot·后端
CodeUp.10 小时前
SpringBoot航空订票系统的设计与实现
java·spring boot·后端
码事漫谈10 小时前
Linux下使用VSCode配置GCC环境与调试指南
后端
求知摆渡10 小时前
RocketMQ 从二进制到 Docker 完整部署(含 Dashboard)
运维·后端