Kafka核心源码分析-生产者-Sender

1.简单介绍

我们来了解下Sender线程发送消息的整个流程:首先,它根据RecordAccumulator的缓存情况,筛选出可以向哪些Node节点发送消息,即上一篇的介绍的RecordAccumulator.ready()方法;然后根据生产者与各个节点的连接情况(由NetworkClinet管理),过滤Node节点;之后,生成相应的请求,这里要特别注意的是,每个Node节点只生成一个请求;最后,调用NetworkClient将请求发送出去。

Sender实现了Runnable接口,并运行在单独的ioThread中。Sender的run()方法调用了其重载run(long),这才是Sender线程的核心方法,也是发送消息的关键流程。
在这里插入图片描述
针对上面的时序图,我们简单分析下获取的步骤

1.metadata.fetch()从metadata中获取Cluster,即获得元数据信息

2.RecordAccumulator.ready()我们上一篇已经讲过了,根据RecordAccumulator的缓存情况,选出可以向哪些Node节点发送消息,返回RecordAccumulator.ReadyCheckResult对象

3.result.unknownLeadersExist判断上一步拿到的ReadyCheckResult中是否有还不知道Leader节点的存在,那么强制Metadata去更新集群信息

4.从第二步获得的ReadyCheckResult中,拿到readyNodes,遍历此节点,并调用KafkaClient的.ready()判断到此节点的网络I/O方面是否符合发送消息的条件,不符合条件的Node将会从readyNodes集合中删除

5.使用第4步处理后的readyNodes()集合,调用RecordAccumulator.drain()方法,获取待发送的消息集合

6.调用RecordAccumulator.abortExpireBatches()方法处理RecordAccumulator中超时的消息。代码逻辑是,遍历RecordAccumulator中保存的全部RecordBatch,调用RecordBatch.maybeExpire()方法进行处理。如果已超时,则调用RecordBatch.done()方法,会触发自定义Callback,并将RecordBatch从队列中移除,释放ByteBuffer()空间

7.调用Sender.createProduceRequests()方法将待发送的消息封装成ClientRequest

8.KafkaClient.send()方法,将ClientRequest写入KafkaChannel的send字段

9.Kafka.poll()方法,将KafkaChannel.send()字段中保存的ClientRequest发送出去,同时,还会处理服务端发回的响应、处理超时的请求、调用用户自定义Callback等

2.Sender分析

2.1 请求头分析

我们先来看看请求的消息格式

Produce Request(Version:2)的请求头和请求体各个字段的含义如下表所示

名称 类型 含义
api_key short API标识
api_version short API版本号
correlation_id int 序号,由客户端产生,单调递增,服务端不做任何修改,在Response中会回传给客户端
client_id String 客户端ID,可为null
acks short 指定服务端响应此请求之前,需要有多少Replica成功复制了此请求的消息。-1表示整个ISR都完成了复制
timeout int 超时时间,单位是ms
topic String Topic的名称
partition int Partition编号
record_set byte数组 消息的有效负载

Produce Response(Version:2)各个字段与含义如下表所示

名称 类型 含义
correlation_id int 序号,由客户端产生,单调递增,服务端不做任何修改,在Response中会回传给客户端
topic String Topic的名称
partition int Partition编号
error_code short 异常编号
base_offset long 服务端为消息生成的偏移量
timestamp long 服务端产生的时间戳
throttle_time_ms int 延迟时长,单位是ms

Sender在发送请求时,会先把请求封装成ClientRequest,ClientRequest里面封装了RequestSend,也就是我们上面的Producer Request的消息格式。Kafka是在Sender.createProduceRequest()方法中处理的。这个方法的核心逻辑如下:

  1. 将一个NodeId对应的RecordBatch集合,重新整理为produceRecordsByPartition(Map<TopicPartition,ByteBuffer>)和recordsByPartition(Map<TopicPartition,RecordBatch>)两个集合
  2. 创建RequestSend,RequestSend是真正通过网络I/O发送的对象,其格式符合上面描述的Produce Request(Version:2)协议,其中有效负载就是produceRecordsByPartition中的数据
  3. 创建RequestCompletionHandler作为回调对象
  4. 将RequestSend对象和RequestCompletionHandler对象封装进ClientRequest对象中,并将其返回。

下面我们来看下源码

/**
     * Create a produce request from the given record batches
     * 发送线程为每个目标节点创建一个客户端请求
     */
    private ClientRequest produceRequest(long now, int destination, short acks, int timeout, List<RecordBatch> batches) {
   
        //注意:produceRecordsByPartition和recordsByPartition的value是不一样的,一个是ByteBuffer,一个是RecordBatch
        Map<TopicPartition, ByteBuffer> produceRecordsByPartition = new HashMap<TopicPartition, ByteBuffer>(batches.size());
        final Map<TopicPartition, RecordBatch> recordsByPartition = new HashMap<TopicPartition, RecordBatch>(batches.size());
        //步骤1:将RecordBatch列表按照partition进行分类,整理成上述两个集合
        for (RecordBatch batch : batches) {
   
            TopicPartition tp = batch.topicPartition;
            produceRecordsByPartition.put(tp, batch.records.buffer());
            recordsByPartition.put(tp, batch);
        }
        //步骤2:创建ProduceRequest和RequestSend
        ProduceRequest request = new ProduceRequest(acks, timeout, produceRecordsByPartition);
        RequestSend send = new RequestSend(Integer.toString(destination),
                                           this.client.nextRequestHeader(ApiKeys.PRODUCE),
                                           request.toStruct());
        //步骤3:创建RequestCompletionHandler作为回调对象,其具体逻辑在后面详解
        RequestCompletionHandler callback = new RequestCompletionHandler() {
   
            public void onComplete(ClientResponse response) {
   
                handleProduceResponse(response, recordsByPartition, time.milliseconds());
            }
        };
        //创建ClientRequest对象。注意其第二个参数,根据acks配置决定请求时是否需要获取响应
        return new ClientRequest(now, acks != 0, send, callback);
    }

到这里,ProduceRequest的格式以及创建过程就分析完了。创建在后面的流程中,发送的是RequestSend对象,会将ClientRequest放入InFlightRequests中缓存,当请求收到响应或出现异常时,通过缓存的ClientRequest调用其RequestCompletionHandler对象。

2.2 KSelector

在介绍NetworkClient之前,我们先来了解NetworkClient的整个结构,以及其依赖的其他组件。

上图中的Selectable的接口的实现类Selector是org.apache.kafka.common.network.Selector,为了方便区分,将其简称为KSelector。KSelector使用NIO异步非阻塞模式实现网络I/O操作,KSelector使用一个单独的线程可以管理多条网络连接上的connect,read,write等操作。底层是通过java.nio.channels来完成对消息的处理。

	//nio 中的Selector类型,用来监听网络I/O事件
    private final java.nio.channels.Selector nioSelector;
    //维护了NodeId和KafkaChannel之间的映射关系,表示生产者客户端与各个Node之间的网络连接
    //KafkaChannel是在SocketChannel上的又一层封装
    private final Map<String, KafkaChannel> channels;
    //记录已经完全发送出去的请求
    private final List<Send> completedSends;
    //记录已经完全接收到的请求
    private final List<NetworkReceive> completedReceives;
    //暂存一次OP_READ事件处理过程中读取到的全部请求,当一次OP_READ事件处理完成后,
    // 会将stagedReceives集合中的请求保存到completeReceives集合中
    private final Map<KafkaChannel, Deque<NetworkReceive>> stagedReceives;
    private final Set<SelectionKey> immediatelyConnectedKeys;
    //记录一次poll过程中发现的断开的链接
    private final List<String> disconnected;
    //记录一次poll过程中发现的新建立的链接
    private final List<String> connected;
    //记录向哪些Node发送的请求失败了
    private final List<String> failedSends;
    //用于创建KafkaChannel的Builder,根据不同的配置创建不同的TransportLayer的子类,然后创建KafkaChannel
    //我们可以认为,其创建的KafkaChannel封装的是PlaintextTransportLayer
    private final ChannelBuilder channelBuilder;
    //用来记录各个连接的使用情况,并据此关闭空闲时间超过connectionsMaxIdleNanos的连接
    private final Map<String, Long> lruConnections;

下面介绍KSelector的核心方法。KSelector.connect()方法主要负责创建KafkaChannel,并添加到channels集合中保存。源码如下:

/**
 *开始连接到给定的地址,并将连接添加到与给定id关联的此nioSelector
 *负责创建KafkaChannel()并添加到channels集合中保存
 */
public void connect(String id, InetSocketAddress address, int sendBufferSize, int receiveBufferSize) throws IOException {
   
        if (this.channels.containsKey(id))
            throw new IllegalStateException("There is already a connection for id " + id);

        //创建SocketChannel
        SocketChannel socketChannel = SocketChannel.open();
        //配置成非阻塞模式
        socketChannel.configureBlocking(false)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值