Kafka uses a binary protocol over TCP. The protocol defines all APIs as request response message pairs. All messages are size delimited and are made up of the following primitive types.
Kafka 的所有通信都是基于 TCP 的(kafka协议),而不是基于HTTP 或其他协议。无论是生产者、消费者,还是 Broker之间的通信都是如此。kafka做为一个消息队列,涉及的网络通信主要有两块:
Kafka 生产者向队列「推」消息
Kafka 消费者向队列「拉」消息
要实现上述的网络通信,我们可以使用 HTTP 协议,比如服务端内嵌一个 jetty 容器,通过 servlet 来实现客户端与服务端之间的交互,但是其性能存在问题,无法满足高吞吐量这个需求。要实现高性能的网络通信,我们可以使用更底层的 TCP 或者 UDP 来实现自己的私有协议,而 UDP 协议是不可靠的传输协议,毕竟我们不希望一条消息在投递或者消费途中丢失了,所以 Kafka 选择 TCP 作为服务间通讯的协议。
从社区的角度来看,在开发客户端时,人们能够利用 TCP本身提供的一些高级功能,比如多路复用请求以及同时轮询多个连接的能力。但严格来说TCP 并不能多路复用,作为一个基于报文的协议,TCP 能够被用于多路复用连接场景的前提是,上层的应用协议(比如 HTTP)允许发送多条消息。
用一个生活场景:饭店规模变化,来帮助理解reactor模型
小饭馆:一个人包揽所有的: 迎宾、点菜、做饭、上菜、送客
饭店:多招几个伙计:大家一起做上面的事情;
酒店:进一步分工:搞一个或多个人专门做迎宾
生活场景 | 类比 | reactor |
---|---|---|
饭店伙计 | 线程 | |
迎宾伙计 | 接入连接 | 1.一个人包揽所有 2.多招几个伙计,大家一起做上面的事 3.进一步分工,搞一个或多个人做迎宾 |
点菜 | 请求 | |
做菜 | 业务处理 | |
上菜 | 响应 | |
送客 | 断开连接 |
Reactor是一种开发模式,特别适合应用于处理多个客户端并发向服务器端发送请求的场景,该模式有个请求分发线程 Dispatcher,也就是图中的 Acceptor,它会使用轮询方式将不同的请求下发到多个工作线程中处理。Acceptor 线程只是用于请求分发,不涉及具体的逻辑处理,非常得轻量级,因此有很高的吞吐量表现。
模式的核心流程:
注册感兴趣的时间-》扫描是否有感兴趣的事件发生-》事件发生后做出相应的处理
client/server | SocketChannel/ServerSocketChannel | op_accept | op_connect | op_write | op_read |
---|---|---|---|---|---|
client | SocketChannel | Y | Y | Y | |
server | ServerSocketChannel | Y | |||
server | SocketChannel | Y | Y |
对于进来的每一个请求,一个人包揽所有,也就是一个线程执行读取、编码、计算、解码、发送响应这些所有的工作
一个人干活太累了,把编码、计算、解码这些耗时的累的活分派给专门的团队来处理,也就是把这些复杂操作放到一个专门的线程池里。这里着重讲一下reactor多线程模型的流程
1)Reactor 对象通过 select 监控客户端请求事件,收到事件后,通过 dispatch 进行分发
2)如果是建立连接请求,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 对象处理完成连接后的各种事件
3)如果不是连接请求,则由 Reactor 对象会分发调用连接对应的 Handler 来处理
4)Handler 只负责响应事件,不做具体的业务处理,通过 read 读取数据后,会分发给后面的 Worker 线程池的某个线程处理业务
5)Worker 线程池会分配独立线程完成真正的业务,并将结果返回给 Handler
6)Handler 收到响应后,通过 send 将结果返回给 Client
面对大量请求,把接收请求的acceptor也用一个线程(主reactor)来单独处理,也就是搞一个人来做迎宾,接收到请求后只是负责分发到另一个线程(子reactor),这个子reactor执行的就是上面提到的多线程模型那一套。
kafka所有的请求都是通过TCP网络以Socket的方式进行通讯的,它也有对应的 Acceptor 线程和一个工作线程池,在 Kafka 中,这个工作线程池叫网络线程池。
Broker 端参数 num.network.threads,用于调整该网络线程池的线程数。默认3个,专门处理客户端发送的请求。
# The number of threads that the server uses for receiving requests from the network and sending responses to the network num.network.threads=3
客户端发来的请求会被 Broker 端的 Acceptor 线程分发到任意一个网络线程中,由它们来进行处理。那么,当网络线程接收到请求后,它是如何处理的?
Kafka 在这个环节又做了一层异步线程池的处理。
当网络线程拿到请求后,它不是自己处理,而是将请求放入到一个共享请求队列中。Broker 端还有个 IO 线程池,负责从该队列中取出请求,执行真正的处理。
如果是 PRODUCE 生产请求,则将消息写入到底层的磁盘日志中;
如果是 FETCH 请求,则从磁盘或页缓存中读取消息。
Broker 端参数num.io.threads,用于配置IO线程池中的线程数,默认创建8个IO线程处理请求。
# The number of threads that the server uses for processing requests, which may include disk I/O
num.io.threads=8
请求队列是所有网络线程共享的,而响应队列则是每个网络线程专属的,因为Dispatcher只用于请求的分发而不负责响应回传,因此只能让每个网络线程自己发送Response给客户端。还有一个Purgatory组件,炼狱组件用于它是用来缓存延时请求(Delayed Request)的。所谓延时请求,就是那些一时未满足条件不能立刻处理的请求。
比如设置了 acks=all 的 PRODUCE 请求,一旦设置了 acks=all,那么该请求就必须等待 ISR 中所有副本都接收了消息后才能返回,此时处理该请求的 IO 线程就必须等待其他 Broker 的写入结果。当请求不能立刻处理时,它就会暂存在 Purgatory 中。稍后一旦满足了完成条件,IO 线程会继续处理该请求,并将 Response 放入对应网络线程的响应队列中。