pagecache kafka

聊聊page cache与Kafka之间的事儿

PageCache

PageCache,它就是一个非常典型的读写缓存。操作系统会利用系统空闲的物理内存来给文件读写做缓存,这个缓存叫做 PageCache。应用程序在写文件的时候,操作系统会先把数据写入到 PageCache 中,数据在成功写到 PageCache 之后,对于用户代码来说,写入就结束了。

然后,操作系统再异步地把数据更新到磁盘的文件中。应用程序在读文件的时候,操作系统也是先尝试从 PageCache 中寻找数据,如果找到就直接返回数据,找不到会触发一个缺页中断,然后操作系统把数据从磁盘文件读取到 PageCache 中,再返回给应用程序。

我们可以看到,在数据写到 PageCache 中后,它并不是同时就写到磁盘上了,这中间是有一个延迟的。操作系统可以保证,即使是应用程序意外退出了,操作系统也会把这部分数据同步到磁盘上。但是,如果服务器突然掉电了,这部分数据就丢失了。

你需要知道,**读写缓存的这种设计,它天然就是不可靠的,是一种牺牲数据一致性换取性能的设计。**当然,应用程序可以调用 sync 等系统调用,强制操作系统立即把缓存数据同步到磁盘文件中去,但是这个同步的过程是很慢的,也就失去了缓存的意义。

Kafka对page cache的利用

Kafka为什么不自己管理缓存,而非要用page cache?原因有如下三点:

  • JVM中一切皆对象,数据的对象存储会带来所谓object overhead,浪费空间;
  • 如果由JVM来管理缓存,会受到GC的影响,并且过大的堆也会拖累GC的效率,降低吞吐量;
  • 一旦程序崩溃,自己管理的缓存数据会全部丢失。
    Kafka三大件(broker、producer、consumer)与page cache的关系可以用下面的简图来表示。
    在这里插入图片描述

producer生产消息时,会使用pwrite()系统调用【对应到Java NIO中是FileChannel.write() API】按偏移量写入数据,并且都会先写入page cache里。consumer消费消息时,会使用sendfile()系统调用【对应FileChannel.transferTo() API】,零拷贝地将数据从page cache传输到broker的Socket buffer,再通过网络传输。

图中没有画出来的还有leader与follower之间的同步,这与consumer是同理的:只要follower处在ISR中,就也能够通过零拷贝机制将数据从leader所在的broker page cache传输到follower所在的broker。关于零拷贝的解释可以参考我之前写的这篇文章。

同时,page cache中的数据会随着内核中flusher线程的调度以及对sync()/fsync()的调用写回到磁盘(sync、fsync
都是linux 同步IO方法),就算进程崩溃,也不用担心数据丢失。另外,如果consumer要消费的消息不在page cache里,才会去磁盘读取,并且会顺便预读出一些相邻的块放入page cache,以方便下一次读取。

由此我们可以得出重要的结论:**如果Kafka producer的生产速率与consumer的消费速率相差不大,那么就能几乎只靠对broker page cache的读写完成整个生产-消费过程,磁盘访问非常少。这个结论俗称为“读写空中接力”。**并且Kafka持久化消息到各个topic的partition文件时,是只追加的顺序写,充分利用了磁盘顺序访问快的特性,效率高。

kafka如何充分利用pageChe

另外,写缓存的实现是非常复杂的。应用程序不停地更新 PageCache 中的数据,操作系统需要记录哪些数据有变化,同时还要在另外一个线程中,把缓存中变化的数据更新到磁盘文件中。在提供并发读写的同时来异步更新数据,这个过程中要保证数据的一致性,并且有非常好的性能,实现这些真不是一件容易的事儿。

所以说,一般情况下,不推荐你来使用读写缓存。
那为什么 Kafka 可以使用 PageCache 来提升它的性能呢?这是由消息队列的一些特点决定的。

读写比例:
首先,消息队列它的读写比例大致是 1:1,因为,大部分我们用消息队列都是一收一发这样使用。这种读写比例,只读缓存既无法给写加速,读的加速效果也有限,并不能提升多少性能。

多副本保证数据丢失:
另外,Kafka 它并不是只靠磁盘来保证数据的可靠性,它更依赖的是,在不同节点上的多副本来解决数据可靠性问题,这样即使某个服务器掉电丢失一部分文件内容,它也可以从其他节点上找到正确的数据,不会丢消息。

而且,PageCache 这个读写缓存是操作系统实现的,Kafka 只要按照正确的姿势来使用就好了,不涉及到实现复杂度的问题。所以,Kafka 其实在设计上,充分利用了 PageCache 这种读写缓存的优势,并且规避了 PageCache 的一些劣势,达到了一个非常好的效果。

和 Kafka 一样,大部分其他的消息队列,同样也会采用读写缓存来加速消息写入的过程,只是实现的方式都不一样。

读多写少才适合使用只读缓存:
不同于消息队列,我们开发的大部分业务类应用程序,读写比都是严重不均衡的,一般读的数据的频次会都会远高于写数据的频次。从经验值来看,读次数一般都是写次数的几倍到几十倍。这种情况下,使用只读缓存来加速系统才是非常明智的选择。

注意事项与相关参数

对于单纯运行Kafka的集群而言,首先要注意的就是为Kafka设置合适(不那么大)的JVM堆大小。从上面的分析可知,Kafka的性能与堆内存关系并不大,而对page cache需求巨大。根据经验值,为Kafka分配6~8GB的堆内存就已经足足够用了,将剩下的系统内存都作为page cache空间,可以最大化I/O效率。

另一个需要特别注意的问题是lagging consumer,即那些消费速率慢、明显落后的consumer。它们要读取的数据有较大概率不在broker page cache中,因此会增加很多不必要的读盘操作。比这更坏的是,lagging consumer读取的“冷”数据仍然会进入page cache,污染了多数正常consumer要读取的“热”数据,连带着正常consumer的性能变差。在生产环境中,这个问题尤为重要。

前面已经说过,page cache中的数据会随着内核中flusher线程的调度写回磁盘。与它相关的有以下4个参数,必要时可以调整。

/proc/sys/vm/dirty_writeback_centisecs:flush检查的周期。单位为0.01秒,默认值500,即5秒。每次检查都会按照以下三个参数控制的逻辑来处理。
/proc/sys/vm/dirty_expire_centisecs:如果page cache中的页被标记为dirty的时间超过了这个值,就会被直接刷到磁盘。单位为0.01秒。默认值3000,即半分钟。
/proc/sys/vm/dirty_background_ratio:如果dirty page的总大小占空闲内存量的比例超过了该值,就会在后台调度flusher线程异步写磁盘,不会阻塞当前的write()操作。默认值为10%。
/proc/sys/vm/dirty_ratio:如果dirty page的总大小占总内存量的比例超过了该值,就会阻塞所有进程的write()操作,并且强制每个进程将自己的文件写入磁盘。默认值为20%。
由此可见,调整空间比较灵活的是参数2、3,而尽量不要达到参数4的阈值,代价太大了。

/proc 文件系统是 Linux 的优秀特性之一,修改/proc不需要重启计算机
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/yangshengwei230612/article/details/112463026
今日推荐