• 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。

  • 相关阅读:
    &【12】Python 函数
    &【11】Python 流程控制
    &【09】Python 数据类型之dictionary
    &【07】Python 数据类型之元组
    &【08】Python 数据类型之set
    &【06】Python 数据类型之list
    &【05】Python 彻底搞懂分片操作
    &【04】Python 数据结构之序列
    SpringBoot-HelloWorld(三)
    SpringBoot-了解微服务(二)
  • 原文地址:https://www.cnblogs.com/yinbiao/p/16277121.html
Copyright © 2020-2023  润新知