k8s最佳实践:部分业务POD内存持续泄露问题
K8S部分业务POD内存持续泄露问题
1.前言
线上K8S集群有极少量的PHP业务,它们的POD内存持续走高直到OOM,相信与特殊代码场景有关,需要展开分析。
我从POD的内存监控原理入手,分析到底内存用到了哪些地方。
2.分析过程
-
第一步:分析pod的内存限制原理
- 容器化依赖Cgroup限制内存资源,Docker采集容器的内存使用量也是基于Cgroup技术
- 实际上,Cgroup标准做法是把每个子系统作为一棵树(Hierarchy),然后在树里面创建子cgroup做 资源限制。
- Centos默认创建了这样的N颗树,每棵树管理1个子系统,K8S就是在这些树中创建子目录来使用Cgroup能力。
-
第二步:分析pod的内存限制是如何实现的(以内存memory为例,我们知道POD可以设置resource limit)
-
首先docker ps找到目标pod的相关容器,至少有2个容器,一个是pause容器,一个是应用容器
-
拿着应用容器的container id,执行docker inspect 可以看到label里有一个pod唯一标识uid
-
K8S创建了kubepods子cgroup,仍旧以memory为例
-
ll /sys/fs/cgroup/memory/kubepods/
-
-
K8S资源限制是POD级的,所以K8S还会在这个cgroup下创建POD的子memory cgroup,进行POD级具体的资源限制。
- 所有POD的总内存限制为30.23G,宿主机是32G内存,其他1G多内存没有纳入cgroup是因为kubelet配置的预留内存导致的。
-
根据上面找到的POD,就可以继续定位到POD级的cgroup了
-
再往POD下面一级就是container的cgroup了,是继承了POD级的限制,反正POD级就那么多内存,里面的单个容器最多也就用这些
-
-
第二步:分析pod的内存都使用到了哪些地方
-
发现应用容器占了1.8G左右,快要把POD的内存限制用满了。(也可以通过docker stats命令查看到容器内存占用)
-
拿着之前发现的pause容器ID,查看一下内存使用,只用了1M左右,因此pause容器的内存占用可以忽略。
-
详细看应用容器的内存使用统计,会发现total_rss和total_cache加起来不过300MB+,其他内存跑哪里去了?
-
[root@10-42-53-112 ~]# cat /sys/fs/cgroup/memory/kubepods/pod931369e9-2a87-4090-a304-dd02122e7acc/7e75c3921b2157ccecc5cff5055940c782f02cb8227ae080874220bb06124dad/memory.stat
-
-
经过了解,cgroup的memory.usage_in_bytes除了计算rss和swap外,还统计了kmem,也就是内核使用内存,我们查看一下实际kmem使用量
-
[root@10-42-53-112 ~]# cat /sys/fs/cgroup/memory/kubepods/pod931369e9-2a87-4090-a304-dd02122e7acc/7e75c3921b2157ccecc5cff5055940c782f02cb8227ae080874220bb06124dad/memory.kmem.usage_in_bytes 1564602368
-
果然1.5G左右,和rss加起来大概就是1.8G了,发现这个应用容器大部分内存都被kernel使用了
-
-
经验告诉我,这些“看不到”的内存大概率是被 slab 使用了。slab allocator 是 Linux 内核的内存分配机制,是给内核对象分配内存的
-
现在虽然知道内存是被 slab 所使用了,但是因为 slab 里面有各种不同的内核对象(object),还需要找到是哪些对象占用了内存,可以查看 /proc/slabinfo 文件,发现占用最多的是 dentry 对象:
-
上述容器使用了790万的dentry,占了1.4G内存;宿主机执行slabtop可以看到整机分配了3000万的dentry,占了6G左右内存。
-
我们只有个别的应用存在内存泄露情况,怀疑与代码特殊行为有关,尝试strace了一下php-fpm,看是否有大量文件操作导致dentry增加:竟然真的在不停的创建临时文件。
-
其行为是先读取socket读进来16384字节的数据:然后才创建了1个临时文件开始写入后续数据,最后再把所有数据从临时文件里读进内存,才开始进入PHP脚本的处理逻辑。
-
我高频抓取了一下/tmp目录,抓到1个临时文件看了一下内容:发现内容就是/comment/bgm_bulk_index接口的POST body体,怀疑PHP-FPM遇到太大的POST体会走临时文件。
-
FPM处理POST表单时,大概会通过php_stream_temp_create_ex创建用于存放解析结果的request_body buffer,第2个参数是内存阈值,一旦超过内存阈值就会写临时文件;
然后循环解析数据写入这个Buffer,因为上述case的POST body总大小是百K,所以就超过了内存阈值,写了临时文件。
这个SAPI_POST_BLOCK_SIZE内存阈值是16进制定义的,实际就是16384
-
3.解决方案
- 最后,在高内存POD所在的node,进行一次slab dentry cache清理,观察POD内存是否下降:
- POD内存从1.8G降到了346M,基本吻合了RSS实际占用,说明kmem部分被释放了。
- 因此,PHP频繁的新建+删除文件,就会不停的分配新的dentry对象,旧的dentry会越来越多直到系统没有更多内存可用才会开始淘汰缓存。
- 定时任务 drop cache,不过过几天就会反弹
- 这个案例告诉我们,docker默认将kmem算作cgroup的内存占用是比较坑的,哪个cgroup创建出来的slab对象就会被算到谁的头上,多多少少有点不合理。
- 所以,也许禁止docker将kmem统计在memory usage内,是不是一个更好的做法呢?网上有诸多讨论,就不赘述了。
- 能不能关闭cgroup kmem counting来避免slab内存计入cgroup呢?是否有风险呢?
4.pod内存泄露的其他场景
1.场景1:nginx反向代理
该问题发生在nginx+php-fpm技术栈,但不限于此场景。
当访问某URL时,其匹配逻辑如下:
- 匹配location /,通过if检查是否存在,如果存在就返回静态文件,否则rewrite到index.php重新匹配。
- 匹配location .php,反向代理请求给PHP-FPM。
也就是说,每个URL都会去磁盘上读一次文件,无论文件是否存在。
这就意味着,有多少种URL,就有多少个slab dentry cache。
当遇到URL美化的场景就有问题了,比如:文章ID是URL的一部分,
/articles/detail/134543
/articles/detail/881929
这种URL的规模是无法估量的,经过nginx先查一次磁盘缓存到dentry,然后再转发给php-fpm进行处理,就必然导致千百万的dentry对象被缓存下来。
类似场景大家可以自行延伸,比如try_files指令也是先找磁盘文件,一样会坑。
场景2:web框架
这个case比较个性化,但也作为一种思路开拓提供给大家。
当我关闭了nginx反向代理先走文件的配置后,发现dentry仍旧在狂涨,因此我就进一步仔细看了一下php-fpm的strace日志。
发现php-fpm每次请求都会去web框架下的cache目录找一个md5样子的文件,难道web框架开启了cache特性?
function _display_cache(&$CFG, &$URI)
{
$cache_path = ($CFG->item('cache_path') == '') ? APPPATH.'cache/' : $CFG->item('cache_path');
// Build the file path. The file name is an MD5 hash of the full URI
$uri = $CFG->item('base_url').
$CFG->item('index_page').
$URI->uri_string;
$filepath = $cache_path.md5($uri);
if ( ! @file_exists($filepath))
{
return FALSE;
}
}
翻了一下框架代码,发现这个框架实现的确有点问题,在没有开启cache特性的情况下仍旧会去cache目录尝试加载一下缓存文件:
因为文件名是URL的MD5,这就导致因为query string的不同而千变万化,即每次请求都将创建1个dentry cache。