apache kafka的所有通信都是基于TCP的,而不是HTTP或者其他协议。为什么?最主要的原因在于TCP和HTTP之间的区别,从社区的角度,开发客户端时,人们能够利用TCP本身提供的一些高级功能,比如多路复用请求以及同时轮询多个连接的能力。多路复用,即将两个或多个数据流合并到一个底层的物理连接中的过程,其实严格来说,TCP并不能多路复用,只是提供可靠的消息交付语义保证,比如自动重传丢失的报文。更严谨地说,作为一个基于报文的协议,TCP能够被用于多路复用连接场景的前提,是上层应用允许发送多条消息。另外一个原因,目前已知的HTTP库在很多编程语言中比较简陋。
- 生产者如何管理TCP连接
Java生产者API主要的对象就是KafkaProducer。通常开发一个生产者分4步:
构造生产者对象所需的参数对象;
创建KafkaProducer对象实例;
使用该对象的send方法发送消息;
调用close方法关闭生产者并释放各种系统资源。
问题来了,生产者会向kafka集群中指定主题发送消息,必然会涉及与broker创建TCP连接。接下来看看producer客户端如何管理TCP连接。
何时创建:首先,生产者应用在创建KafkaProducer实例是会建立与broker的TCP连接。也就是说,生产者应用会在后台创建并启动一个名为Sender的线程,该线程开始运行时首先会创建于broker的连接。在调用send方法之前,并不知道给那个主题发送消息,更不知道与哪个broker建立连接,所以会连接bootstrap.servers参数指定的所有的broker 。这个参数是producer端核心参数之一,指定了producer启动时要连接的broker地址。如果指定了1000个broker连接信息,就会创建与这1000个broker的TCP连接。一般只需指定3~4台即可。因为一旦producer连接上集群中任意一台broker,就能拿到整个集群broker信息。
在上面的日志输出中,可以发现,在KafkaProducer实例被创建后以及消息被发送前,producer应用就开始与两台broker创建连接。这里在测试环境中,为bootstrap.servers配置了localhost:9092、localhost:9093来模拟不同broker。日志最后一行标明了,producer向某一台broker发送了METADATA请求,尝试获取集群元数据。
讲到这里,我们都知道了,在创建kafkaproducer实例的时候是会建立TCP连接的。不过,TCP连接还可能在两个地方被创建:一个是更新元数据,一个是消息发送时。也就是说,当更新元数据或者消息发送时,发现与某broker当前没有连接,就会创建一个连接。
更新元数据的两个场景:当Producer尝试给一个不存在的主题发送消息时,broker会告诉说这个主体不存在。此时,producer会发送METADATA请求给kafka集群,尝试获取最新的元数据信息;producer通过metadata.max.age.ms参数定期去更新元数据信息。该参数默认值300000,即5分钟。这两个场景都是,一个producer会默认与集群所有broker建立连接,不管是否真的需要传输请求,再加上kafka还支持强制关闭空闲的TCP连接资源,所以这里应该是有优化空间的。试想一下,在一个有着1000台broker集群中,你的producer可能只会与其中3~5台长期通信,但producer会长期与这1000台broker建立连接,一段时间后,大约有995个TCP连接被关闭,这难道不是一种资源浪费?
何时关闭:一种是用户主动关闭;一种是kafka自动关闭。主动关闭,是广义的主动关闭,甚至包括kill -9主动杀掉producer进程,不过还是推荐使用close方法来关闭。第二种是kafka帮你关闭连接,这与Producer端参数connections.max.idle.ms值有关。默认情况是9分钟,即如果在9分钟内,没有任何请求流过某个TCP连接,那么kafka会在broker端自动关闭TCP连接。用户可以再producer端设置connections.max.idle.ms=-1禁掉这种机制,这只是软件层面的“长连接”机制,由于kafka创建的socket连接开启了keepalive,因此keepalive探活机制还是会遵守的。值得注意的是,第二种方式中,TCP连接是在broker端被关闭,但这个链接发起端是客户端,因此在TCP看来,这属于被动关闭场景,即passive close。被动关闭后就会产生大量CLOSE_WAIT连接,因此client端没有机会显示观测到连接中断。
- 消费者如何管理TCP连接
何时创建:消费者端的程序入口是KafkaConsumer类。和生产者不同的是,构建KafkaConsumer实例的时候不会创建任何连接,主要原因是KafkaProducer构建实例的时候,在后台启动了sender线程,这个线程负责socket连接的创建。从这一点来看,个人觉得KafkaConsumer 的设计要比 KafkaProducer要好。在消费者端,TCP连接是在调用KafkaConsumer.poll方法是创建的。再细粒度的说,再poll方法内部有3个时机可以创键TCP连接。
1. 发起端FindCoordinator请求时
在broker端有个协调者(coordinator)组件驻留在broker端内存中,负责消费者组的组成员管理和各个消费者的位移提交管理。当程序首次调用poll方法时,需要向kafka集群发送一个名为FindCoordinator请求,希望集群告诉它哪个broker是管理它的协调者。不过,消费者应该向哪个broker发送这个请求,理论上可以是集群的任意一台broker。这里,社区做了一点优化:消费者程序会向集群中当前负载最小的那台broker发送请求。负载最小,也就是看与消费者连接的broker中,谁的待发送请求最少,但是这只是消费者端的单向评估,并非是全局角度,所以不一定是最优解。注意,如果是刚开始启动的时候,消费者只能随机选择broker。
2. 连接协调者时
broker处理完上一步发送的FindCoordinator请求之后,会返回响应结果,显示地告诉消费者哪个broker是真正的协调者,然后消费者就会创建与该broker的socket连接。只有成功连入协调者,协调者才能开启正常的组协调操作,比如加入组、等待组分配方案、心跳请求处理、位移获取、位移提交等。
3. 消费数据时
消费者会为每个要消费的分区创建与该分区leader副本所在broker的TCP连接。假设消费者要消费5个分区的数据,这5个分区的leader副本分布在4台broker上,那么消费者在消费时会与这4台broker建立TCP连接。
创建多少个TCP连接:
看上面这段日志,第一个日志段,消费者程序创建第一个TCP连接,用于发送FindCoordinator请求。因为是第一个连接,消费者对kafka集群一无所知,因此broker节点的ID 是-1,表示消费者不知道要连接的broker的任何信息。第二个日志段,消费者复用了刚刚建立的连接,发送元数据请求,获取整个集群信息。第三个日志段,消费者开始发送FindCoordinator请求,在十几ms之后,消费者程序成功获悉协调者所在的broker信息,也就是第四个日志段的中橙色的"node_id=2"。
完成这些之后,消费者就知道协调者broker的连接信息,因此在第五个日志段发起第二个socket连接,创建连向localhost:9094的TCP连接。只有连接协调者,消费者进程才能正常开启消费者组的各种功能以及后续的消息消费。这里注意一下,id为什么是2147483645,它是由Java的Interger.MAX_VALUE减去协调者所在broker的真实id, 即2147483645 - 2 得来的。这点是kafka社区特意为之,目的就是为了让组协调请求和真正的数据获取请求使用不同的socket连接。最后三个日志段,消费者又分别创建了新的TCP连接,即连向要消费分区的leader副本所在的broker,这三个id就是 server.property中配置的broker.id值。
总结一下以上的3类TCP连接:确定协调者和获取集群元数据;连接协调者,令其执行组成员管理;执行实际的消息获取。
何时关闭:消费者关闭socket连接也分主动关闭和kafka主动关闭,主动关闭是指手动调用KafkaConsumer.close()方法,或是执行kill命令。kafka自动关闭同样也是由消费者端参数connection.max.idle.ms控制的。但是如果在程序中循环调用poll方法消费消息,上面提到的所有的请求都会被定期发送到broker,因此socket连接上总是能保证有请求发送,从而实现“长连接”效果。
另外,上面提到的第三类连接成功后,消费者程序会废弃掉第一类TCP连接,之后在定期请求元数据时,会改为使用第三类TCP连接。对于运行过一段时间的消费者程序,后台只会有后两类连接存在。但是可能存在一些问题,目前kafka只使用ID这一个维度数据表征socket连接信息,所以第一类连接是无法重用的,这样在实际使用过程中,当connection.max.idle.ms被设置为-1,即禁用定时关闭时,像第一类连接不会被定期清理,只会成为“僵尸”连接。基于这个原因,社区应该考虑更好的解决方案,比如考虑使用<主机、端口、ID>三元组方式定位socket资源,这样或许能少创建一些连接。
标注:这个系列文章是本人在极客时间专栏---kafka核心技术与实战中的学习笔记
https://time.geekbang.org/column/article/101171