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()方法中处理的。这个方法的核心逻辑如下:
- 将一个NodeId对应的RecordBatch集合,重新整理为produceRecordsByPartition(Map<TopicPartition,ByteBuffer>)和recordsByPartition(Map<TopicPartition,RecordBatch>)两个集合
- 创建RequestSend,RequestSend是真正通过网络I/O发送的对象,其格式符合上面描述的Produce Request(Version:2)协议,其中有效负载就是produceRecordsByPartition中的数据
- 创建RequestCompletionHandler作为回调对象
- 将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)