kubernetes上报Pod已用内存不准问题分析
1.问题描述:
经常有业务反馈在使用容器云平台过程中监控展示的业务使用内存不准,分析了下kubernetes采集Pod内存使用的实现原理以及相应的解决思路
2.问题分析:
2.1 问题排查:
监控数据是采集的kubernetes上报的container_memory_working_set_bytes字段:
分析kubernetes代码可以看到container_memory_working_set_bytes是取自cgroup memory.usage_in_bytes 与memory.stat total_inactive_file两者的差值:
分析内核代码发现memory.usage_in_bytes的统计数据是包含了所有的file cache的, total_active_file和total_inactive_file都属于file cache的一部分,并且这两个数据并不是业务真正占用的内存,只是系统为了提高业务的访问IO的效率,将读写过的文件缓存在内存中,file cache并不会随着进程退出而释放,只会当容器销毁或者系统内存不足时才会由系统自动回收。
__add_to_page_cache_locked..->mem_cgroup_cache_charge->mem_cgroup_charge_common->__mem_cgroup_try_charge->mem_cgroup_do_charge
综上分析,kubernetes采用memory.usage_in_bytes - total_inactive_file并不能真正计算出Pod实际已使用的内存空间,当Pod内存资源紧张时total_active_file也是可回收利用的。
2.2 验证分析结论:
通过下面例子可以验证上面的分析:
<1> 创建一个容器:
cgcreate -g memory:test-docker-memory
docker run --cgroup-parent=/test-docker-memory --net=none -v /root/test_mem:/test -idt --name test --privileged csighub.tencentyun.com/admin/tlinux2.2-bridge-tcloud-underlay:latest
<2> 进入容器遍历一个大小约为580M message的文件:
memory.stat统计数据total_inactive_file增加580多M:
<3> 对同一个文件再遍历一遍
此时total_inactive_file的统计数据会将message文件占用的内存转为total_active_file
<3> 通过drop_caches触发一次内存回收可以看到active(file) 和 inactive(file)都会被回收:
3 解决方法:
3.1 Linux 如何计算free内存
要解决该问题先要了解内核是如何统计free内存的,从下面代码可以知道内核计算空闲内存方法如下(容器平台未使用swap):
NR_FREE_PAGES + NR_FILE_PAGES – NR_SHMEM_PAGES + NR_SLAB_RECLAIMABLE
从下列代码可以知道NR_FILE_PAGES在无swap的情况下包含cache pages和buffer pages:
因此有效内存可以通过转化为cgroup memory.meminfo对应字段计算得出:
available = MemFree+(Cached- Shmem + Buffers+ SReclaimable)
3.2 Cgroup的真实使用内存如何计算?
由于cgroup当前并未提供memory.meminfo的统计信息,所以kubernetes无法通过
该公式获取Pod所在的cgroup已使用内存。
Cgroup当前提供了memory.stat和memory.usage_in_bytes的统计信息,看下如何将memory.meminfo的计算方法转为memory.stat的计算公式:
因为”Shmem”(即IPCS shared memory & tmpfs)包含在Cached中,而不在Active(file)和Inactive(file)中,并且Active(file)和Inactive(file)还包含Buffers。另外mem lock的内存不在LRU链表中,所以如果有mlock的话,得出如下等式(mlock包括file和anon两部分,/proc/meminfo中并未分开统计,下面的mlock_file只是用来表意,实际并没有这个统计值):
【Active(file) + Inactive(file) + Shmem + mlock_file】== 【Cached + Buffers】
memory.usage_in_bytes统计包含了Cached和Buffers,Cached中除了mlock_file和Shmem(IPCS shared memory & tmpfs)外,其他部分File cache是可以回收使用的,Buffers也是可以回收利用的,所以pod容器所在cgroup实际使用的内存计算公式可以转化为(因memory.stat未导出SReclaimable,这里忽略SReclaimable):
real_used = memory.usage_in_bytes – (Cached- Shmem - mlock_file + Buffers )
= memory.usage_in_bytes – memory.stat .( total_inactive_file + total_active_file )
因此kubernetes container_memory_working_set_bytes字段在计算实际已使用内存时应该改为
memory.usage_in_bytes – memory.stat .( total_inactive_file + total_active_file )
附录:
Cache包含ipcs shm和tmpfs内存验证:
1.运行申请tmpfs和ipcs shm共享内存前读取当前memory.stat数据:
2. 拷贝一个580M左右的文件到tmpfs挂载点/run占用580M的共享内存,运行IPCS 测试程序申请一段300M的ipcs shm:
Tmpfs + ipcs shm = 582349583+314572800 = 896922383
3.再次查看memory.stat的total_cache,增加的值约等于步骤2中tmpfs和ipcs shm增加的内存使用值:
Added total_cache = 1077981184 – 181751808 = 896229376
ipcs shm测试代码:
# cat test_shm.c
#include <sys/types.h>
#include<stdio.h>
#include<string.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <time.h>
#define LEN (300*1024*1024)
static char buf[LEN] = {0};
int main(int agrc, char *argv[])
{
key_t key = 888999;
int shmid = shmget(key, LEN, IPC_CREAT | IPC_EXCL);
if(shmid == -1)
{
printf("shmget err: %d %s\n", errno, strerror(errno));
return -1;
}
void *p = shmat(shmid, NULL, SHM_R | SHM_W);
if (p == NULL)
{
printf("shmat failed\n");
return -1;
}
//shmctl(shmid, IPC_RMID, 0);
printf("first shmid: %d p: %p\n", shmid, p);
memset(p, 'a', LEN-1);
p = shmat(shmid, NULL, SHM_R | SHM_W);
if (p == NULL)
{
printf("shmat xx failed\n");
return -1;
}
memcpy(buf, p, LEN-1);
printf("second shmid: %d p: %p\n", shmid, p);
getchar();
shmctl(shmid, IPC_RMID, 0);
}