前言:

消息系统通常由生产者( producer ) 、消费者( consumer )和消息代理( broker ) 三大部分组成,生产者会将消息写入消息代理,消费者会从消息代理中读取消息。对于消息代理而育,生产者和消费者都属于客户端:生产者和消费者会发送客户端请求给服务端,服务端的处理分别是存储消息和获取消息,最后服务端返回响应结果给客户端。

客户端和服务端的通信涉及网络中不同的节点,客户端和服务端都会有一个连接对象,负责数据的发送和接收:比如客户端会发送请求、接收响应,服务端会接收请求、发送响应。生产者和消费者客户端与服务端完成一次网络通信的具体步骤如下。
(1)生产者客户端应用程序产生消息。
(2)客户端连接对象将消息包装到请求中,发送到服务端。
(3)服务端连接对象负责接收请求,并将消息以文件形式存储。
(4)服务端返回响应结果给生产者客户端。
(5)消费者客户端应用程序消费消息。
(6)客户端连接对象将消费信息也包装到请求中,发送给服务端。
(7)服务端从文件存储系统中取阳消息。
(8)服务端返回响应结果给消费者客户端。
(9)客户端将响应结果还原成消息,并开始处理消息。

 

为消息选择分区:

如果没有提前创建消息所属的主题,默认情况下主题的分区数量只有一个。一个主题只有一个分区时,会导致同一个主题的所有消息都只会保存到一个节点上。一般我们会提前创建主题,指定更多的分区数,这样同一个主题的所有消息就会分散在不同的节点上。
PartitionInfo对象表示一个分区的分布信息,它的成员变量有主题名称、分区编号、所在的主副本节点、所有的副本、ISR列表。把消息和PartitionInfo组合起来,就能表示消息被发送到哪个主题的哪个分区。

partition()方法会为消息、选择一个分区编号。为了保证消息负载均衡地分布到各个服务端节点,对于没有键的消息, 通过计数器自增轮询的方式依次将消息分配到不同的分区上;对于有键的消息,对键计算散列值,然后和主题的分区数进行取模得到分区编号。

客户端记录收集器:

生产者发迭的消息先在客户端缓存到记录收集器RecordAccumulator中,等到一定时机再由发送线程Send e 「批量地写入Kafka集群。生产者每生产一条消息,就向记录收集器中追加一条消息,追加方法的返回值表示批记录( Reco 「dBatch )是否满了: 如果批记录满了, 则开始发送这一批数据。如图2-5所示,每个分区都有一个双端队列用来缓存客户端的消息,队列的每个元素是一个批记录。一旦分区的队列中有批记录满了,就会被发送线程发送到分区对应的节点; 如果批记录没有满,就会继续等待直到收集到足够的消息。

kafka 连接服务端 kafka服务端与客户端_消息队列

追加消息时首先要获取分区所属的队列,然后取队列中最后一个批记录,如果队列中不存在批记录或者上一个批记录已经写满,应该创建新的批记录,并且加入队列的尾部。这里我们把每个批记录看作队列的一个元素,先创建的批记录最先被旧的消息填满,后创建的批记录表示最近的消息,追加消息时总是往最近的批记录中添加。如图2 -6所示,往记录收集器中追加一条记录有多个分支条件和判断。
(1)队列中不存在批记录,进入步骤(5) 。
(2)如果存在旧的批记录,尝试追加当前一条消息,并判断能不能追加成功。
(3)如果追加成功,说明已有的批记录可以容纳当前这条消息,返回结果。
(4)如果追加不成功,说明虽然有旧的批记录,但是容纳不下当前这一条消息,进入下一步。
(5)创建一个新的批记录,并往其中添加当前消息,新的批记录一定能容纳当前这条消息。

kafka 连接服务端 kafka服务端与客户端_服务端_02

客户端消息发送线程:

1 . 从记录收集器获取数据

生产者发送的消息在客户端首先被保存到记录收集器中,发送线程需要发送消息时,从中获取就可以了。不过记录收集器并不仅仅将消息暂存起来, 而且为了使发送线程能够更好地工作,追加到记录收集器的消息将按照分区放好。在发送线程需要数据时, 记录收集器能够按照节点将消息重新分组再交给发送线程。发送线程从记录收集器中得到每个节点上需要发送的批记录列表, 为每个节点都创建一个客户端请求(CllentRequest)。

(1)消息被记录收集器收集,并按照分区追加到队列的最后一个批记录中。
(2)发送钱程通过ready ()从记录收集器中找出已经准备好的服务端节点。
(3)节点已经准备好, 如果客户端还没有和它们建立连接,通过connect () 建立到服务端的连接。
(4)发送钱程通过drain()从记录收集器获取按照节点整理好的每个分区的批记录。
(5)发送线程得到每个节点的批记录后,为每个节点创建客户端请求,并将请求发送到服务端。

kafka 连接服务端 kafka服务端与客户端_消息队列_03

 创建生产者客户端请求:

从记录收集器获取出来的batches 已经按照节点分组了, 发送线程的produceRequest () 方法会为每个节点都创建一个客户端请求。produceRequest() 方法的batches参数是指定目标节点的批记录列表。由于一个目标节点就有多个分区,每个批记录都对应了一个分区。创建客户端请求时, 批记录列表需要转成分区到字节缓冲区的字典结构。

kafka 连接服务端 kafka服务端与客户端_kafka_04

这里需要注意的是, 发送线程并不负责真正发送客户端请求, 它会从记录收集器中取出要发送的消息, 创建好客户端请求,然后把请求交给客户端网络对象( NetworkClient )去发送。因为没有在发送线程中发送请求,所以创建客户端请求时需要保留目标节点,这样客户端网络对象获取出客户端请求时, 才能知道要发送给哪个目标节点。

客户端网络连接对象:

客户端网络连接对象( NetworkClient )管理了客户端和服务端之间的网络通信,包括连接的建立、发送客户端请求、读取客户端响应。

1 准备发送客户端请求

客户端向服务端发送请求需要先建立网络连接。如果服务端还没有准备好,即还不能连接,这个节点在客户端就会被移除掉,确保消息不会发送给还没有准备好的节点;如果服务端已经准备好了,则调用select. connect () 方法建立到目标节点的网络连接。

连接建立后,发送线程调用NetworkClient.send () ,先将客户端请求加入inFlightRequests列表,然后调用selector. send ()方法。注意:这一步只是将请求暂存到节点对应的网络通道中,还没有真正地将客户端请求发送出去。

2. 客户端轮询并调用回调函数

发送线程run ()方法的最后一步是调用NetworkClient 的poll () 方法。轮询的最关键步骤是调用selector.poll ()方法,而在轮询之后,定义了多个处理方法。轮询不仅仅会发送客户端请求,也会接收客户端响应。客户端发送请求后会调用handleCompletedSends ()处理已经完成的发送,客户端接收到响应后会调用handleCompletedReceives ()处理已经完成的接收。

kafka 连接服务端 kafka服务端与客户端_客户端_05

 

服务端网络连接:

SocketServer主要关注网络层的通信协议,具体的业务处理逻辑则交给KafkaRequestHandler和KafkaApis来完成。SocketSe rver和这两个组件一起完成一次请求处理的具体步骤如下。

(1)客户端发送的请求被接收器( Acceptor)转发给处理器( processor)处理。
(2)处理器将请求放到请求通道( RequestChannel )的全局请求队列中。
(3)   KafkaRequestHandler取出请求通道中的客户端请求。
(4)调用KafkaApis 进行业务逻辑处理。
(5)   KafkaApis将响应结果发送给请求通道中与处理器对应的响应队列。
(6)处理器从对应的响应队列中取出响应结果。
(7)处理器将响应结果返回给客户端,客户端请求处理完毕。

kafka 连接服务端 kafka服务端与客户端_消息队列_06

最后一图,kafka交互:

kafka 连接服务端 kafka服务端与客户端_kafka_07