overcommit详解

linux的虚拟内存(物理RAM和SWAP)有优化策略,可以优化实际物理内存使用量,其中一种策略叫做“Copy on Write”。

Copy on write:一般我们运行程序都是Fork一个进程后马上执行Exec加载程序,而Fork的时候实际上用的是父进程的堆栈空间,Linux通过Copy On Write技术极大地减少了Fork的开销。Copy On Write的含义是只有真正写的时候才把数据写到子进程的数据,Fork时只会把页表复制到子进程,这样父子进程都指向同一个物理内存页,只有在写子进程的时候才会把内存页的内容重新复制一份。

这样做的结果是,进程实际使用的物理内存,通常比通过/proc文件系统(或者ps命令)查看到的该进程内存使用量要少。

换句话说,在linux世界里,有的进程可能只是“嘴炮”,申请了大量内存,但实际并没有全部使用。只有进程真正要写该虚拟内存的时候,linux才会为它分配物理内存。

因此,overcommit是可以接受的,应该允许分配比物理内存更多的内存。

但无限制的允许overcommit也不合适,Linux支持通过vm.overcommit_memory来设置overcommit策略。内核实现在__vm_enough_memory里。

overcommit 0

这是Linux的默认策略,Heuristic overcommit handling。在这种情况下,除了一些特别夸张的内存申请,一般的内存申请都会被允许。

什么样的内存申请算是“夸张”呢?

如果申请的内存,比剩余内存+swap抠掉共享内存、抠掉保留内存(如sysctl_admin_reserve_kbytestotalreserve_pages)以后还要大,那么就认为这是“夸张的”,内存申请会返回ENOMEM。

overcommit 0能应对大部分情况。

	if (sysctl_overcommit_memory == OVERCOMMIT_GUESS) {
		free = global_page_state(NR_FREE_PAGES);
		free += global_page_state(NR_FILE_PAGES);

		/*
		 * shmem pages shouldn't be counted as free in this
		 * case, they can't be purged, only swapped out, and
		 * that won't affect the overall amount of available
		 * memory in the system.
		 */
		free -= global_page_state(NR_SHMEM);

		free += get_nr_swap_pages();

		/*
		 * Any slabs which are created with the
		 * SLAB_RECLAIM_ACCOUNT flag claim to have contents
		 * which are reclaimable, under pressure.  The dentry
		 * cache and most inode caches should fall into this
		 */
		free += global_page_state(NR_SLAB_RECLAIMABLE);

		/*
		 * Leave reserved pages. The pages are not for anonymous pages.
		 */
		if (free <= totalreserve_pages)
			goto error;
		else
			free -= totalreserve_pages;

		/*
		 * Reserve some for root
		 */
		if (!cap_sys_admin)
			free -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10);

		if (free > pages)
			return 0;

		goto error;
	}

overcommit 1

适合科学应用,典型的例子是使用稀疏矩阵,并且虚拟内存的很多页全部是0。

在这种模式下,任何分配都将成功。

	/*
	 * Sometimes we want to use more memory than we have
	 */
	if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS)
		return 0;

overcommit 2

使用此模式,Linux会严格统计的内存使用情况,并且只会在物理内存可用时才会允许内存申请。由于检查是在分配时完成的,请求内存的程序可以正常处理该故障的情况下,并清理遇到该错误的会话。

overcommit 2会分配一部分物理RAM用于内核使用。分配的数量由设置vm.overcommit_ratio配置。这意味着可用于程序的虚拟内存的数量实际上是:

RAM *(overcommit_ratio / 100) + SWAP

	allowed = (totalram_pages - hugetlb_total_pages())
	       	* sysctl_overcommit_ratio / 100;
	/*
	 * Reserve some for root
	 */
	if (!cap_sys_admin)
		allowed -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10);
	allowed += total_swap_pages;

	/*
	 * Don't let a single process grow so big a user can't recover
	 */
	if (mm) {
		reserve = sysctl_user_reserve_kbytes >> (PAGE_SHIFT - 10);
		allowed -= min_t(long, mm->total_vm / 32, reserve);
	}

	if (percpu_counter_read_positive(&vm_committed_as) < allowed)
		return 0;
error:
	vm_unacct_memory(pages);

	return -ENOMEM;
}

当然也可以根据vm.overcommit_kbytes来设置。

注意,需要ratio不能设置的太高,还需要保留内存用于IO缓冲区和系统调用等,据说有些系统上光网络缓冲区一次就需要超过25 GB的内存,量还是比较大的。

OOM Killer

发生Page Fault时,如果没有足够的可用物理内存,系统将触发OOM Killer。OOM Killer将选择当前使用内存最多的进程,并将杀死该进程。

请注意,通常无法预测何时会OOM Killer,也无法预测会终止哪些进程。

内核中处理这部分的代码在pagefault_out_of_memory,其遍历所有用户态进程,并为进程计算其badness得分,得分最高的会被杀死。

unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
			  const nodemask_t *nodemask, unsigned long totalpages)
{
	long points;
	long adj;

	if (oom_unkillable_task(p, memcg, nodemask))
		return 0;

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

	adj = (long)p->signal->oom_score_adj;
	if (adj == OOM_SCORE_ADJ_MIN) {
		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) + p->mm->nr_ptes +
		 get_mm_counter(p->mm, MM_SWAPENTS);
	task_unlock(p);

	/*
	 * Root processes get 3% bonus, just like the __vm_enough_memory()
	 * implementation used by LSMs.
	 */
	if (has_capability_noaudit(p, CAP_SYS_ADMIN))
		points -= (points * 3) / 100;

	/* 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;
}

总的来说是使用内存越多得分越高,但需要注意几个特殊点:

  • root进程可以有3%的bonus,即只计算得分的97%
  • 可以为进程设置oom_score_adj调整进程的得分。adj范围为-1000 ~ 1000。如果oom_score_adj为-1000(OOM_SCORE_ADJ_MIN),则得分为0;否则按千分之oom_score_adj来计算。这个值可以通过/proc/{pid}/oom_score_adj来设置。
  • 你可能会注意到proc下还有一个文件:/proc/{pid}/oom_adj。它也可以用来调整进程的得分,不过这个文件主要是为了向前兼容旧内核而保留的,它的值与oom_score_adj是线性一致的,如果设置oom_adj,会发现oom_score_adj的值也一起改了。oom_adj的范围为-16 ~ 15。

应该如何选择overcommit策略

前面我们了解了overcommit的三种策略,以及page fault时没有可用内存时OOM Killer的行为,那么应该如何选择哪一种策略呢?

来看两个案例。

案例1:Greenplum

Greenplum要求,必须设置overcommit模式为2。

Greenplum的理由是,策略0和策略1允许过度使用,会造成故障的不可预知性。这对GP来说会比较严重,有可能导致数据文件或事务日志的损坏:因为内存故障可能发生在事务中期,导致立即没有做进行任何清除就关闭了数据库进程。

GP的做法是,关闭overcommit,然后GP自己handle内存申请失败的情况(GP是C语言实现,a piece of cake)。

案例2:kubernetes

然而,k8s这里是另一种风景。

k8s是允许overcommit的,而且玩的很花。

K8s对运行在它之上的Pod定义了3级QoS:

  • Guaranteed (QoS):Pod中所有容器的limits == requests。此类Pod认为是高优先级的,k8s会保证它们不会被杀死。他们的oom_score_adj设置为-998
  • Burstable (QoS):Pod中所有容器的requests < limits(可以是只设置requests,此时limits认为是nodes剩余资源;也可以是同时设置requests和limits,切requests < limits)。这种情况下Pod在系统空闲时可以使用更多资源。Bustable比较复杂,下面单独说。
  • Best-Effort (QoS):对资源没有任何要求。此类Pod认为是最低优先级的,k8s会设置他们的oom_score_adj为1000,一旦有事,先杀他们。

Burstable的情况:

  • 如果内存请求超过总内存的99.8%,设置oom_score_adj为2;否则设置为1000 - 10 * (% of memory requested)
  • burstable的Pod的score总是 > 1,
  • 如果内存的request为0,则设置oom_overcommit_adj为999
  • 如果burstable Pod没有用完request的内存,它的score小于1000,所以肯定是best-effort pod先被kill掉

除了上述3中QoS,k8s还有2种情况:

  • Pod infra containers or Special Pod init process,设置为-998
  • 为了防止自己的管控被OOM Killer干掉,特意为kubelet和docker进程设置了oom_score_adj为-999(干脆设置-1000得了)。

可见,通过定义不同的QoS,k8s能够更灵活、更充分的使用系统内存。

kubernetes不仅允许overcommit,而且更进一步的,它把vm.overcommit_memory设置为了1。为什么要这么做呢?

k8s对于设置了内存requests的Pod有一个承诺,它总是可以使用不超过requests的内存。但当系统内存不足的时候,由于默认内核是启发式的overcommit策略,Best-Effort类型的Pod可能会引起Guaranteed类型的Pod里出现malloc失败,这违背了k8s对guaranted类型的容器的承诺。

为了解决这个问题,k8s在这个issue里,将overcommit策略特意改为了1,即放飞自我,想要多少内存就有多少内存。

With the current QoS policy, kubernetes guarantees that memory allocations will not fail up until the memory request specified by the user. But the nodes are usually set up to fail memory allocations based on kernel heuristics, if there isn't enough memory available to satisfy the request. A Best-Effort container can cause malloc to fail for a Guaranteed container.
To avoid this, we can change the memory overcommit policy to never fail and let the OOM killer handle real memory allocations.
Kubelet on startup will have to run echo 1 > /proc/sys/vm/overcommit_memory.

这样,guaranted类型的Pod在申请内存时,不会被内核拒绝(对比之下,默认overcommit策略在malloc时还是会检查下剩余内存的,如果有些内存被Best-Effort的占用了,Guaranted类型的Pod就申请不到了)。

这么任性的申请显然会造成系统内存过于过度使用,自然会在Page Fault的时候申请不到物理内存,引发OOM Killer。但这个时候就体现出来kuberntes精妙的地方了:

kubernetes精心设计了不同QoS级别的任务的oom_overcommit_adj的值,OOM Killer会按照kubernetes的规划来杀死kuberntes想让它杀死的进程(优先杀死Best-Effort),而保障它想保障的进程,一方面充分利用了内存,另一方面还提供了QoS,设计非常精巧。

谁更合适?

其实Greenplum和kubernetes在他们的场景下,都是合适的。

对于Greenplum来说,它认为整个集群的资源都是独占的,所以不允许overcommit对它来说是最优解;而kubernetes为Pod定义了QoS,允许overcommit可以更灵活、更充分的使用整个系统的内存资源。

各有各的道理。

但如果Greenplum跟kubernetes混布的时候,怎么设置overcommit值呢?

相对来说,kuberntes的方案适应性会更好一些,所以应该采取overcommit 1 策略;但由于这对于Greenplum比较危险,而且它很有可能因为比较胖而被杀死,所以应该同步设置Greenplum那一堆Postgres进程的oom_score_adj

不妨就设置为-1000好了,kuberlet挂了,GP都不会挂。

Ref: