1 层次结构

负责进行网络IO请求的是NetworkClient,主要层次结构如下

ClusterConnectionStates报存了每个节点的状态,以node为key,以node的状态为value;inFlightRequets中保存了每个节点已经发送的请求,但是还没有返回的请求,以node为key,以List<ClientRequest>为value。inFlightRequets从名字也可以看出,表示“正在空中飞”的请求。

2 如何保证每次只发送一个请求

sender线程启动后,如果RecordBatch中有消息,会将消息按照所在节点重新排列,每个节点会创建一个ClientRequest,用来发送,每个节点每次只能发送一个ClientRequest,如下

KafkaChannel#setSend(..)

public void setSend(Send send) {
if (this.send != null) // 如果已经有send,会抛出异常
throw new IllegalStateException("Attempt to begin a send operation with prior send operation still in progress.");
this.send = send;
this.transportLayer.addInterestOps(SelectionKey.OP_WRITE);
}

那么kafka是如何保证避免setSend的时候KafkaChannel中已经没有send了呢,这个关键就是在sender线程中会调用NetworkClient#ready(..),会将没有ready的节点去除掉,从而不会在该节点上setSend:

while (iter.hasNext()) {
Node node = iter.next();
if (!this.client.ready(node, now)) { // 关键
iter.remove();
notReadyTimeout = Math.min(notReadyTimeout, this.client.connectionDelay(node, now));
}
}

3 NetworkClient#ready(..)

NetworkClient#ready(..)检查节点是否准备好,从而决定是否可以将消息封装成ClientRequest放到KafkaChannel上。

public boolean ready(Node node, long now) {
if (node.isEmpty())
throw new IllegalArgumentException("Cannot connect to empty node " + node); if (isReady(node, now)) // 关键
return true; if (connectionStates.canConnect(node.idString(), now))
initiateConnect(node, now); return false;
}

我们来分析下isReady

public boolean isReady(Node node, long now) {
return !metadataUpdater.isUpdateDue(now) && canSendRequest(node.idString());
}

isReady主要两个条件,一个是判断metadata是否到了更新的时候了,如果metadata需要更新,那么就不发送本次请求,也就是metadata更新优先级高。第二个是判断这个节点是否canSendRequest。

private boolean canSendRequest(String node) {
return connectionStates.isConnected(node) && selector.isChannelReady(node)
&& inFlightRequests.canSendMore(node); // 重点
}

inFlightRequests保存的是“正在空中飞”的请求

public boolean canSendMore(String node) {
Deque<ClientRequest> queue = requests.get(node);
return queue == null || queue.isEmpty() ||
(queue.peekFirst().request().completed() && queue.size() < this.maxInFlightRequestsPerConnection);
}

满足以下几个条件,表示可以继续send

  1. queue是空,即该节点没有“正在空中飞”的request
  2. queue不为空。queue中排在最开头的request已经completed 并且queue的大小小于允许的最大值。如何理解呢?queue是一个双端队列,每次add的时候都会在queue的头部插入,所以queue中第一个就是正在发送的,同样也是KafkaChannel中的send。只有当send发送到网络中的时候才可以继续发送。这就保证了前面说的“如何保证每次只发送一个请求”。

4 inFlightRequests

inFlightRequests保存了"正在空中飞"的请求,所谓“正在空中飞”的意思就是request已经发送到了网络上,但是服务端还没有回ack。NetworkClient#doSend会往inFlightRequests头部放置一个request,同时会在KafkaChannel中放置一个request.send。

public void add(ClientRequest request) {
Deque<ClientRequest> reqs = this.requests.get(request.request().destination());
if (reqs == null) {
reqs = new ArrayDeque<>();
this.requests.put(request.request().destination(), reqs);
}
reqs.addFirst(request); // 重点,插入到头部
}

5 Selector#pollSelectionKeys

Selector#pollSelectionKeys用来处理读写事件。先看写事件

if (channel.ready() && key.isWritable()) {
Send send = channel.write();
if (send != null) {
this.completedSends.add(send); // 请求写完了会放到completedSends中
this.sensors.recordBytesSent(channel.id(), send.size());
}
}

往网络中写的时候,会调用KafkaChannel#write来写。

public Send write() throws IOException {
Send result = null;
if (send != null && send(send)) {
result = send;
send = null; // kafkaChannel中的send被置为null,这时候新的request可以发送了
}
return result;
}
private boolean send(Send send) throws IOException {
send.writeTo(transportLayer);
if (send.completed())
transportLayer.removeInterestOps(SelectionKey.OP_WRITE); return send.completed();
}

发送可能一次不能完全发送完毕,需要发送好几次才能将request全部发送到网络上,只有这个request发送完毕了才能将KafkaChannel中的send置为null,新的request才可以setSend。但此时inFlightRequests并没有移除该request,也就是说该request还在"飞",但是新的request可以添加。发送完毕后会将channel的SelectionKey.OP_WRITE移除,没有发送完毕不会移除,下次轮询的时候该节点没有ready,不会添加新的request,会继续发送没有发完的request。

对于ack=0,不要求服务端ack就表示发送成功。该处理是在NetworkClient#handleCompletedSends(..)中进行的

private void handleCompletedSends(List<ClientResponse> responses, long now) {
// if no response is expected then when the send is completed, return it
for (Send send : this.selector.completedSends()) {
ClientRequest request = this.inFlightRequests.lastSent(send.destination());
if (!request.expectResponse()) { // ack = 0不需要服务端response
this.inFlightRequests.completeLastSent(send.destination()); // request从inFlightRequests中移除,表示此次请求完毕
responses.add(new ClientResponse(request, now, false, null));
}
}
}

对于ack !=0 ,则要求服务端ack才表示发送成功,该处理是在

NetworkClient#handleCompletedReceives(..)中进行

private void handleCompletedReceives(List<ClientResponse> responses, long now) {
for (NetworkReceive receive : this.selector.completedReceives()) {
String source = receive.source();
ClientRequest req = inFlightRequests.completeNext(source);
Struct body = parseResponse(receive.payload(), req.request().header());
if (!metadataUpdater.maybeHandleCompletedReceive(req, now, body))
responses.add(new ClientResponse(req, now, false, body));
}
}

5 参考

https://blog.csdn.net/chunlongyu/article/details/52651960

kafka生产者网络层总结的更多相关文章

  1. 【转】 详解Kafka生产者Producer配置

    粘贴一下这个配置,与我自己的程序做对比,看看能不能完善我的异步带代码:   -----------------------------------------    详解Kafka生产者Produce ...

  2. Kafka生产者-向Kafka中写入数据

    (1)生产者概览 (1)不同的应用场景对消息有不同的需求,即是否允许消息丢失.重复.延迟以及吞吐量的要求.不同场景对Kafka生产者的API使用和配置会有直接的影响. 例子1:信用卡事务处理系统,不允 ...

  3. Python 使用python-kafka类库开发kafka生产者&消费者&客户端

    使用python-kafka类库开发kafka生产者&消费者&客户端   By: 授客 QQ:1033553122       1.测试环境 python 3.4 zookeeper- ...

  4. Kafka集群安装部署、Kafka生产者、Kafka消费者

    Storm上游数据源之Kakfa 目标: 理解Storm消费的数据来源.理解JMS规范.理解Kafka核心组件.掌握Kakfa生产者API.掌握Kafka消费者API.对流式计算的生态环境有深入的了解 ...

  5. [Spark][kafka]kafka 生产者,消费者 互动例子

    [Spark][kafka]kafka 生产者,消费者 互动例子 # pwd/usr/local/kafka_2.11-0.10.0.1/bin 创建topic:# ./kafka-topics.sh ...

  6. Kafka权威指南 读书笔记之(三)Kafka 生产者一一向 Kafka 写入数据

    不管是把 Kafka 作为消息队列.消息总线还是数据存储平台来使用 ,总是需要有一个可以往 Kafka 写入数据的生产者和一个从 Kafka 读取数据的消费者,或者一个兼具两种角色的应用程序. 开发者 ...

  7. kafka生产者

    1.kafka生产者是线程安全的,她允许多个线程共享一个kafka实例 2.kafka管理一个简单的后台线程,所有的IO操作以及与每个broker的tcp连接通信,如果没有正确的关闭生产者可能会造成资 ...

  8. java实现Kafka生产者示例

    使用java实现Kafka的生产者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 3 ...

  9. kafka生产者和消费者流程

    前言 根据源码分析kafka java客户端的生产者和消费者的流程. 基于zookeeper的旧消费者 kafka消费者从消费数据到关闭经历的流程. 由于3个核心线程 基于zookeeper的连接器监 ...

随机推荐

  1. git子模块的使用

    1. 在项目中添加子模块 命令: git submodule add <url> 例子: git submodule add https://github.com/chaconinc/Db ...

  2. C#析构函数(方法)

    析构方法是在垃圾回收.释放资源时使用的.析构函数用于析构类的实例.备注:    不能在结构中定义析构函数.只能对类使用析构函数.    一个类只能有一个析构函数.    无法继承或重载析构函数.    ...

  3. String类型转int类型方法

    System.out.println( "Integer.parseInt(\"5\") =" + Integer.parseInt("5" ...

  4. Linux下搭建iSCSI共享存储

    转至:https://www.linuxidc.com/Linux/2016-09/135655.htm Linux下搭建iSCSI共享存储 拓扑: 实验步骤: ------------------- ...

  5. ROS入门介绍

    1.ROS版本介绍 ROS版本:(已经推出数十个版本) 2013 ------> Hydro 2014 ------> Indigo (对应Ubuntu14.04) (现在已经基本废弃) ...

  6. MongoDB聚合查询及Python连接MongoDB操作

    今日内容概要 聚合查询 Python操作MongoDB 第三方可视化视图工具 今日内容详细 聚合查询 Python操作MongoDB 数据准备 from pymongo import MongoCli ...

  7. pandas模块篇(终章)及初识mataplotlib

    今日内容概要 时间序列 针对表格数据的分组与聚合操作 其他函数补充(apply) 练习题(为了加深对DataFrame操作的印象) mataplotlib画图模块 今日内容详细 时间序列处理 时间序列 ...

  8. ROC/AUC以及相关知识点

    参考博文,特别的好!!!:https://www.jianshu.com/p/82903edb58dc AUC的计算: 法1:AUC为ROC曲线下的面积,那我们直接计算面积可得.面积为一个个小的梯形面 ...

  9. linux(Ubuntu)下机器学习/深度学习环境配置

    为了开发环境纯净,应该首先创建虚拟环境 mkvirtualenv -p python3 虚拟环境名称 如,mkvirtualenv -p python3 ai 但是有的童鞋会卡在这一步,会报一个这样的 ...

  10. Goland sync.Map大白话解析

    Goland sync.Map大白话解析 代码解析链接:https://mp.weixin.qq.com/s/H5HDrwhxZ_4v6Vf5xXUsIg 建议对照参考链接代码食用 结构体 可以简单理 ...