欢迎大家关注 github.com/hsfxuebao/j… ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
Kafka源码分析7-筛选可以发送消息的broker 中如何建立网络连接,本章将详细讲解这部分内容。
网络核心组件分析
我们先来介绍下网络设计的核心组件,NetworkClient
、Selector
、KafkaChannel
、TransportLayer
对应的代码如下:
public class NetworkClient implements KafkaClient {
private static final Logger log = LoggerFactory.getLogger(NetworkClient.class);
/* the selector used to perform network i/o */
private final Selectable selector;
private final MetadataUpdater metadataUpdater;
private final Random randOffset;
/* the state of each node's connection */
private final ClusterConnectionStates connectionStates;
/* the set of requests currently being sent or awaiting a response */
private final InFlightRequests inFlightRequests;
// 省略。。。
}
NetworkClient初始化而言,大家就应该已经搞清楚了,NetworkClient主要是一个网络通信组件,底层核心的Selector负责最最核心的建立连接、发起请求、处理实际的网络IO,初始化的入口初步找到了,kafka自己封装的一些组件,但是,他的底层是有最最核心的Java NIO的Selector。
/***
* todo 这个Selector是kafka自己封装出来
* 基于java nio 里面的selector去封装的
*/
public class Selector implements Selectable {
public static final long NO_IDLE_TIMEOUT_MS = -1;
private static final Logger log = LoggerFactory.getLogger(Selector.class);
//这个对象就是javaNIO里面的Selector
//Selector是负责网络的建立,发送网络请求,处理实际的网络IO。
//所以他是最最核心的这么样的一个组件。
private final java.nio.channels.Selector nioSelector;
//broker 和 KafkaChannel的映射
//这儿的kafkaChannel大家暂时可以理解为就是SocketChannel
//代表的就是一个网络连接。
private final Map<String, KafkaChannel> channels;
//已经完成发送的请求
private final List<Send> completedSends;
//已经接收到的,并且处理完了的响应。
private final List<NetworkReceive> completedReceives;
//已经接收到了,但是还没来得及处理的响应。
//一个连接,对应一个响应队列
private final Map<KafkaChannel, Deque<NetworkReceive>> stagedReceives;
private final Set<SelectionKey> immediatelyConnectedKeys;
private final Map<String, KafkaChannel> closingChannels;
//没有建立连接的主机
private final List<String> disconnected;
// 建立连接的主机
private final List<String> connected;
//建立连接失败的主机。
private final List<String> failedSends;
private final Time time;
private final SelectorMetrics sensors;
private final String metricGrpPrefix;
private final Map<String, String> metricTags;
private final ChannelBuilder channelBuilder;
private final int maxReceiveSize;
private final boolean metricsPerConnection;
private final IdleExpiryManager idleExpiryManager;
}
最最核心的一点,就是在KafkaSelector的底层,其实就是封装了原生的Java NIO的Selector,很关键的组件,就是一个多路复用组件,他会一个线程调用他直接监听多个网络连接的请求和响应。相关参数的定义如下:
-
maxReceiveSize,最大可以接收的数据量的大小
-
connectionsMaxIdle,每个网络连接最多可以空闲的时间的大小,就要回收掉
-
Map<String, KafkaChannel> channels,这里保存了每个broker id到Channel的映射关系,对于每个broker都有一个网络连接,每个连接在NIO的语义里,都有一个对应的SocketChannel,我们估计,KafkaChannel封装了SocketChannel
-
List completedSends,已经成功发送出去的请求
-
List completedReceives,已经接收回来的响应而且被处理完了
-
Map<KafkaChannel, Dequeue>,每个Broker的收到的但是还没有被处理的响应
-
conneted、disconnected、failedSends,已经成功建立连接的brokers,以及还没成功建立连接的brokers,发送请求失败的brokers
-
channels
broker id对应一个网络连接,一个网络连接对应一个KafkaChannel,底层对应的是SocketChannel,SocketChannel对应的是最最底层的网络通信层面的一个Socket,套接字通信,Socket通信,TCP -
Send,应该是说要交给这个底层的Channel发送出去的请求,可能会不断的变换的,因为发送完一个请求需要发送下一个请求
-
NetworkReceive,这个Channel最近一次读出来的响应,先暂存在这里,也是会不断的变换的,因为会不断的读取新的响应数据
-
TransportLayer是封装了底层的Java NIO的SocketChannel
Selector内部的源码一定要带着大家深入到每个细节的研究,因为这是完全经历过全世界大量的、复杂的、大规模的场景考验的一套网络通信的框架,基于NIO封装的一套网络通信的框架
kafkaChannel
和PlaintextTransportLayer如下:
public class KafkaChannel {
private final String id;
private final TransportLayer transportLayer;
private final Authenticator authenticator;
private final int maxReceiveSize;
// 读缓存,底层使用ByteBuffer实现
private NetworkReceive receive;
// 写缓存,底层使用ByteBuffer实现
private Send send;
// Track connection and mute state of channels to enable outstanding requests on channels to be
// processed after the channel is disconnected.
private boolean disconnected;
private boolean muted;
}
public class PlaintextTransportLayer implements TransportLayer {
private final SelectionKey key;
private final SocketChannel socketChannel;
private final Principal principal = KafkaPrincipal.ANONYMOUS;
}
Network、Selector、Channel他们是如何初始化的,kafka如何封装的,以及与原生Java NIO的Selector、Channel的关系是如何的,结合如下框图进行理解:
代码分析
接着 Kafka源码分析7-筛选可以发送消息的broker 进行如下分析,initiateConnect(node, now)
如下:
private void initiateConnect(Node node, long now) {
String nodeConnectionId = node.idString();
try {
log.debug("Initiating connection to node {} at {}:{}.", node.id(), node.host(), node.port());
this.connectionStates.connecting(nodeConnectionId, now);
//TODO 尝试建立连接
selector.connect(nodeConnectionId,
new InetSocketAddress(node.host(), node.port()),
this.socketSendBuffer,
this.socketReceiveBuffer);
} catch (IOException e) {
/* attempt failed, we'll try again after the backoff */
connectionStates.disconnected(nodeConnectionId, now);
/* maybe the problem is our metadata, update it */
metadataUpdater.requestUpdate();
log.debug("Error connecting to node {} at {}:{}:", node.id(), node.host(), node.port(), e);
}
}
接着看org.apache.kafka.common.network.Selector 的connect() 方法:
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);
//要是了解java NIO编程的同学,这些代码就是一些基本的
//代码,跟我们NIO编程的代码是一模一样。
//获取到SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞的模式
socketChannel.configureBlocking(false);
Socket socket = socketChannel.socket();
socket.setKeepAlive(true);
//设置一些参数
//这些网络的参数,我们之前在分析Producer的时候给大家看过
//都有一些默认值。
if (sendBufferSize != Selectable.USE_DEFAULT_BUFFER_SIZE)
socket.setSendBufferSize(sendBufferSize);
if (receiveBufferSize != Selectable.USE_DEFAULT_BUFFER_SIZE)
socket.setReceiveBufferSize(receiveBufferSize);
//这个的默认值是false,代表要开启Nagle的算法
//它会把网络中的一些小的数据包收集起来,组合成一个大的数据包
//然后再发送出去。因为它认为如果网络中有大量的小的数据包在传输
//其实是会影响网络拥塞。
//kafka一定不能把这儿设置为false,因为我们有些时候可能有些数据包就是比较
//小,他这儿就不帮我们发送了,显然是不合理的。
socket.setTcpNoDelay(true);
boolean connected;
try {
//尝试去服务器去连接。
//因为这儿非阻塞的
//有可能就立马连接成功,如果成功了就返回true
//也有可能需要很久才能连接成功,返回false。
connected = socketChannel.connect(address);
} catch (UnresolvedAddressException e) {
socketChannel.close();
throw new IOException("Can't resolve address: " + address, e);
} catch (IOException e) {
socketChannel.close();
throw e;
}
//SocketChannel往Selector上注册了一个OP_CONNECT
SelectionKey key = socketChannel.register(nioSelector, SelectionKey.OP_CONNECT);
//根据根据SocketChannel 封装出来一个KafkaChannel
KafkaChannel channel = channelBuilder.buildChannel(id, key, maxReceiveSize);
//把key和KafkaChannel关联起来
//后面使用起来会比较方便
//我们可以根据key就找到KafkaChannel
//也可以根据KafkaChannel找到key
key.attach(channel);
//缓存起来了
this.channels.put(id, channel);
//所以正常情况下,这儿网络不能完成连接。
//如果这儿不能完成连接。大家猜一下
//kafka会在哪儿完成网络最后的连接呢?
if (connected) {
// OP_CONNECT won't trigger for immediately connected channels
log.debug("Immediately connected to node {}", channel.id());
immediatelyConnectedKeys.add(key);
// 取消前面注册 OP_CONNECT 事件。
key.interestOps(0);
}
}
SocketChannel的参数
-
configureBlocking
设置为非阻塞模式 -
epalive的意思,主要是避免客户端和服务端任何一方如果断开连接之后,别人不知道,一直保持着网络连接的资源;所以设置这个之后,2小时内如果双方没有任何通信,那么发送一个探测包,根据探测包的结果保持连接、重新连接或者断开连接
-
设置socket的发送和接收的缓冲区的大小,分别是128kb和32kb,这个缓冲区的大小一般都是在NIO编程里需要自己去设置的
-
TcpNoDelay,如果默认是设置为false的话,那么就开启Nagle算法,就是把网络通信中的一些小的数据包给收集起来,组装成一个大的数据包然后再一次性的发送出去,如果大量的小包在传递,会导致网络拥塞;如果设置为true的话,意思就是关闭Nagle,让你发送出去的数据包立马就是通过网络传输过去,所以这个参数大家也要注意下
NetworkClient、Selector、KafkaChannel、ConnectStates,这些东西是极为值得我们来研究的,对我们的技术底层的功底的夯实极为有好处,假设我们真的要去开发一个网络通信的程序,打算基于NIO来做
对于客户端而言,他的SocketChannel到底应该如何来设置呢?你就可以参考人家做法:KeepAlive、TcpNoDelay、SocketBuffer
connect(address)
如果这个SocketChannel是被设置为非阻塞模式的话,那么对这个connect方法的调用,会初始化一个非阻塞的连接请求,如果这个发起的连接立马就成功了,比如说客户端跟要连接的服务端都在一台机器上,此时就会出现一个立马就连接成功的情况,然后就会返回一个true;否则只要不是那种立马可以连接成功的情况,就会返回一个false,接着就需要在后面去调用SocketChannel的finishConnect方法,去完成最终的连接
缓存kafkaChannel
初始化了一个SocketChannel然后就发起了一个连接请求,接着不管连接请求是成功还是暂时没成功,都需要把这个SocketChannel给缓存起来,接下来你才可以基于这个东西去完成连接,或者是发起读写请求
发起连接之后,直接就把这个SocketChannel给注册到Selector上去了,让Selector监视这个SocketChannel的OP_CONNECT事件,就是是否有人同意跟他建立连接,会获取到一个SelectionKey
接着就是将SelectionKey、brokerid封装为了KafkaChannel,他是先把SelectionKey封装到TransportLayer里面去(SelectionKey底层是跟SocketChannel是一一对应起来),Authenticator,brokerid,直接封装一个KafkaChannel
大概可以认为是把一个核心的组件跟SelectionKey给关联起来,后续在通过SelectionKey进行网络请求和相应的处理的时候,就可以从SelectionKey里获取出来SocketChannel,可以获取出来之前attach过的一个核心组件,复制请求响应的处理。缓存起来立即建立好连接的SelectionKey
总结
原生NIO编程的时候,可以把他原生的 API和组件封装一下,就可以基于你自己的需求实现不同的功能了,你一定要学会进行一定的缓存机制的设计,比如说针对多个机器进行连接,那么对应的连接组件就需要进行缓存
参考文档:
史上最详细kafka源码注释(kafka-0.10.2.0-src)
kafka技术内幕-图文详解Kafka源码设计与实现