孙玄责编
郭芮来源
架构之美(ID:beautyArch)Kafka是一个高吞吐量的分布式的发布订阅消息系统,在全世界都很流行,在大数据项目里面使用尤其频繁。笔者看过多个大数据开源产品的源码,感觉Kafka的源码是其中质量比较上乘的一个,这得益于作者高超的编码水平和高超的架构设计能力。Kafka的核心源码分为两部分:客户端源码和服务端源码,客户端又分为生产者和消费者,而个人认为Kafka的源码里面生产者的源码技术含量最高,所以今天给大家剖析Kafka的生产者的架构设计,Kafka是一个飞速发展的消息系统,其架构也在一直演进中,我们今天分析的Kafka的版本是比较成熟稳定的Kafka.0.0版本源码。图Kafka核心模块
生产者流程概述
先给大家介绍一下生产者的大概的运行的流程。图2Kafka运行方式如上图所示:步骤一:一条消息过来首先会被封装成为一个ProducerRecord对象。步骤二:接下来要对这个对象进行序列化,因为Kafka的消息需要从客户端传到服务端,涉及到网络传输,所以需要实现序列。Kafka提供了默认的序列化机制,也支持自定义序列化(这种设计也值得我们积累,提高项目的扩展性)。步骤三:消息序列化完了以后,对消息要进行分区,分区的时候需要获取集群的元数据。分区的这个过程很关键,因为这个时候就决定了,我们的这条消息会被发送到Kafka服务端到哪个主题的哪个分区了。步骤四:分好区的消息不是直接被发送到服务端,而是放入了生产者的一个缓存里面。在这个缓存里面,多条消息会被封装成为一个批次(batch),默认一个批次的大小是6K。步骤五:Sender线程启动以后会从缓存里面去获取可以发送的批次。步骤六:Sender线程把一个一个批次发送到服务端。大家要注意这个设计,在Kafka0.8版本以前,Kafka生产者的设计是来一条数据,就往服务端发送一条数据,频繁的发生网络请求,结果性能很差。后面的版本再次架构演进的时候把这儿改成了批处理的方式,性能指数级的提升,这个设计值得我们积累。生产者细节深度剖析接下来我们生产者这儿技术含量比较高的一个地方,前面概述那儿我们看到,一个消息被分区以后,消息就会被放到一个缓存里面,我们看一下里面具体的细节。默认缓存块的大小是32M,这个缓存块里面有一个重要的数据结构:batches,这个数据结构是key-value的结果,key就是消息主题的分区,value是一个队列,里面存的是发送到对应分区的批次,Sender线程就是把这些批次发送到服务端。图3生产者架构0生产者高级设计之自定义数据结构生产者把批次信息用batches这个对象进行存储。如果是大家,大家会考虑用什么数据结构去存储批次信息?Kafka这儿采取的方式是自定义了一个数据结构:CopyOnWriteMap。熟悉Java的同学都知道,JUC下面是有一个CopyOnWriteArrayList的数据结构的,但是没有CopyOnWriteMap,我这儿给大家解释一下Kafka为什么要设计这样的一个数据结构。他们存储的信息的是key-value的结构,key是分区,value是要存到这个分区的对应批次(批次可能有多个,所以用的是队列),故因为是key-value的数据结构,所以锁定用Map数据结构。
2
这个Kafka生产者面临的是一个高并发的场景,大量的消息会涌入这个这个数据结构,所以这个数据结构需要保证线程安全,这样我们就不能使用HashMap这样的数据结构了。
3
这个数据结构需要支持的是读多写少的场景。读多是因为每条消息过来都会根据key读取value的信息,假如有万条消息,那么就会读取batches对象万次。写少是因为,比如我们生产者发送数据需要往一个主题里面去发送数据,假设这个主题有50个分区,那么这个batches里面就需要写50个key-value数据就可以了(大家要搞清楚我们虽然要写万条数据,但是这万条是写入queue队列的batch里的,并不是直接写入batches,所以就我们刚刚说的这个场景,batches里只需要最多写50条数据就可以了)。
根据第二和第三个场景我们总结出来,Kafka这儿需要一个能保证线程安全的,支持读多写少的Map数据结构。但是Java里面并没有提供出来的这样的一个数据,唯一跟这个需求比较接近的是CopyOnWriteArrayList,但是偏偏它又不是Map结构,所以Kafka这儿模仿CopyOnWriteArrayList设计了CopyOnWriteMap。采用了读写分离的思想解决了线程安全且支持读多写少等问题。高效的数据结构保证了生产者的性能。(CopyOnWriteArrayList不熟悉的同学,可以尝试百度学习)。这儿笔者建议大家可以去看看Kafka生产者往batches里插入数据的源码,生产者为了保证插入数据的高性能,采用了多线程,又为了线程安全,使用了分段加锁等多种手段,源码非常精彩。02生产者高级设计之内存池设计刚刚我们看到batches里面存储的是批次,批次默认的大小是6K,整个缓存的大小是32M,生产者每封装一个批次都需要去申请内存,正常情况下如果一个批次发送出去了以后,那么这6K的内存就等着GC来回收了。但是如果是这样的话,就可能会频繁的引发FullGC,故而影响生产者的性能,所以在缓存里面设计了一个内存池(类似于我们平时用的数据库的连接池),一个6K的内存用完了以后,把数据清空,放入到内存池里,下个批次用的时候直接从里面获取就可以。这样大大的减少了GC的频率,保证了生产者的稳定和高效(Java的GC问题是一个头疼的问题,所以这种设计也非常值得我们去积累)。结尾Kafka的设计之中精彩的地方有很多,今天我们截取了一部分跟大家分享。之前我看到过Kafka的源码以后,就想以后如果我要去当老师,去培养架构师的话,那么我一定得跟学生分享Kafka的源码,通过学习Kafka源码提升系统架构能力,再次建议大家有空可以研究研究Kafka的源码,大家加油!!推荐阅读?钉钉跃居AppStore榜首背后,全民云办公时代来临??远程办公4大坑,坑坑“致命”!?连登GitHubTOP榜,中国开发者在行动!?揭秘阿里、腾讯、字节跳动在家办公的区别?深度好文!新浪微博架构师详析微博云原生技术的思考与实践?年区块链和分布式账本技术的5大趋势你点的每一个在看,我认真当成了喜欢预览时标签不可点收录于话题#个上一篇下一篇