揭秘容器内存统计

本文最先发布在 blog.hdls.me/17255242628...

最近工作中经常会被问到关于容器内存统计的问题:

  1. PageCache 的使用会不会算到 Memory limit 的计算中?
  2. 当容器的内存使用达到了 limit 值,会 Kill 哪个进程?会重启容器吗?
  3. 容器使用缓存盘时,会影响容器的内存统计吗?

本文将整理容器内存统计相关内容,并尝试回答并验证以上几个问题。

内存记账

进程消耗的内存主要包括以下两部分:

  1. 虚拟地址空间映射的物理内存。
  2. 通过读写磁盘生成的 PageCache 消耗的内存。

虚拟地址空间映射的物理内存涵盖了堆、栈等内存使用情况。除了通过 MMap 文件直接映射外,进程还可以通过系统调用进行 I/O 操作,在 Flush 到磁盘之前,会先将数据写入 PageCache。因此,PageCache 也会占用一部分内存,如下图所示。

Cgroup

Cgroup 是一种用于限制、管理和隔离一组进程资源的技术,也是容器实现隔离的重要机制。Cgroup 采用分层管理方式,每个节点包含一组文件,用于统计该节点所涵盖的控制组的各项指标。其中,内存相关统计指标如下:

Memory Cgroup 文件中需要关注的指标:

  • memory.limit_in_bytes:限制当前控制组可以使用的内存大小。对应 K8s、Docker 下 memory limit 指标。
  • memory.usage_in_bytes:当前控制组里所有进程实际使用的内存总和。
  • memory.stat:当前控制组的内存统计详情。

memory.stat 中的字段含义:

  • cache:PageCache 缓存页大小。
  • rss:控制组中所有进程的 anno_rss 内存之和。
  • mapped_file:控制组中所有进程的 file_rss 和 shmem_rss 内存之和。
  • active_anon:活跃 LRU 列表中所有 Anonymous 进程使用内存和 Swap 缓存,包括 tmpfs(shmem)。
  • inactive_anon:不活跃 LRU 列表中所有 Anonymous 进程使用内存和 Swap 缓存,包括 tmpfs(shmem)。
  • active_file:活跃 LRU 列表中所有 file-backed 进程使用内存。
  • inactive_file:不活跃 LRU 列表中所有 file-backed 进程使用内存。

总的来说:

ini 复制代码
cache = active_file + inactive_file
usage_in_bytes = rss + cache

kubectl top

kubectl top 命令通过 Metric-server 获取 Cadvisor 中 working_set 的值,表示 Pod 实例使用的内存大小(不包括 Pause 容器)。

Cadvisor 内存 WorkingSet 算法如下:

go 复制代码
func setMemoryStats(s *cgroups.Stats, ret *info.ContainerStats) {
    ret.Memory.Usage = s.MemoryStats.Usage.Usage
    ret.Memory.MaxUsage = s.MemoryStats.Usage.MaxUsage
    ret.Memory.Failcnt = s.MemoryStats.Usage.Failcnt

    if s.MemoryStats.UseHierarchy {
        ret.Memory.Cache = s.MemoryStats.Stats["total_cache"]
        ret.Memory.RSS = s.MemoryStats.Stats["total_rss"]
        ret.Memory.Swap = s.MemoryStats.Stats["total_swap"]
        ret.Memory.MappedFile = s.MemoryStats.Stats["total_mapped_file"]
    } else {
        ret.Memory.Cache = s.MemoryStats.Stats["cache"]
        ret.Memory.RSS = s.MemoryStats.Stats["rss"]
        ret.Memory.Swap = s.MemoryStats.Stats["swap"]
        ret.Memory.MappedFile = s.MemoryStats.Stats["mapped_file"]
    }
    if v, ok := s.MemoryStats.Stats["pgfault"]; ok {
        ret.Memory.ContainerData.Pgfault = v
        ret.Memory.HierarchicalData.Pgfault = v
    }
    if v, ok := s.MemoryStats.Stats["pgmajfault"]; ok {
        ret.Memory.ContainerData.Pgmajfault = v
        ret.Memory.HierarchicalData.Pgmajfault = v
    }

    workingSet := ret.Memory.Usage
    if v, ok := s.MemoryStats.Stats["total_inactive_file"]; ok {
        if workingSet < v {
            workingSet = 0
        } else {
            workingSet -= v
        }
    }
    ret.Memory.WorkingSet = workingSet
}

总的来说,kubectl top pod 命令查询到的 Memory Usage 与 Cgroup 中的指标关系:

ini 复制代码
Memory WorkingSet 
= usage_in_bytes - inactive_file 
= RSS + active cache

当 WorkingSet 的统计值达到 Memory Limit 时,则会触发 OOM。

OOM

有了以上的内存统计说明后,再来回到开头提到的 3 个问题。

PageCache 对 OOM 的影响

WorkingSet 包含了 RSS 和 active cache,所以 PageCache 中的 active file cache 会影响到进程的 OOM。

启动一个 Pod,运行两个进程,一个不断地申请内存,一个不断地写文件,设置 100MB 的 Memory Limit。查看其监控信息:

可以看到,container_memory_usage_bytes 确实包含了 PageCache。我们还可以看到当 usage_in_bytes 达到 limit 后,并不会立刻 OOM,PageCache 会释放 inactive file cache,直到 WorkingSet 达到 Memory limit 后,才触发 OOM。这是 make sense 的,因为 PageCache 随时可以从内存中逐出,仅仅为了使用磁盘 I/O 就终止进程是没有意义的。

内核中相关的代码如下:

c 复制代码
/*
 * This is the main entry point to direct page reclaim.
 *
 * If a full scan of the inactive list fails to free enough memory then we
 * are "out of memory" and something needs to be killed.
 *
 * If the caller is !__GFP_FS then the probability of a failure is reasonably
 * high - the zone may be full of dirty or under-writeback pages, which this
 * caller can't do much about.  We kick the writeback threads and take explicit
 * naps in the hope that some of these pages can be written.  But if the
 * allocating task holds filesystem locks which prevent writeout this might not
 * work, and the allocation attempt will fail.
 *
 * returns:	0, if no pages reclaimed
 * 		else, the number of pages reclaimed
 */
static unsigned long do_try_to_free_pages(struct zonelist *zonelist,
					  struct scan_control *sc)

Kill 哪个进程

毫无疑问,Kill 的是永远使用内存高的那个进程。只有当容器 1 号进程退出,容器才会退出,Pod 根据其重启策略决定需不需要重启。

我们启动一个 Pod,父进程启动子进程,且 wait 子进程,其退出时打印日志,主进程不退出。其中,父进程不断地写文件,子进程不断地申请内存。

shell 复制代码
$ kubectl logs pc-mem-test
run command
sub command memory
execute sub command [memory]
wait error signal: killed
$ kubectl describe po pc-mem-test
...
Status:           Running
IP:               10.233.89.212
Containers:
  pc-mem-test:
    ...
    State:          Running
      Started:      Sun, 08 Sep 2024 04:52:02 +0000
    Ready:          True
    Restart Count:  0
...

$ // 去对应的节点上
$ dmesg -T | grep oom
[Sun Sep  8 05:04:45 2024] page invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=975
[Sun Sep  8 05:04:45 2024]  oom_kill_process.cold+0xb/0x10
[Sun Sep  8 05:04:45 2024] [  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[Sun Sep  8 05:04:45 2024] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=cri-containerd-37a61e1f48e92a706660d5a4e147692c0630f28b9dd8ee4a84d6929f07c81cf3.scope,mems_allowed=0,oom_memcg=/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podceb06323_be34_45be_ae42_386a69731d8f.slice,task_memcg=/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podceb06323_be34_45be_ae42_386a69731d8f.slice/cri-containerd-37a61e1f48e92a706660d5a4e147692c0630f28b9dd8ee4a84d6929f07c81cf3.scope,task=page,pid=612054,uid=0
[Sun Sep  8 05:04:45 2024] Memory cgroup out of memory: Killed process 612054 (page) total-vm:1369624kB, anon-rss:97512kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:412kB oom_score_adj:975

在 Pod 日志中可以看到父进程捕获到了子进程的退出信息,其收到了 SIGKill 信号。再看 Pod 本身的状态,并未重启。而节点上 demsg 则显示触发了 OOM。

查看该 Pod 的监控信息可以发现,当子进程被 kill 后,父进程仍然在不停地写文件占用 Page Cache。所以 memory_cache 会慢慢上涨,而 working_set 则停留在一个比较低的水位:

OOM 相关的代码如下:

c 复制代码
unsigned long oom_badness(struct task_struct *p, unsigned long totalpages)
{
	long points;
	long adj;

	if (oom_unkillable_task(p))
		return 0;

	p = find_lock_task_mm(p);
	if (!p)
		return 0;

	/*
	 * Do not even consider tasks which are explicitly marked oom
	 * unkillable or have been already oom reaped or the are in
	 * the middle of vfork
	 */
	adj = (long)p->signal->oom_score_adj;
	if (adj == OOM_SCORE_ADJ_MIN ||
			test_bit(MMF_OOM_SKIP, &p->mm->flags) ||
			in_vfork(p)) {
		task_unlock(p);
		return 0;
	}

	/*
	 * The baseline for the badness score is the proportion of RAM that each
	 * task's rss, pagetable and swap space use.
	 */
	points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
		mm_pgtables_bytes(p->mm) / PAGE_SIZE;
	task_unlock(p);

	/* Normalize to oom_score_adj units */
	adj *= totalpages / 1000;
	points += adj;

	/*
	 * Never return 0 for an eligible task regardless of the root bonus and
	 * oom_score_adj (oom_score_adj can't be OOM_SCORE_ADJ_MIN here).
	 */
	return points > 0 ? points : 1;
}


static inline unsigned long get_mm_rss(struct mm_struct *mm)
{
	return get_mm_counter(mm, MM_FILEPAGES) +
		get_mm_counter(mm, MM_ANONPAGES) +
		get_mm_counter(mm, MM_SHMEMPAGES);
}

缓存盘对 OOM 的影响

将宿主机的缓存盘 /dev/shm 挂载进 pod 中,pod 不断地向 /dev/shm 中的某个文件进行追加写,且一直不释放。

从监控中可以看出,缓存盘的使用是统计在 PageCache 的 active file 中,直到触发 OOM。

查看 pod 状态可以看出,进程被 oom 后,page cache 没有被释放,OOM Killer 会 kill Pod 的 init 进程。

shell 复制代码
$ kubectl describe po tmpfs
Name:             tmpfs
Namespace:        default
...
Containers:
  tmpfs:
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       StartError
      Message:      failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: container init was OOM-killed (memory limit too low?): unknown
      Exit Code:    128
      Started:      Thu, 01 Jan 1970 00:00:00 +0000
      Finished:     Mon, 09 Sep 2024 03:14:13 +0000
    Ready:          False
    Restart Count:  153
...

参考

  1. 内存统计说明
  2. How much is too much? The Linux OOMKiller and "used" memory
相关推荐
aherhuo6 小时前
kubevirt网络
linux·云原生·容器·kubernetes
catoop6 小时前
K8s 无头服务(Headless Service)
云原生·容器·kubernetes
liuxuzxx7 小时前
1.24.1-Istio安装
kubernetes·istio·service mesh
道一云黑板报8 小时前
Flink集群批作业实践:七析BI批作业执行
大数据·分布式·数据分析·flink·kubernetes
运维小文8 小时前
K8S中的PV、PVC介绍和使用
docker·云原生·容器·kubernetes·存储
ζั͡山 ั͡有扶苏 ั͡✾9 小时前
Kubeadm+Containerd部署k8s(v1.28.2)集群(非高可用版)
云原生·容器·kubernetes
Hadoop_Liang9 小时前
Kubernetes ConfigMap的创建与使用
云原生·容器·kubernetes
年薪丰厚18 小时前
如何在K8S集群中查看和操作Pod内的文件?
docker·云原生·容器·kubernetes·k8s·container
zhangj112518 小时前
K8S Ingress 服务配置步骤说明
云原生·容器·kubernetes
岁月变迁呀18 小时前
kubeadm搭建k8s集群
云原生·容器·kubernetes