跳到主要内容

06、Kafka 实战 - 生产者:消息发送线程解析

追加消息到 RecordAccumulator 的时候会根据对应的 topic 分区信息获取队列,然后将消息放到 batches 集合中,最后使用一个 Sender 线程迭代 batches 的每个分区

Sender 发送线程的两种发送方式

  • 按照分区直接发送:有多少个分区就要发送多少次请求
  • 按照分区的目标节点发送:把属于同一个节点的所有分区放在一起发送,减少网络开销

Sender 流程

  • 消息被 RecordAccumulator 按照分区追加到队列的最后一个批记录中
  • Sender 通过 ready() 从 RecordAccumulator 中找出已经准备好的服务端节点
  • 如果节点已经准备好,但是客户端还没有和它们建立连接,通过 connect() 建立到服务端节点的连接
  • Sender 通过 drain() 从 RecordAccumulator 获取按照节点整理好的每个分区的批记录
  • Sender 得到每个节点的批记录后,为每个节点创建请求,并发送到服务端

从 RecordAccumulator 获取数据

  • 追加到 RecordAccumulator 的消息按照分区放好,发送线程读取数据的时候,RecordAccumulator 按照节点将消息重新分组 再交给 Sender

  • batches 存储结构

  • 分区存储 batches:ConcurrentMap<TopicPartition, Deque>

  • 节点存储 batches:Map<Integer, List>

  • 从分区存储 batches 转为节点存储 batches 流程

  • 迭代分区存储 batches 的每个分区,获取 TopicPartition 对应的节点 NodeId

  • 获取分区存储的批记录队列中的第一个批记录 RecordBatch

  • 将相同节点的所有批记录放在一起,放入 List 中,数据结构 Map<NodeId, List>

Sender 代码截取

void run(long now) {
   
     
	// TODO 获取元数据
	// 第一次里面没有元数据,第二次进来就有元数据
	Cluster cluster = metadata.fetch();
	// get the list of partitions with data ready to send 获取准备发送数据的分区列表
	// 判断哪些 partition 有消息可以发送,然后获取到这个 partition 的 leader partition 对应的 broker 主机
	RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);
	// remove any nodes we aren't ready to send to 删除我们不准备发送到的所有节点
	// readyNodes 可以发送消息的
	Iterator<Node> iter = result.readyNodes.iterator();
	while (iter.hasNext()) {
   
     
        Node node = iter.next();
        // TODO 检查与要发送数据的主机的网络是否已经建立好
        if (!this.client.ready(node, now)) {
   
     
            iter.remove();
        }
    }
	/**
	 * TODO 按照 broker 进行分组,同一个 broker 的 partition 为同一组
	 * create produce requests 创建生成请求
	 * 发送的 partition 有多个,可能有一些 leader partition 在同一台服务器上面
	 * 按照 broker 进行分组,同一个 broker 的 partition 为同一组
	 *  0:[p1,p2]
	 *  1:[p3,p4,p5]
	 */
	Map<Integer, List<RecordBatch>> batches = this.accumulator.drain(cluster,result.readyNodes,this.maxRequestSize,now);
	/**
	 * TODO 创建发送消息的请求
	 * 如果网络连接没有建立好,batches 还是为空,这里也不执行
	 * 创建请求
	 *      往 partition 上面发送消息的时候,有一些 partition 在同一台服务器上,如果一个个分区发送会浪费资源
	 *      发往同一个 broker 上面 partition
	 */
	List<ClientRequest> requests = createProduceRequests(batches, now);
	for (ClientRequest request : requests)
	    // TODO 发送请求
	    client.send(request, now);
}

创建生产者客户端请求

  • 从 RecordAccumulator 获取到的 batches 已经按照节点分组,produceRequest() 方法会为每个节点创建客户端请求
  • 由于每个节点的 batches 批记录对应一个分区,同一个节点有多个分区,所以需要把 batches 转成 Map<TopicPartition, ByteBuffer>
// run() -> createProduceRequests() -> produceRequest()

/**
 * Create a produce request from the given record batches
 * 从给定的记录批次创建一个生产请求
 */
private ClientRequest produceRequest(long now, int destination, short acks, int timeout, List<RecordBatch> batches) {
   
     
    Map<TopicPartition, ByteBuffer> produceRecordsByPartition = new HashMap<TopicPartition, ByteBuffer>(batches.size());
    final Map<TopicPartition, RecordBatch> recordsByPartition = new HashMap<TopicPartition, RecordBatch>(batches.size());
    for (RecordBatch batch : batches) {
   
     
        // 每一个 RecordBatch 都有唯一的 TopicPartition
        TopicPartition tp = batch.topicPartition;
        // RecordBatch 的 records 是 MemoryRecords,底层是 ByteBuffer
        produceRecordsByPartition.put(tp, batch.records.buffer());
        recordsByPartition.put(tp, batch);
    }
    // 构造生产者请求,最后封装为统一的客户端请求
    ProduceRequest request = new ProduceRequest(acks, timeout, produceRecordsByPartition);
    RequestSend send = new RequestSend(Integer.toString(destination),
                                       this.client.nextRequestHeader(ApiKeys.PRODUCE),
                                       request.toStruct());
    // 封装请求里面带一个回调函数,当客户端请求完成后,会调用回调函数
    RequestCompletionHandler callback = new RequestCompletionHandler() {
   
     
        public void onComplete(ClientResponse response) {
   
     
            // 回调函数执行的方法
            handleProduceResponse(response, recordsByPartition, time.milliseconds());
        }
    };
    return new ClientRequest(now, acks != 0, send, callback);
}

注意:Sender 并不是真正负责发送客户端请求的线程,只是获取数据并创建客户端请求,然后交给 NetworkClient 客户端网络对象去发送