Kafka 是一个高吞吐量的分布式的发布订阅消息系统,在全世界都很流行,在大数据项目里面使用尤为频繁。笔者看过多个大数据开源产品的源码,感受 Kafka 的源码是其中质量比较上乘的一个,这得益于做者高超的编码水平和高超的架构设计能力。数据库
Kafka 的核心源码分为两部分:客户端源码和服务端源码,客户端又分为生产者和消费者,而我的认为 Kafka 的源码里面生产者的源码技术含量最高,因此今天给你们剖析 Kafka 的生产者的架构设计,Kafka 是一个飞速发展的消息系统,其架构也在一直演进中,咱们今天分析的 Kafka 的版本是比较成熟稳定的 Kafka1.0.0 版本源码。
图1 Kafka核心模块
生产者流程概述
先给你们介绍一下生产者的大概的运行的流程。
图2 Kafka运行方式
如上图所示:步骤一:一条消息过来首先会被封装成为一个 ProducerRecord 对象。
步骤二:接下来要对这个对象进行序列化,由于 Kafka 的消息须要从客户端传到服务端,涉及到网络传输,因此须要实现序列。Kafka 提供了默认的序列化机制,也支持自定义序列化(这种设计也值得咱们积累,提升项目的扩展性)。
步骤三:消息序列化完了之后,对消息要进行分区,分区的时候须要获取集群的元数据。分区的这个过程很关键,由于这个时候就决定了,咱们的这条消息会被发送到 Kafka 服务端到哪一个主题的哪一个分区了。
步骤四:分好区的消息不是直接被发送到服务端,而是放入了生产者的一个缓存里面。在这个缓存里面,多条消息会被封装成为一个批次(batch),默认一个批次的大小是 16K。
步骤五:Sender 线程启动之后会从缓存里面去获取能够发送的批次。
步骤六:Sender 线程把一个一个批次发送到服务端。你们要注意这个设计,在 Kafka0.8 版本之前,Kafka 生产者的设计是来一条数据,就往服务端发送一条数据,频繁的发生网络请求,结果性能不好。后面的版本再次架构演进的时候把这儿改为了批处理的方式,性能指数级的提高,这个设计值得咱们积累。
生产者细节深度剖析
接下来咱们生产者这儿技术含量比较高的一个地方,前面概述那儿咱们看到,一个消息被分区之后,消息就会被放到一个缓存里面,咱们看一下里面具体的细节。默认缓存块的大小是 32M,这个缓存块里面有一个重要的数据结构:batches,这个数据结构是 key-value 的结果,key 就是消息主题的分区,value 是一个队列,里面存的是发送到对应分区的批次,Sender 线程就是把这些批次发送到服务端。
图3 生产者架构
01 / 生产者高级设计之自定义数据结构
生产者把批次信息用 batches 这个对象进行存储。若是是你们,你们会考虑用什么数据结构去存储批次信息?
Kafka 这儿采起的方式是自定义了一个数据结构:CopyOnWriteMap。熟悉 Java 的同窗都知道,JUC 下面是有一个 CopyOnWriteArrayList 的数据结构的,可是没有 CopyOnWriteMap,我这儿给你们解释一下 Kafka 为何要设计这样的一个数据结构。
1.他们存储的信息的是 key-value 的结构,key 是分区,value 是要存到这个分区的对应批次(批次可能有多个,因此用的是队列),故由于是 key-value 的数据结构,因此锁定用 Map 数据结构。
2.这个 Kafka 生产者面临的是一个高并发的场景,大量的消息会涌入这个这个数据结构,因此这个数据结构须要保证线程安全,这样咱们就不能使用 HashMap 这样的数据结构了。
3.这个数据结构须要支持的是读多写少的场景。读可能是由于每条消息过来都会根据 key 读取 value 的信息,假若有 1000 万条消息,那么就会读取 batches 对象 1000 万次。写少是由于,好比咱们生产者发送数据须要往一个主题里面去发送数据,假设这个主题有 50 个分区,那么这个 batches 里面就须要写 50 个 key-value 数据就能够了(你们要搞清楚咱们虽然要写 1000 万条数据,可是这 1000 万条是写入 queue 队列的 batch 里的,并非直接写入 batches,因此就咱们刚刚说的这个场景,batches 里只须要最多写 50 条数据就能够了)。
根据第二和第三个场景咱们总结出来,Kafka 这儿须要一个能保证线程安全的,支持读多写少的 Map 数据结构。可是 Java 里面并无提供出来的这样的一个数据,惟一跟这个需求比较接近的是 CopyOnWriteArrayList,可是恰恰它又不是 Map 结构,因此 Kafka 这儿模仿 CopyOnWriteArrayList 设计了 CopyOnWriteMap。采用了读写分离的思想解决了线程安全且支持读多写少等问题。
高效的数据结构保证了生产者的性能。(CopyOnWriteArrayList 不熟悉的同窗,能够尝试百度学习)。这儿笔者建议你们能够去看看 Kafka 生产者往 batches 里插入数据的源码,生产者为了保证插入数据的高性能,采用了多线程,又为了线程安全,使用了分段加锁等多种手段,源码很是精彩。
02 / 生产者高级设计以内存池设计
刚刚咱们看到 batches 里面存储的是批次,批次默认的大小是 16K,整个缓存的大小是 32M,生产者每封装一个批次都须要去申请内存,正常状况下若是一个批次发送出去了之后,那么这 16K 的内存就等着 GC 来回收了。可是若是是这样的话,就可能会频繁的引起 FullGC,故而影响生产者的性能,因此在缓存里面设计了一个内存池(相似于咱们平时用的数据库的链接池),一个 16K 的内存用完了之后,把数据清空,放入到内存池里,下个批次用的时候直接从里面获取就能够。这样大大的减小了 GC 的频率,保证了生产者的稳定和高效(Java 的 GC 问题是一个头疼的问题,因此这种设计也很是值得咱们去积累)。
结尾
Kafka 的设计之中精彩的地方有不少,今天咱们截取了一部分跟你们分享。以前我看到过 Kafka 的源码之后,就想之后若是我要去当老师,去培养架构师的话,那么我必定得跟学生分享 Kafka 的源码,经过学习 Kafka 源码提高系统架构能力,再次建议你们有空能够研究研究 Kafka 的源码,你们加油!!
领取更多有关架构知识及其视频缓存