我在网上看到很多帖子,似乎是在抱怨托管的VPS意外终止进程,因为它们使用了太多的RAM。
这怎么可能?我以为所有现代OS都通过对物理RAM上的内容使用磁盘交换来提供“无限RAM”。它是否正确?
如果进程“由于RAM不足而被杀死”会发生什么情况?
我在网上看到很多帖子,似乎是在抱怨托管的VPS意外终止进程,因为它们使用了太多的RAM。
这怎么可能?我以为所有现代OS都通过对物理RAM上的内容使用磁盘交换来提供“无限RAM”。它是否正确?
如果进程“由于RAM不足而被杀死”会发生什么情况?
Answers:
如果进程“由于RAM不足而被杀死”会发生什么情况?
有时有人说linux默认情况下从不拒绝从应用程序代码获取更多内存的请求-例如malloc()
。1 事实并非如此;默认使用启发式
明显的地址空间过量使用被拒绝。用于典型系统。它确保严重的野生分配失败,同时允许过量使用以减少交换使用。
From [linux_src]/Documentation/vm/overcommit-accounting
(所有引号均来自3.11树)。确切地讲,没有明确说明什么是“严重的疯狂分配”,因此我们将不得不通过源头来确定细节。我们还可以使用脚注2(如下)中的实验方法来尝试启发式思考-基于此,我的初步经验观察是,在理想情况下(==系统处于空闲状态),如果您不这样做,没有任何交换,您将可以分配大约一半的RAM,如果有交换,则将获得大约一半的RAM加上所有交换。每个过程或多或少都差不多(但是请注意,此限制是动态的,并且由于状态而可能会发生变化,请参见脚注5中的一些观察结果)。
一半的RAM加交换明确是中“ CommitLimit”字段的默认值/proc/meminfo
。这就是它的意思-请注意,它实际上与刚才讨论的限制无关(来自[src]/Documentation/filesystems/proc.txt
):
CommitLimit:基于超量分配比率('vm.overcommit_ratio'),这是当前可在系统上分配的内存总量。仅当启用了严格的过量使用记帐(“ vm.overcommit_memory”中的模式2)时,才遵守此限制。CommitLimit使用以下公式计算:CommitLimit =('vm.overcommit_ratio'*物理RAM)+交换例如,在具有1G物理RAM和7G交换且'vm.overcommit_ratio'为30的系统上,它将产生7.3G的CommitLimit。
前面引用的overcommit-accounting doc指出默认vm.overcommit_ratio
值为50。因此,如果使用sysctl vm.overcommit_memory=2
,则可以调整vm.covercommit_ratio(带有sysctl
)并查看结果。3 默认模式为when,CommitLimit
并且不强制执行when 且仅“拒绝地址空间的明显过量使用” vm.overcommit_memory=0
。
虽然默认策略确实具有启发式的按进程限制,以防止“严重的分配”,但它确实使整个系统可以自由地进行严重的分配。4 这意味着它有时可能会耗尽内存,并且必须通过OOM杀手向某些进程声明破产。
OOM杀手会杀死什么?不一定是在没有内存时才请求内存的进程,因为这不一定是真正有罪的进程,更重要的是,也不一定是最迅速使系统摆脱其所处问题的进程。
这是从这里引用的,它可能引用了2.6.x源:
/*
* oom_badness - calculate a numeric value for how bad this task has been
*
* The formula used is relatively simple and documented inline in the
* function. The main rationale is that we want to select a good task
* to kill when we run out of memory.
*
* Good in this context means that:
* 1) we lose the minimum amount of work done
* 2) we recover a large amount of memory
* 3) we don't kill anything innocent of eating tons of memory
* 4) we want to kill the minimum amount of processes (one)
* 5) we try to kill the process the user expects us to kill, this
* algorithm has been meticulously tuned to meet the principle
* of least surprise ... (be careful when you change it)
*/
这似乎是一个不错的理由。但是,在没有取证的情况下,#5(这是#1的多余部分)似乎很难实施,而#3则是#2的冗余部分。因此,将其缩减至#2/3和#4可能是有意义的。
我浏览了最近的资料(3.11),发现此注释在此期间已更改:
/**
* oom_badness - heuristic function to determine which candidate task to kill
*
* The heuristic for determining which task to kill is made to be as simple and
* predictable as possible. The goal is to return the highest value for the
* task consuming the most memory to avoid subsequent oom failures.
*/
关于#2,这是更明确的一点:“目标是[杀死]消耗最大内存的任务,以避免随后的oom失败,”并暗示#4(“我们要杀死最少数量的进程(一个) )。
如果要查看实际使用中的OOM杀手,请参阅脚注5。
1幻觉吉尔斯幸免于我,见评论。
2这是简单的C语言,它要求越来越大的内存块来确定何时请求更多的请求失败:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#define MB 1 << 20
int main (void) {
uint64_t bytes = MB;
void *p = malloc(bytes);
while (p) {
fprintf (stderr,
"%lu kB allocated.\n",
bytes / 1024
);
free(p);
bytes += MB;
p = malloc(bytes);
}
fprintf (stderr,
"Failed at %lu kB.\n",
bytes / 1024
);
return 0;
}
如果您不了解C,则可以对其进行编译gcc virtlimitcheck.c -o virtlimitcheck
,然后运行./virtlimitcheck
。它是完全无害的,因为该进程不使用它所要求的任何空间-即,它从未真正使用过任何RAM。
在具有4 GB系统和6 GB交换空间的3.11 x86_64系统上,我失败于〜7400000 kB;数量波动,所以状态可能是一个因素。巧合的CommitLimit
是/proc/meminfo
,它接近in ,但修改该via vm.overcommit_ratio
并没有任何区别。但是,在具有64 MB交换空间的3.6.11 32位ARM 448 MB系统上,我无法使用〜230 MB。这很有趣,因为在第一种情况下,其数量几乎是RAM数量的两倍,而在第二种情况下,其数量约为RAM的1/4,这强烈暗示着交换数量是一个因素。当故障阈值降低到〜1.95 GB时(与小型ARM盒非常相似),通过在第一个系统上关闭交换来确认这一点。
但这真的是每个过程吗?好像是。下面的简短程序要求用户定义的内存块,如果成功,则等待您按下return键–这样,您可以尝试同时执行多个实例:
#include <stdio.h>
#include <stdlib.h>
#define MB 1 << 20
int main (int argc, const char *argv[]) {
unsigned long int megabytes = strtoul(argv[1], NULL, 10);
void *p = malloc(megabytes * MB);
fprintf(stderr,"Allocating %lu MB...", megabytes);
if (!p) fprintf(stderr,"fail.");
else {
fprintf(stderr,"success.");
getchar();
free(p);
}
return 0;
}
但是请注意,无论使用什么,这都不严格是关于RAM和交换量的-关于系统状态影响的观察,请参见脚注5。
3表示 当vm.overcommit_memory = 2时系统CommitLimit
允许的地址空间量。大概,那么,您可以分配的数量应减去已提交的内容,这显然是该字段。Committed_AS
一个可能有趣的实验证明了这一点,它是添加#include <unistd.h>
到virtlimitcheck.c的顶部(请参见脚注2),然后添加到循环fork()
之前while()
。如果没有一些繁琐的同步,则不能保证按此处所述进行工作,但是,YMMV有很大的机会:
> sysctl vm.overcommit_memory=2
vm.overcommit_memory = 2
> cat /proc/meminfo | grep Commit
CommitLimit: 9231660 kB
Committed_AS: 3141440 kB
> ./virtlimitcheck 2&> tmp.txt
> cat tmp.txt | grep Failed
Failed at 3051520 kB.
Failed at 6099968 kB.
这是有道理的-详细查看tmp.txt,您可以看到进程交替分配越来越大的分配(如果将pid放入输出中,则更容易),直到一个明显声称另一个已失败为止。然后,获胜者可以自由地将一切CommitLimit
减为零Committed_AS
。
4在这一点上,值得一提的是,如果您还不了解虚拟寻址和需求分页,那么首先使过度承诺成为可能的是,内核分配给用户区进程的根本不是物理内存,而是虚拟地址空间。例如,如果某个进程为某件事保留了10 MB的空间,则将其布置为一系列(虚拟)地址,但这些地址尚未对应于物理内存。访问此类地址时,将导致页面错误然后内核尝试将其映射到实际内存中,以便可以存储实际值。进程通常保留比实际访问更多的虚拟空间,这使内核可以最有效地利用RAM。但是,物理内存仍然是有限的资源,当所有物理内存都已映射到虚拟地址空间时,必须消除某些虚拟地址空间以释放一些RAM。
5首先警告:如果尝试使用vm.overcommit_memory=0
,请确保先保存您的工作并关闭所有关键应用程序,因为系统将冻结约90秒,并且某些进程将死亡!
这个想法是运行一个90秒后超时的前叉炸弹,前叉会分配空间,其中一些会向RAM报告大量数据,同时始终向stderr报告。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>
/* 90 second "Verbose hungry fork bomb".
Verbose -> It jabbers.
Hungry -> It grabs address space, and it tries to eat memory.
BEWARE: ON A SYSTEM WITH 'vm.overcommit_memory=0', THIS WILL FREEZE EVERYTHING
FOR THE DURATION AND CAUSE THE OOM KILLER TO BE INVOKED. CLOSE THINGS YOU CARE
ABOUT BEFORE RUNNING THIS. */
#define STEP 1 << 30 // 1 GB
#define DURATION 90
time_t now () {
struct timeval t;
if (gettimeofday(&t, NULL) == -1) {
fprintf(stderr,"gettimeofday() fail: %s\n", strerror(errno));
return 0;
}
return t.tv_sec;
}
int main (void) {
int forks = 0;
int i;
unsigned char *p;
pid_t pid, self;
time_t check;
const time_t start = now();
if (!start) return 1;
while (1) {
// Get our pid and check the elapsed time.
self = getpid();
check = now();
if (!check || check - start > DURATION) return 0;
fprintf(stderr,"%d says %d forks\n", self, forks++);
// Fork; the child should get its correct pid.
pid = fork();
if (!pid) self = getpid();
// Allocate a big chunk of space.
p = malloc(STEP);
if (!p) {
fprintf(stderr, "%d Allocation failed!\n", self);
return 0;
}
fprintf(stderr,"%d Allocation succeeded.\n", self);
// The child will attempt to use the allocated space. Using only
// the child allows the fork bomb to proceed properly.
if (!pid) {
for (i = 0; i < STEP; i++) p[i] = i % 256;
fprintf(stderr,"%d WROTE 1 GB\n", self);
}
}
}
编译这个gcc forkbomb.c -o forkbomb
。首先,尝试一下sysctl vm.overcommit_memory=2
-您可能会得到类似以下内容的信息:
6520 says 0 forks
6520 Allocation succeeded.
6520 says 1 forks
6520 Allocation succeeded.
6520 says 2 forks
6521 Allocation succeeded.
6520 Allocation succeeded.
6520 says 3 forks
6520 Allocation failed!
6522 Allocation succeeded.
在这种环境下,这种叉子炸弹不会走得很远。请注意,“说N个分叉”中的数目不是进程总数,而是链中导致该分支的分支/分支中的进程数。
现在尝试使用vm.overcommit_memory=0
。如果将stderr重定向到文件,则可以随后进行一些粗略分析,例如:
> cat tmp.txt | grep failed
4641 Allocation failed!
4646 Allocation failed!
4642 Allocation failed!
4647 Allocation failed!
4649 Allocation failed!
4644 Allocation failed!
4643 Allocation failed!
4648 Allocation failed!
4669 Allocation failed!
4696 Allocation failed!
4695 Allocation failed!
4716 Allocation failed!
4721 Allocation failed!
只有15处理未能分配1 GB -证明为overcommit_memory = 0启发式是通过状态的影响。那里有几个过程?查看tmp.txt的末尾,可能> 100,000。现在实际上如何才能使用1 GB?
> cat tmp.txt | grep WROTE
4646 WROTE 1 GB
4648 WROTE 1 GB
4671 WROTE 1 GB
4687 WROTE 1 GB
4694 WROTE 1 GB
4696 WROTE 1 GB
4716 WROTE 1 GB
4721 WROTE 1 GB
八个-这又很有意义,因为当时我有约3 GB的可用RAM和6 GB的交换空间。
完成此操作后,请查看系统日志。您应该看到OOM杀手的报告分数(以及其他);大概与oom_badness
。
如果您仅将1G数据加载到内存中,则不会发生这种情况。如果负载更多,该怎么办?例如,我经常处理包含数百万个概率的巨大文件,这些文件需要加载到R中。这需要大约16GB的RAM。
在我的笔记本电脑上运行上述过程后,一旦我的8GB RAM装满,它就会开始疯狂地交换。反过来,这将减慢所有速度,因为从磁盘读取比从RAM读取要慢得多。如果我有一台笔记本电脑只有2GB的内存,只有10GB的可用空间,该怎么办?一旦该进程占用了所有RAM,它也将填满磁盘,因为它正在写入交换空间,而我没有更多的RAM,也没有交换空间(人们倾向于将交换限制为专用分区而不是磁盘空间)。正是出于这个原因,swapfile)。这就是OOM杀手的来历,并开始杀死进程。
因此,系统确实会耗尽内存。此外,仅由于交换引起的I / O操作缓慢,大量交换系统可能在很久之前就变得不可用。人们通常希望尽可能避免交换。即使在具有快速SSD的高端服务器上,性能也会明显下降。在我的笔记本电脑上,该笔记本电脑具有经典的7200RPM驱动器,任何重要的更换操作都会使系统无法使用。交换的次数越多,获取的速度就越慢。如果我不及时终止令人讨厌的过程,那么一切都会挂起,直到OOM终止者介入。
没有更多的RAM时不会杀死进程,以这种方式被欺骗时会杀死它们:
即使在系统未主动进行交换的情况下(例如,如果交换区域中充满了睡眠守护程序内存页面),也可能会发生这种情况。
在不过度使用内存的OS上永远不会发生这种情况。使用它们,没有随机进程被杀死,但是在虚拟内存耗尽时要求虚拟内存的第一个进程使malloc(或类似的)返回错误。因此,有机会适当地处理这种情况。但是,在这些OS上,也可能会发生系统用尽虚拟内存而仍然有可用RAM的情况,这非常令人困惑并且通常会被误解。
当可用RAM耗尽时,内核开始将处理位换出到磁盘。实际上,内核在RAM即将用尽时开始交换a:它在有空闲时刻时开始主动交换,以便在应用程序突然需要更多内存时更加敏感。
请注意,RAM不仅用于存储进程的内存。在典型的运行状况良好的系统上,进程仅使用大约一半的RAM,另一半用于磁盘高速缓存和缓冲区。这在运行的进程和文件输入/输出之间提供了良好的平衡。
交换空间不是无限的。在某个时候,如果进程不断分配越来越多的内存,则来自RAM的溢出数据将填充交换空间。发生这种情况时,尝试请求更多内存的进程将看到其请求被拒绝。
默认情况下,Linux 过量使用内存。这意味着它有时将允许进程使用已保留但未使用的内存运行。过度承诺的主要原因是分叉的工作方式。当一个进程启动一个子进程时,子进程从概念上讲是在父进程的内存副本中运行的-两个进程最初具有具有相同内容的内存,但是随着进程在各自的空间中进行更改,该内容将有所不同。为了完全实现这一点,内核将必须复制父代的所有内存。这会使分叉变慢,因此内核实践写时复制:最初,孩子与父母共享所有的记忆;每当任何一个进程写入共享页面时,内核都会创建该页面的副本以破坏共享。
通常,一个孩子会留下很多未触及的页面。如果内核分配了足够的内存来复制每个派生服务器上父代的内存空间,那么在子进程将不再使用的预留空间中会浪费大量内存。因此,过度使用:内核仅根据子代需要多少页的估计量来保留一部分内存。
如果某个进程尝试分配一些内存而没有足够的内存,则该进程将收到错误响应并按其认为适当的方式进行处理。如果某个进程通过写入必须不共享的共享页面间接请求内存,则情况就不同了。无法将这种情况报告给应用程序:它认为那里有可写的数据,甚至可以读取它-只是编写需要在后台进行一些更为精细的操作。如果内核无法提供新的内存页,则它所能做的就是杀死请求进程,或者杀死其他进程来填充内存。
您可能会认为此时终止请求进程是显而易见的解决方案。但是实际上,这不是很好。该过程可能是一个重要的过程,碰巧现在只需要访问其页面之一,而可能正在运行其他次要的过程。因此,内核包括复杂的启发式方法来选择要杀死的进程-著名的OOM杀手。
一段时间后,Linux占用了外部虚拟空间。那就是交换分区。当Ram被填满时,Linux将使用此交换区域来运行低优先级进程。