目录
背景
问题
分析并解决
1.控制线程数
2.更换IO组件
3.Linux进程信息文件分析
总结加餐
参考文档
背景
隔壁业务组系统是简单的主从结构,写索引的服务(主)叫primary, 读索引并提供搜索功能的服务(从)叫replica。业务线同步数据并不是平滑的,在同步数据时primary瞬间负载上升,磁盘写IO增加,这是合理的。
primary 写完索引,会马上触发索引的同步操作,因此,瞬间的磁盘读IO增加以及网络流量的增加都是合理的。
问题
在节假日高峰期前期,团队增加了50%的replica服务器,这造成了primary的同步压力进一步增加,观测系统监控,发现了“吃swap”的现象,这就不合理了,需要分析优化。
这里简单解释下“吃swap”:
在计算机系统中,"吃swap"指的是系统开始使用交换空间(swap space)来补充物理内存(RAM)的不足。交换空间是一块预留在磁盘上的区域,用于存储那些当前不常用的内存数据。当物理内存不足时,操作系统会将一些数据从内存中移到交换空间,以腾出内存给其他需要的进程。
具体来说,“吃swap”通常表示以下几种情况:
- 内存不足:系统的物理内存已经被用完,导致操作系统不得不使用交换空间来存储数据。
- 性能下降:因为磁盘的读写速度远低于内存的读写速度,当系统频繁访问交换空间时,性能会显著下降。用户可能会注意到系统变得响应缓慢。
- 高负载场景:在高负载场景下,如大量的索引写入和同步操作,内存需求突然增加,导致系统不得不使用swap。
以上描述的情况下,primary服务器在索引写入和同步操作的高负载下,内存不足,导致操作系统开始频繁使用swap,从而出现“吃swap”的现象。这是不合理的,因为频繁使用swap会显著降低系统性能,影响整体服务的响应速度和稳定性。因此,需要通过优化措施来减少对swap的依赖,提高系统的性能。
分析并解决
1.控制线程数
参考以前的经验,replica 有个索引备份功能,在晚上4点会备份本地索引文件(30G)到指定目录
这个操作就会造成瞬间IO的增加以及负载的升高,因此最初我们判断这是索引复制太快,导致的内存耗尽和负载增加。我们尝试通过控制primary服务线程池的大小,来放慢索引复制的速度。
经过压测发现,线程很小(5个)的情况,载峰值有所降低,但是吃swap的问题仍然存在。
结论:primary采用的gRPC框架,通过控制线程池大小来放慢索引复制的速度在gRPC框架中并不奏效,因为gRPC的异步调用机制允许高并发执行,独立于线程数量。有效的优化策略需要针对异步调用特性和具体负载情况进行调整,而不仅仅是控制线程数。
2.更换IO组件
Lucene提供了两种磁盘访问方式MMapDirectory和NIODirectory,因为mmap方式会大量使用堆外内存,因此我们怀疑是堆外内存失控导致的系统内存不足,经过更换Directory,发现吃swap问题仍然存在。
Apache Lucene 是一个高性能、可伸缩的信息检索库,用于全文搜索和索引。Lucene 提供了多种方式来访问磁盘存储的索引数据,主要包括
MMapDirectory
和NIODirectory
。
结论:mmap虽然会使用堆外内存,但是linux应该是可以控制好堆外内存的回收的。
3.Linux进程信息文件分析
我们分析了linux下的 /proc/$pid/status文件,VmSwap是swap占用情况,VmHWM是历史内存占用。以及java进程当前的内存占用情况 sudo -u tomcat jhsdb jmap --heap --pid $pid
发现java进程的历史内存占用很高,而当前内存占用并不高(所以排除了内存泄漏),各个进程都有占用swap的现象,说明是java进程在某个时刻(文件传输)耗尽了内存,导致其他进程吃swap,而那个时刻过后,java进程是可以回收内存的。所以推测是索引文件传输过程中,某些buffer设置不合理导致的。
经过代码排查 ,发现gRPG(高度异步)在读取索引文件时需要多次判断serverCallStreamObserver.isReady(),如果只判断一次isReady,就把所以数据写入响应流,就会占用大量系统缓冲区。官方推荐的是一种callback机制,示例代码如下:
|
- 缓冲区管理不当:原始代码在判断
serverCallStreamObserver.isReady()
一次后,立即写入所有数据,导致系统缓冲区被大量占用,内存迅速耗尽。- 回调机制:通过设置
serverCallStreamObserver.setOnReadyHandler
回调函数,确保只有在缓冲区准备好时才写入数据。这样可以避免一次性写入过多数据,导致内存占用过高。- 状态管理:使用
upto
变量跟踪传输偏移量,每次isReady
时仅写入一部分数据,缓解系统缓冲区的压力。- 资源释放:通过
setOnCancelHandler
和setOnCloseHandler
确保在取消或关闭时正确关闭输入流,避免资源泄漏。
这种写法会比较反直觉,通过回调函数关闭InputStream看上去没有finally去关闭安全,好处是缓冲区压力小,最终问题解决。
总结加餐
以上描述的问题的根本原因在于 gRPC 在文件传输过程中缓冲区管理不当,导致内存占用过高。通过引入官方推荐的回调机制和合理的状态管理,成功解决了该问题。因此在日常开发中,涉及到文件写入与读取时,还是需要多关注一下流的关闭与打开与缓冲区buffer的使用是否合理。
Linux
将物理内存分为内存段,叫做页面。交换是指内存页面被复制到预先设定好的硬盘空间(叫做交换空间)的过程,目的是释放这份内存页面。物理内存和交换空间的总大小是可用的虚拟内存的总量。我们知道swap space
是磁盘上的一块区域,可以是一个分区,也可以是一个文件,或者以它们的组合方式出现。简单点说,当系统物理内存吃紧时,Linux
系统会将内存中不常访问的数据保存到 swap 上,这样系统就有更多的物理内存为其他进程服务,而当系统需要访问swap
上存储的内容时,系统会再将 swap 上的数据加载到内存中,这就是我们常说的swap out
和swap in
了。那么配置多大的 Swap 比较合适?
- 当物理内存小于
1G
且不需要休眠时,设置和内存同样大小的swap
空间即可;当需要休眠时,建议配置两倍物理内存的大小,但最大值不要超过两倍内存大小。- 当物理内存大于
1G
且不需要休眠时,建议大小为sqrt(RAM)
,其中RAM
为物理内存大小;当需要休眠时,建议大小是RAM+round(sqrt(RAM))
,但最大值不要超过两倍内存大小。- 如果两倍物理内存大小的
swap
空间还不够用,建议增加内存而不是增加swap
。
此外,在一篇参考文献中看到如下这段话:
如果频繁的访问 swap 的话,怎么优化 swap 都没用,跟内存比还是低几个数量级,性能还是下降的厉害,如果不频繁访问 swap 的话,优化 swap 又有啥意义呢?所以其实优化 swap 性能的实际意义不大。
参考文档
https://zhuanlan.zhihu.com/p/467976849