kafka系列2---kafka数据存储原理
1、kafka高性能文件读写设计原理
kafka中的数据是以日志的形式进行存储,所以分区实际上就是日志。
1.1 磁盘读写并不慢
磁盘线性读写的速度比随机读写的速度快6000倍。现代操作系统提供了 read-ahead 和 write-behind 技术,read-ahead 是以大的 data block 为单位预先读取数据,而 write-behind 是将多个小型的逻辑写合并成一次大型的物理磁盘写入。关于该问题的进一步讨论可以参考 ACM Queue article,他们发现实际上顺序磁盘访问在某些情况下比随机内存访问还要快!
为了弥补这种性能差异,现代操作系统在越来越注重使用内存对磁盘进行 cache。现代操作系统主动将所有空闲内存用作 disk caching,代价是在内存回收时性能会有所降低。
此外,Kafka 建立在 JVM 之上,任何了解 Java 内存使用的人都知道两点:
1、对象的内存开销非常高,通常是所存储的数据的两倍(甚至更多)。
2、随着堆中数据的增加,Java 的垃圾回收变得越来越复杂和缓慢。
受这些因素影响,相比于维护 in-memory cache 或者其他结构,使用文件系统和 pagecache 显得更有优势--我们可以通过自动访问所有空闲内存将可用缓存的容量至少翻倍,并且通过存储紧凑的字节结构而不是独立的对象,有望将缓存容量再翻一番。此外,即使服务重新启动,缓存依旧可用
这里给出了一个非常简单的设计:相比于维护尽可能多的 in-memory cache,并且在空间不足的时候匆忙将数据 flush 到文件系统,我们把这个过程倒过来。所有数据一开始就被写入到文件系统的持久化日志中,而不用在 cache 空间不足的时候 flush 到磁盘。实际上,这表明数据被转移到了内核的 pagecache 中。这种 pagecache-centric 的设计风格出现在一篇关于 Varnish 设计的文章中。
1.2 采用常量时间的数据读写算法
传统的持久化数据结构通常是Btree。BTree 的操作复杂度是 O(log N),通常我们认为 O(log N) 基本等同于常数时间,但这条在磁盘操作中不成立。
磁盘寻址是每10ms一跳,并且每个磁盘同时只能执行一次寻址,因此并行性受到了限制。因此即使是少量的磁盘寻址也会很高的开销。由于存储系统将非常快的cache操作和非常慢的物理磁盘操作混合在一起,当数据随着 fixed cache 增加时,可以看到树的性能通常是非线性的——比如数据翻倍时性能下降不只两倍。
直观来看,持久化队列可以建立在简单的读取和向文件后追加两种操作之上,这和日志解决方案相同。这种架构的优点在于所有的操作复杂度都是O(1),而且读操作不会阻塞写操作,读操作之间也不会互相影响。这有着明显的性能优势,由于性能和数据大小完全分离开来——服务器现在可以充分利用大量廉价、低转速的1+TB SATA硬盘。 虽然这些硬盘的寻址性能很差,但他们在大规模读写方面的性能是可以接受的,而且价格是原来的三分之一、容量是原来的三倍。
在不产生任何性能损失的情况下能够访问几乎无限的硬盘空间,这意味着我们可以提供一些其它消息系统不常见的特性。例如:在 Kafka 中,我们可以让消息保留相对较长的一段时间(比如一周),而不是试图在被消费后立即删除。正如我们后面将要提到的,这给消费者带来了很大的灵活性。
1.3 小结
kafka每个分区读是没问题的,写则是在分区的后面直接把数据加进去,这样就能保证读写分离(读写之间互不影响),而且读写的时间复杂度都是O(1)。所以kafka可以把数据直接刷入磁盘,更多信息参考:http://kafka.apachecn.org/documentation.html#appvsosflush
实际上kafka会直接把数据写到操作系统,由操作系统来决定何时把数据刷入磁盘。因为现代操作系统在磁盘操作上做了很多优化,虽然kafka的磁盘操作已经很快,但是也可以藉由操作系统的缓存来进行优化,实现更高的吞吐量
如下图所示:
2、日志过期
Kafka 集群保留所有发布的记录—无论他们是否已被消费—并通过一个可配置的参数——保留期限来控制. 举个例子, 如果保留策略设置为2天,一条记录发布后两天内,可以随时被消费,两天过后这条记录会被抛弃并释放磁盘空间。Kafka的性能和数据大小无关,所以长时间存储数据没有什么问题.
事实上,在每一个消费者中唯一保存的元数据是offset(偏移量)
3、日志压缩
传统的日志会根据设定的时间自动删除。比如你设置日志保留7天删除,则如果你现在在日志中插入数据,7天后这些数据就会被自动从日志中删除,以减少日志大小。
而日志压缩则采取一种不一样的方式,示例如下,下图中的数据是要被删除的日志数据:
这里k1有3个值,offset分别为0,2,3。最新值是offset为3的那个,所以压缩后,就只剩下offset为3的那个,值为v4。最后的压缩结果就是图中箭头所指向的表。
再看一下日志的逻辑结构:
整个日志被cleaner point分为头部和尾部。尾部的数据就是要被压缩的。关于这个图的解释:假如今天插入100条数据,offset是0到99,7天后过期,明天又插入100条数据,offset是100到199,则7天后,cleaner point会指到100,于是0到99就处于尾部,然后会被压缩。
topic使用min.compaction.lag.ms来保障消息写入之前必须经过的最小时间长度,才能被压缩。 这限制了一条消息在Log Head中的最短存在时间。
日志压缩比传统直接删除的做法相比,最大的好处就是可以尽可能多地保存日志数据。
4、配置Log Cleaner
Log Cleaner默认启用。这会启动清理的线程池。如果要开始特定Topic的清理功能,可以开启特定的属性:
- log.cleanup.policy=compact
这个可以通过创建Topic时配置或者之后使用Topic命令实现。
Log Cleaner可以配置保留最小的不压缩的head log。可以通过配置压缩的延迟时间:
- log.cleaner.min.compaction.lag.ms
这可以保证消息在配置的时长内不被压缩。 如果没有设置,除了最后一个日志外,所有的日志都会被压缩。 活动的 segment 是不会被压缩的,即使它保存的消息的滞留时长已经超过了配置的最小压缩时间长。
关于cleaner更详细的配置在 这里:http://kafka.apache.org/documentation.html#brokerconfigs