kafka原理剖析(2)-producer元数据的获取

522 阅读4分钟

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而已,第一次发的时候元数据是不知道的。

kafka-send 得到元数据过程.png

先看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都管理个超时时间,计算剩下的时间,一旦超过,就报超时。

8A9AD2D4-1546-4489-97EF-A59B877300E0.png

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);这里是封装一个拉取元数据的请求,

A34313A0-594D-4AB7-899E-E8C5555E917A.png 一般情况只针对我们发送的topic拉取元数据信息,封装一个clientRequest,调用dosend方法,目的是把这个元数据请求放入inFlightRequests队列,并加入到kafka自己封装的Selectable的kafkaChannel的发送对象中,kafkaChannel一次只会发一个请求,这个组件在服务端也会用到。顺理成章,请求放入kafkachannel,那么后面肯定有java的channel进行下一步发送。

C8B67A99-FFEC-441C-ACB8-3CB529F6C4B8.png

doSend追下去,还有个重要的部分,把对应的connect关注op_write事件,:

A8781342-2BE6-49E1-AAE0-15E4B02D1B23.png

(4) this.client.poll(pollTimeout, now) -> this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs)); -> pollSelectKeys kafka自己封装的selector 直接处理了各种场景,通过区分selectkey关注的事件,处理不同场景,在这里先看连接场景:

B198767F-17B9-4F62-9401-4FACD52A1237.png 通过finneshConnect,等待连接建立完成(因为上一步发起连接是非阻塞的没结果,要用连接就得等着完成),同时通过底层组件TransportLayer把selectkey取消connect事件,增加op_read事件, 因为第三步,添加了op_write事件,所以这里连接完成后,也会进入后面的write分支,把刚才封装好的元数据send请求,通过底层cannel发出去,并且记录在completedSends,说明发送成功。

0AB60EC3-40AD-4789-A2D8-58F8C71A448F.png

(5)理想情况下,过一段时间,服务器返回response了,那么理应走到op_read的逻辑,读取response数据放入stageReceives队列里,

E8813441-596C-449E-9E5F-6282C53D2DA0.png

(6)回到NetworkClient的核心poll,第一个maybeupdate封装了元数据请求request,poll负责发送和接受, 后面会对请求和响应进行下一步处理,这里只关心元数据,可以先只看 handleCompletedSends -> handleResponse -> this.metadata.update(cluster,now) , 注意这里是集群信息更新,最重要的是versioin+1,然后就可以回到producer的send过程里awaiteUpdate的地方,因为在一遍一遍的等新版本,这里新版本来了就可以往下真正发消息了。得到元数据的过程,本来也是一个发请求和接受请求的过程,这个路和发消息的过程是一致的,是对nio的封装和多层抽象,网络组件和业务组件分离,通过一些中间队列通信,值得思考学习。

4D3AFE5F-97BC-4163-8BE8-9B957F5C0A88.png