1 整体流程
(1)自定义消息拦截器,一般没啥用。
(2)同步等待,`拉取元数据`。第一次发topic需要拉元数据,是懒加载思想。拉取的是cluster的信息。cluster包含了集群topic-broker-partition等信息。
(3)对topic和key和value进行序列化,转化成byte[]数组
(4)根据Partitioner对key和value计算,得到要发送到哪个分区
(5)判断消息大小,不能大于单条请求限制大小和缓冲区大小。
(6)绑定消息回调函数和拦截器回调函数
(7)`发送消息到Accumulator`
(8)欢迎sender线程。如果batchisfull代表一个批次已满,或者有了新批次,都代表有批次可发,都会唤醒sender线程。
2 元数据的获取
发消息的时候,producer只知道topic而已,第一次发的时候元数据是不知道的。
先看producer的send方法
(1) 首先要获取topic的元数据,真正dosend的第一步waitOnMetadata,进行阻塞
,直到获得元数据。
ClusterAndWaitTime clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
(2)把需要发送的topic记录在metadata的topic的map中。
(3)先从metadata里面拿到集群信息,如果第一次发这个topic信息,那么拿不到元数据。后面再来就能拿到直接返回了。
(4)把metadata的needupdate设置为true,并且记录下当前的元数据version,为以后对比用。
(5)唤醒sender线程,然后自己就awaite阻塞了。awaitUpdate(final int lastVersion, final long maxWaitMs)
await过程比较简单,就是个while循环,根据配置的超时时间,计算出还剩的时间 ,然后wait等待,要么中间sender线程唤醒,要么到时间自己醒过来,然后看看版本更新没,更新了说明数据拉到了,没更新。
注意,整个过程里,producer都管理个超时时间,计算剩下的时间,一旦超过,就报超时。
producer的send等待了,sender线程在干什么,怎么唤醒producer的send线程呢?看看sender线程:
(1)sender本身也是个线程,在kafkaProducer启动的时候一起启动起来,里面是个while死循环跑run方法。
(2)this.client.ready ,检查和broker是否建立好连接,没建立就发起连接。
检查连接:connectionStates.canConnect(node.idString(), now))
发起连接:initiateConnect(Node node, long now)
一些关键参数:
//连接非阻塞
socketChannel.configureBlocking(false);
// keepalive 2小时自动探活,
socket.setKeepAlive(true);
//关闭nagle算法,不组合小数据包发送,降低延迟
socket.setTcpNoDelay(true);
由于建立连接是非阻塞
的,这里发起连接直接就走,后面有地方等待连接完成。
(3)由于启动连接都没有,中间很多过程可以省略,直接看sender的run的最后,
this.client.poll(pollTimeout, now) -> metadataUpdater.maybeUpdate(now);这里是封装一个拉取元数据的请求,
一般情况只针对我们发送的topic拉取元数据信息,封装一个clientRequest,调用dosend方法,目的是把这个元数据请求放入inFlightRequests队列,并加入到kafka自己封装的Selectable的kafkaChannel的发送对象中,kafkaChannel一次只会发一个请求,这个组件在服务端也会用到。顺理成章,请求放入kafkachannel,那么后面肯定有java的channel进行下一步发送。
doSend追下去,还有个重要的部分,把对应的connect关注op_write事件
,:
(4)this.client.poll(pollTimeout, now)
-> this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs));
-> pollSelectKeys
kafka自己封装的selector 直接处理了各种场景,通过区分selectkey关注的事件,处理不同场景,在这里先看连接场景:
通过finneshConnect,等待连接建立完成(因为上一步发起连接是非阻塞的没结果,要用连接就得等着完成),同时通过底层组件TransportLayer把selectkey取消connect事件,增加op_read事件
,
因为第三步,添加了op_write事件,所以这里连接完成后,也会进入后面的write分支,把刚才封装好的元数据send请求,通过底层cannel发出去,并且记录在completedSends,说明发送成功。
(5)理想情况下,过一段时间,服务器返回response了,那么理应走到op_read的逻辑,读取response数据放入stageReceives队列里,
(6)回到NetworkClient的核心poll,第一个maybeupdate封装了元数据请求request,poll负责发送和接受,
后面会对请求和响应进行下一步处理,这里只关心元数据,可以先只看
handleCompletedSends -> handleResponse -> this.metadata.update(cluster,now) ,
注意这里是集群信息更新,最重要的是versioin+1
,然后就可以回到producer的send过程里awaiteUpdate的地方,因为在一遍一遍的等新版本,这里新版本来了就可以往下真正发消息了。得到元数据的过程,本来也是一个发请求和接受请求的过程,这个路和发消息的过程是一致的,是对nio的封装和多层抽象,网络组件和业务组件分离,通过一些中间队列通信,值得思考学习。