近几年开发了一些大型的应用程序在程序性能调优或者解决一些疑难杂症问题的过程中遇到最多的还是与内存相关的一些问题。例如glibc内存分配器ptmallocgoogle的内存分配器tcmalloc都存在“内存泄漏”即内存不归还操作系统的问题ptmalloc内存分配性能低下的问题随着系统长时间运行buffer/cache被某些应用大量使用几乎完整占用系统内存导致其他应用程序内存申请失败等等问题。之所以内存相关的问题层出不穷关键还是它的地位太重要了。这次还是与内存相关分享的是追踪buffer/cache占用的内存到底被谁哪些应用程序偷吃了有关buffer/cache的文章大多提及的是如何释放并归还到系统的方法但是分析buffer/cache内存消耗背后原因的相关文章却凤毛麟角。buffer/cache为什么会增长它到底被哪些程序使用了我相信这也是很多同行的疑惑因此想通过本篇文章分享一些buffer/cache内存消耗问题的跟踪方法为类似问题的优化和解决提供一些参考。二、问题描述如下图1和图2所示buffer/cache已经占用了46GB的内存达到了整个系统内存的37%这个占比已经非常高了。buffer/cache长期占用不释放同时供系统上其它进程使用的可用内存几乎快没了。图1图2长此以往会出现什么问题呢最直接的问题就是其他进程没法玩了比如大一点的内存块就无法申请。之前分享过一次相关问题的定位参见链接记一次进程阻塞诊断 - T-BARBARIANS - 博客园我在这篇文章里也详细介绍了buffer/cache的释放方法解决了当时的燃眉之急。为啥最近又开始与buffer/cache纠缠上了呢“echo 1 /proc/sys/vm/drop_caches”释放的是所有cache这些cache是当前系统上所有程序在运行过程中加载到内存的一些文件信息这些信息被当做缓存用好处是CPU下次读取某个文件时就会比第一次从磁盘读取快多了。drop_caches执行时会清空所有cache这样会带来一个问题当某些程序需要读取之前加载到cache的信息时就需要重新从磁盘读取这就会产生IO等待或者IO竞争从而拖累程序性能。在某些平台上我们已经发现有高性能程序因为cache的粗暴清空产生了性能抖动。因此我们就没法像以前一样回避buffer/cache到底被谁使用的问题并且直接粗暴释放的策略在某些平台上也就失效了。根据上面的描述我们当前面临的问题就是究竟是谁占用了buffer/cache以及弄清是谁占用后是否可以规避它对buffer/cache的大量使用。面对这个问题老板最近又上火了。三、buffer/cache使用跟踪开始介绍一下调查buffer/cache占用的跟踪思路吧。1、hcache网上有一些帖子分享了hcache可以查看哪些文件使用了cache那hache真的可以帮助我们对buffer/cache进行全面调查吗我们一起来看看。根据前面的问题描述当前buffer/cache已经占用了46GB的内存。先使用hcache查看一下top100的cache占用如下图所示截取了Cached靠前的一部分。图3top100即使top200的Size统计之和也只有几个GB离46GB相差甚远结果说明hcache遗漏了很多cache的使用统计。hcache还有一个能力查看某个进程当前使用的cache。我们看看clickhouse的cache使用结果如下图所示。图4正在运行的clickhouse居然只能看到程序可执行文件本身当前的cache占用程序运行过程中已打开的cache文件却没统计。不过这里有个小收获程序加载进内存后程序的可执行文件依赖的库文件使用的内存都是在buffer/cache里。图5从上面的结果发现hcahce有很多缺点只能粗略的看到一些可执行程序文件或者一些库文件使用的cache大小没有统计各程序运行态的cache使用因此对cache占用问题的排查作用非常有限。2、top lsof fincore找了很多资料除了hcache确实没有其他方法可以统计当前运行程序消耗的cache大小了但是hcache本身不可靠。没有直接的办法那就只有另辟蹊径了这也是buffer/cache分布情况不便跟踪调查的原因。该从哪里入手呢当然是top命令给方向哪些程序cpu使用率高且使用了一定的内存那就查它。因为只有它们才有可能在不断的使用cache调查大方向有了。图6下一步呢buffer/cache的使用肯定跟文件相关啊还是那句话linux一切皆文件。那有没有可以实时查看某个进程当前已打开的文件方法lsof命令可以我们用lsof查一下clickhouse某时刻clickhouse打开的文件如下图7图8所示篇幅太长图7只截取了前面部分。图7图8只截取了类型TYPEREGREG表示文件类型为普通还有DIR为目录等等等即截取了clickhouse当前打开且正在使用的一部分类型为普通的文件。图8不断的执行lsof -p $(pidof clickhouse-server)发现每次查看到的文件名都不一样。好了这说明clickhouse会在运行过程中不断的大量打开读写和关闭文件。嫌疑很重了。下一步呢有没有办法可以实时查看当前这些文件是不是使用了cache以及各自使用cache的大小还真有fincore可以查看某个文件使用的cache大小链接GitHub - david415/linux-ftools: fork of http://code.google.com/p/linux-ftools/ · GitHub。轮子就是齐全啊要啥有啥。命令行lsof -p $(pidof clickhouse-server) | grep REG | awk {print $9} | xargs ./fincore --pagesfalse --summarize --only-cached *截图较大点开看会清晰一点。图9fincore统计了命令行执行时clickhouse当前打开的文件使用的cache之和为1.2GB左右。到这里当前的探索结果与前文提到的问题究竟是谁占用了buffer/cache越来越接近了。通过top lsof发现了一个非常重要的线索就是clickhouse在目录/opt/runtime/esch/ch/store下频繁的打开了很多文件那这个目录下面到底都是一些什么文件有没有都使用了cache呢clickhouse是不是cache的消耗大户呢解决这些疑惑就产生了另外一个需求需要一种可以统计指定目录的cache大小的工具。这次fincore也不行了fincore有一个致命弱点即只能获得某个指定文件的cache占用大小不能获取指定目录使用的cache大小更别指望统计嵌套目录的cache大小。因此是时候该请vmtouch出场了链接GitHub - hoytech/vmtouch: Portable file system cache diagnostics and control · GitHub还是这句话轮子就是齐全啊要啥有啥。3、vmtouchvmtouch可以统计指定目录的cache占用大小即使是嵌套目录。迫不及待的直接奔主题看看clickhouse目录/opt/runtime/esch/ch/store下是什么以及使用了多少cache。截取了该目录下的部分文件内容如下图所示。图10直接统计一下/opt/runtime/esch/ch/store目录占用的cache规模吧结果如下图所示。图11shit居然吃了我42GB的内存啊地主家的余粮也不多啊---老板哭着说。激动之余我还要确认一下42GB cache的使用者是不是它如何证明呢还是使用“echo 1 /proc/sys/vm/drop_caches”看看释放完毕之后free可用内存的大小是否会增长42GB左右。执行前的内存分布情况图12图13执行后的内存分布情况图14图15执行cache释放后free从2GB变为了45GB扩大了43GBbuffer/cache从46GB变为了3GB减小了43GB。从cache释放了clickhouse的42GB1GB其它程序占用的cache说明我们环境上clickhouse就是cache的消耗大户老板沸不沸腾我不知道反正我是沸腾了。四、clickhouse cache耗费为什么clickhouse对buffer/cache的消耗如此巨大在好奇心的驱使下又开始了新的调查。此时此景想到了一句歌词一波还未平息一波又来侵袭茫茫人海狂风暴雨。。。1、clickhouse cache耗费原因从哪开始调查呢想起了lsof命令的执行结果如前文图9所示的重要线索clickhouse有大量时间都在打开目录/opt/runtime/esch/ch/store/032/03216cf6-357f-477f-bc9b-5eedb07a5d07判断该目录下面肯定有大量消耗cache的文件。直接进入该目录继续使用vmtouch统计不出所料结果如下图所示032目录就吃了24GB内存心好痛啊。图16clickhouse的什么机制会如此疯狂的消耗cache呢我们再看看目录下有些什么类型的文件截取了部分文件。图17发现目录下主要是很多以日期编号开头的目录文件有纯数字组成的也有带有merge字符的目录。随便打开一个5月17日当天的目录文件20230517_563264_565136_5图1820230517_563264_565136_5目录就占用了2GB cache惊不惊喜意不意外而且上面的所有文件都完全加载到了cache中比如在磁盘中占用743MB的文件cuid.bin同样在cache中占用了742MB。图19查阅clickhouse资料后才发现数字编号的目录都是clickhouse的很多分区partclickhouse服务会根据相关策略自动的在后台合并这些分区。想想如果在每一次合并分区时才将上一次的某两个分区从磁盘进行IO读取那将带来多大的性能开销。因此clickhouse的开发者会将上一次的分区合并结果保存在cache里下一次该分区与其它分区再次进行合并时直接从内存里读取数据就好了。这就是为什么clickhouse消耗如此巨大cache的原因。当然clickhouse对cache的消耗与您当前环境的数据存储规模呈正相关。再来看一个问题那昨天的所有分区加载的数据还会保留在cache里吗我找了一个昨天的分区可以发现昨天的分区目录里的文件是不再占用cache的。上一天的分区clickhouse认为是合并完成的分区已经不需要再进行合并了自然就clear了cache的占用开发者也是想到了的。图202、clickhouse cache耗费调整可是当天分区消耗的cache以及merge过程中使用的cache就让我没法玩了尤其是在clickhouse服务并未独立部署的场景。那clickhouse自身可以支持改变这一机制吗带着疑问又开始了一探究竟完全没法停下来。后来在clickhouse社区找到了一个可以节省cache使用的相关问题。链接feature: dont waste page cache resources when merging big parts · Issue #1566 · ClickHouse/ClickHouse · GitHub有配置min_part_size_for_direct_merge意思是超过min_part_size时启用direct_io。也就是此时clickhouse会通过direct_io的方式读写merge的源文件和目的文件而不是使用cache缓存通过这种方式减少cache的使用。图21我们的clickhouse版本比较高看社区记录clickhouse官方将之前的min_part_size_for_direct_merge改成为min_merge_bytes_to_use_direct_ioMinimal amount of bytes to enable O_DIRECT in merge (0 - disabled)。默认超过10GB时会使用direct_io的方式进行merge。图22那我将min_merge_bytes_to_use_direct_io设置足够小甚至是1byte是不是就可以完全避免对cache的使用了答案是否定的原因是min_merge_bytes_to_use_direct_io只是读写表数据时使用了direct_io替换了常用的buffer_io。也就是说只是在数据传输过程不使用cache节省的是这个环节的cache内存消耗。merge完成后先通过direct_io将数据写入到磁盘同时会继续使用cache缓存merge完成后的数据方便为下一次与其它分区进行快速merge做准备。因为每次merge都是merge旧数据与新数据因此新合成的分区所使用的cache只会比merge前的更大。direct_io与buffer_io的区别如下图所示。