Kafka网络层第一道门:Acceptor线程是怎么启动的

摘要:前 23 篇一直在讲 Producer 端。从这篇开始翻到 Broker 端------Kafka 是怎么监听 9092 端口、接受 Producer 连接、把连接分发给 IO 线程的?这是整个 Broker 网络层的起点。

关键词:Kafka Broker、Acceptor、SocketServer、Processor、NIO、网络线程模型

前情:从 Producer 翻到 Broker

前面讲了 23 篇 Producer 端------从 send() 初始化到内存回收、异常处理、超时机制。但有个问题一直没回答:Producer 发出去的 TCP 连接,Broker 这边是怎么接的?

Kafka Broker 的网络层不是简单的 ServerSocket 监听。它用了经典的 Reactor 模式------一个 Acceptor 接连接,多个 Processor 处理 IO。这篇从源码看这个模型是怎么启动的。

01 从 Kafka.main() 到 SocketServer

Broker 启动的入口是 Kafka.main(),最终走到 KafkaServer.startup()。网络层的初始化在 SocketServer 里:

java 复制代码
// KafkaServer.startup() 中
this.socketServer = new SocketServer(config, metrics, time);
this.socketServer.startup();

SocketServer.startup() 做了三件事:

java 复制代码
// SocketServer.startup()
public void startup() {
    // 1. 创建 RequestChannel(请求队列,供业务线程消费)
    this.requestChannel = new RequestChannel(...);

    // 2. 创建 Acceptor 线程(每个监听器一个)
    for (Listener listener : listeners) {
        Acceptor acceptor = new Acceptor(...);
        acceptorThread = new Thread(acceptor, "kafka-network-thread-acceptor");
        acceptorThread.start();
    }

    // 3. 创建 N 个 Processor 线程
    for (int i = 0; i < numNetworkThreads; i++) {
        Processor processor = new Processor(i, ...);
        Thread thread = new Thread(processor, "network-processor-" + i);
        thread.start();
    }
}

关键:Acceptor 是每个监听器一个 (listener 决定监听哪个端口/协议),Processor 是全局共享的 N 个。默认 num.network.threads=3。

02 为什么是 1 + N 模型

为什么 Acceptor 只有一个,Processor 有 N 个?因为 accept() 操作本身很快------内核完成三次握手后把连接放到 accept 队列,accept() 就是从队列取一个。真正耗时的是读请求、写响应。

如果用单线程:accept 慢的时候 IO 也跟着慢,IO 慢的时候 accept 也堵。

如果每个连接一个线程:连接数上去了线程爆炸。

Kafka 的解法:Acceptor 只负责 accept,不做任何 IO;Processor 用 NIO Selector 处理大量连接的读写。这就是经典的"单 Reactor 多线程"模型。

Acceptor 接受连接后,不是自己处理,而是轮询分发给 Processor:

java 复制代码
// Acceptor.run() 中分发逻辑
private volatile int nextProcessorIndex = 0;

private Processor nextProcessor() {
    // round-robin 轮询
    int idx = nextProcessorIndex;
    nextProcessorIndex = (nextProcessorIndex + 1) + processors.size();
    return processors.get(idx);
}

每个 Processor 有自己的 Selector 和一个 newConnections 队列。Acceptor 把新连接塞进队列,Processor 自己在 run() 循环里取出来、注册 OP_READ。

03 Acceptor.run() 核心循环

Acceptor 的核心就是一个 while 循环:

java 复制代码
// Acceptor.run()
public void run() {
    // 绑定端口
    serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.bind(new InetSocketAddress(port));
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

    while (isRunning) {
        // 1. 等待 accept 事件,500ms 超时
        selector.select(500);
        Set<SelectionKey> keys = selector.selectedKeys();

        for (SelectionKey key : keys) {
            if (key.isAcceptable()) {
                // 2. 接受新连接
                SocketChannel socketChannel = serverSocketChannel.accept();

                // 3. 限流检查
                if (connectionLimitReached(socketChannel)) {
                    socketChannel.close();
                    continue;
                }

                // 4. 轮询选 Processor,加入其队列
                Processor processor = nextProcessor();
                processor.accept(new Connection(...));
            }
        }
        keys.clear();
    }
}

几个细节值得注意:

select(500ms) 不是 select()。为什么带超时?因为需要定期检查 isRunning 标志------如果 Broker 要关闭,不能无限阻塞在 select 上。500ms 超时后醒一次,看看是不是该退出了。

accept() 是非阻塞的。ServerSocketChannel 配置了 non-blocking,accept() 立即返回。如果 accept 队列里没有连接,返回 null。

限流检查在分发之前。max.connections 和 max.connections.per.ip 控制全局和单 IP 连接数。超限的连接直接 close------不进 Processor 队列。

04 一个容易忽略的设计:newConnections 队列

Acceptor 调 processor.accept(),不是直接把 SocketChannel 注册到 Processor 的 Selector 上,而是先塞进一个队列:

java 复制代码
// Processor.accept()
public void accept(Connection connection) {
    newConnections.add(connection);
    wakeup();  // 唤醒 Processor 的 selector
}

为什么要队列?因为 Acceptor 和 Processor 是不同线程。Acceptor 不能直接操作 Processor 的 Selector(Selectable 不是线程安全的)。Processor 自己在 run() 循环里:

java 复制代码
// Processor.run() 开头
public void run() {
    // 把 newConnections 队列里的连接全部注册到 selector
    configureNewConnections();
    // 然后才开始 select
    selector.poll(pollTimeoutMs);
}

这个设计和 Producer 端的 RecordAccumulator 思路一致------跨线程通信通过队列,不直接共享状态。

写在最后

Acceptor 线程虽然代码不长,但它决定了整个 Broker 网络层的骨架:一个线程接连接,N 个线程处理 IO,连接通过队列分发,Selector 各自独立。

相关推荐
银河麒麟操作系统1 小时前
两案例入选全球计算联盟年度产业应用案例汇编
大数据·汇编·人工智能
huaweichenai2 小时前
spring boot对接kafka实现消息队列处理
spring boot·后端·kafka
Elastic 中国社区官方博客2 小时前
Elastic 在《Forrester Wave™:2026 年第四季度智能运维平台》报告中被评为强劲表现者
大数据·运维·数据库·elasticsearch·搜索引擎·全文检索
数治研习社2 小时前
数据治理第十九篇|DAMA 十一个核心数据管理领域:从信息化到智能化,用三层主线读懂
大数据·人工智能
你看我名字2 小时前
Kafka 架构、消息可靠性与生产运维:从分区模型到故障治理
运维·架构·kafka
QYRese2 小时前
CAGR3.1%!抗辐射相机市场迎替换潮,核设施数字化、水下场景带来增量
大数据·数码相机
棣廷3 小时前
初识Spark Core
大数据·分布式·spark
HRaitest11 小时前
秋招技术岗招聘提效:三类 AI 招聘方案的技术底层与选型逻辑
大数据·人工智能·求职招聘·ai-native
starzy199013 小时前
Kafka源码深度剖析:消息有异常是如何处理的
大数据·kafka