【Kafka官方文档翻译】5.4.2. 存储
原文地址:https://kafka.apache.org/0101/documentation.html#persistence
不要对文件系统感到恐惧
kafka 严重依赖于底层的文件系统来保存和缓存消息记录。一种普遍的观念是磁盘很慢,大家都会怀疑kafka 的存储结构是否能提供有竞争力的存储性能。实际上磁盘比人们现象中的还快,这就看你怎么用了。一个合理设计的磁盘存储结构,往往可以和网络一样快。
关于磁盘性能的关键事实是,硬盘驱动器的吞吐量在过去十年时,磁道的寻址延迟就已经达到了极限了。使用 jaod 方式配置6个7200rpm SATA RAID-5 组的磁盘阵列大概是 600MB/sec,但是随机写性能只有100k/sec,差距是6000X万倍,线性读写在使用上是最容易预测的方式,所以大部分操作系统都对这方面做了很多优化措施。现在的操作系统,都有提前读和缓存写的技术,从大的数据块中批量读取数据,并汇总小的逻辑写请求后,使用一次大的物理写请求代替。更多关于这方面的研究可以查看 ACM Queue article 这里,它指出这样的一个事实,顺序写在某些情况下比随机的内存读取还要快。
为了弥补这种性能上的差距,现代操作系统更多使用内存来为磁盘做缓存。现代的操作系统更乐意使用所有的空闲内存为磁盘做缓存,在内存的回收上只需要花费极小的代价。所有的磁盘读写都通过统一的缓存。如果没有使用直接 I/O 这个开关,这种特性不会很容易被屏蔽掉。因此,即使一个进程内部独立维持一个数据缓存,那么数据也有可能在系统页中再被缓存一次,所有的数据都会被存储两次。
此外,我们基于 jvm 上面构建应用,有花费时间在 java 内存上的人都知道两件事:
- 1.内存中存有大量的对象需要消耗很高,经常是双倍于存储到磁盘时大小(可能更多)
- 2.java的垃圾收集器在内存数据增加时变得既烦琐又慢
考虑到这些因素,使用文件系统并使用页缓存机制比自己去进行内存缓存或使用其他存储结构更为有效——我们访问内存的时候已经起码至少访问了两次缓存,很有可能在写字节的时候也是两次存储而非单次。这样做的话,在一个缓存达到32GB的机器上,可以减少GC的代价,这样也可以减少代码在维护缓存和系统文件间的一致性,比再尝试新的方法有更高的正确性。如果你对磁盘的使用充分利用到线性读,那么预取机制将会很有效的在每次磁盘读取时实现填充好缓存空间。
这意味设计非常简单,系统不是更多把数据保存到内存空间,在内存空间耗尽时才赶紧写入到文件系统中,相反的,所有的数据都被马上写入到文件系统的日志文件中,但没有必要马上进行 flush 磁盘操作。只是把数据传输到系统内核的页面空间中去了。
这种页面缓存风格设计可以参考这里 : article
常量耗时需求
在消息系统中,大部分持久化的数据结构通常使用一个消费者队列一个 btree 结构,或其他随机读取的数据结构用于维持消息的元数据信息。btree 结构是最通用的数据结构类型,它在消息系统中,能够支持广泛的事物或非事物的语义。虽然 btree 操作的代价是 O(log N),但是实际使用时消耗的代价却很高。通常O(log N) 被认为是消耗常量时间,但是这个对硬盘操作却不是这样,硬盘寻址需要使用10ms的耗时,每次请求只能做一次硬盘寻址,不能并发执行。所以即使少数的几次硬盘寻址也会有很高的负载,因为存储系统混合和快速缓存操作和慢速的物理磁盘操作,btree 树的性能一般逼近与缓存到硬盘里面的数据大小,当数据量加倍时,效率可能下降一半或更慢。
直观地,一个持久化队列可以使用简单的读和追加数据到文件的日志方式进行实现,这种结构有一个好处是,所有操作都是O(1)性能的,而且读和写入数据不会相互阻塞,这样性能和数据的大小完全无关,一台服务器可以完全充分利用了廉价、低速的1+TB SATA 硬盘,虽然它们的寻道性能不高,但是他们以3分之一的价格和3倍的容量接受大量的读写请求。
能够以微小地性能代价存取数据到无限的硬盘中,这意味着我们可以提供一些其他消息系统没有的特性。例如,,在kafka中,不需要在消费者消费了数据后马上把消息从队列中删除掉,相反的我们可以保留一段很长的时间,例如一个礼拜。这对消费者来说提供了很大的灵活性,下面我们就会讲到。