我在网上看到一些帖子,人们显然在抱怨托管的 VPS 意外终止进程,因为它们使用了太多 RAM。
这怎么可能?我认为所有现代操作系统都通过仅对物理 RAM 上的任何内容使用磁盘交换来提供“无限 RAM”。它是否正确?
如果进程“由于 RAM 不足而被终止”,可能会发生什么情况?
答案1
如果进程“由于 RAM 不足而被终止”,可能会发生什么情况?
有时有人说,Linux 默认情况下从不拒绝应用程序代码对更多内存的请求——例如malloc()
。1 事实上这并非事实;默认使用启发式
明显的地址空间过量使用将被拒绝。用于典型系统。它确保严重的疯狂分配失败,同时允许过度使用以减少交换使用。
来自[linux_src]/Documentation/vm/overcommit-accounting
(所有引用均来自 3.11 树)。到底什么才算是“严重疯狂的分配”并没有明确说明,所以我们必须通过源代码来确定细节。我们还可以使用脚注 2(如下)中的实验方法来尝试获得启发式的一些反映——基于此,我最初的经验观察是,在理想情况下(==系统空闲),如果你不这样做如果没有任何交换,您将被允许分配大约一半的 RAM,如果您有交换,您将获得大约一半的 RAM 加上所有交换。也就是或多或少每个进程(但请注意这个限制是动态并可能因状态而发生变化,请参阅脚注 5)中的一些观察结果。
RAM 的一半加上交换区明确地默认为/proc/meminfo
.这就是它的含义 - 请注意它实际上与刚刚讨论的限制无关(来自[src]/Documentation/filesystems/proc.txt
):
提交限制:基于过量使用比率('vm.overcommit_ratio'),这是当前可分配的内存总量在系统上。仅当启用严格的过量使用记账(“vm.overcommit_memory”中的模式 2)时,才会遵守此限制。 CommitLimit 使用以下公式计算: CommitLimit = ('vm.overcommit_ratio' * 物理 RAM) + Swap 例如,在具有 1G 物理 RAM 和 7G 交换的系统上,'vm.overcommit_ratio' 为 30 时,将产生CommitLimit 为 7.3G。
前面引用的 overcommit-accounting 文档指出默认值为vm.overcommit_ratio
50。因此,如果您sysctl vm.overcommit_memory=2
,则可以调整 vm.covercommit_ratio (使用sysctl
)并查看结果。3 默认模式,当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
in接近/proc/meminfo
,但修改此 viavm.overcommit_ratio
没有任何区别。然而,在具有 64 MB 交换空间的 3.6.11 32 位 ARM 448 MB 系统上,我在大约 230 MB 时失败。这很有趣,因为在第一种情况下,该数量几乎是 RAM 数量的两倍,而在第二种情况下,大约是 RAM 数量的 1/4——强烈暗示交换量是一个因素。当故障阈值降至约 1.95 GB 时,关闭第一个系统上的交换区证实了这一点,与小 ARM 盒子的比率非常相似。
但这真的是每个进程都存在的吗?看起来是的。下面的简短程序要求用户定义一块内存,如果成功,则等待您按回车键 - 这样您就可以尝试多个同时发生的实例:
#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 CommitLimit
指的是允许的地址空间量系统当 vm.overcommit_memory = 2 时。据推测,您可以分配的内存量应该减去已提交的内存量,这显然是该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
.
答案2
如果您只将 1G 数据加载到内存中,则不会发生这种情况。如果你加载更多怎么办?例如,我经常处理包含数百万个概率的巨大文件,需要将其加载到 R 中。这需要大约 16GB 的 RAM。
在我的笔记本电脑上运行上述过程将导致一旦我的 8GB RAM 被填满,它就会开始疯狂地交换。反过来,这会减慢一切速度,因为从磁盘读取比从 RAM 读取慢得多。如果我的笔记本电脑有 2GB 内存但只有 10GB 可用空间怎么办?一旦进程占用了所有 RAM,它也会填满磁盘,因为它正在写入交换,而我没有更多的 RAM 和交换空间(人们倾向于将交换限制到专用分区而不是交换文件正是出于这个原因)。这就是 OOM 杀手发挥作用并开始杀死进程的地方。
因此,系统确实可能会耗尽内存。此外,大量交换的系统可能早在这种情况发生之前就变得不可用,这仅仅是因为交换导致 I/O 操作缓慢。人们通常希望尽可能避免交换。即使在配备快速 SSD 的高端服务器上,性能也会明显下降。在我的笔记本电脑上,它有一个经典的 7200RPM 驱动器,任何重大的交换基本上都会导致系统无法使用。交换的越多,速度就越慢。如果我不快速终止有问题的进程,一切都会挂起,直到 OOM 杀手介入。
答案3
当没有更多的 RAM 时,进程不会被杀死,当它们被这样欺骗时,它们会被杀死:
- Linux 内核通常允许进程分配(即保留)一定量的虚拟内存,该内存量大于实际可用内存(部分 RAM + 所有交换区域)
- 只要进程只访问它们保留的页面的子集,一切就可以正常运行。
- 如果一段时间后,进程尝试访问它拥有的页面,但没有更多页面可用,则会发生内存不足的情况
- OOM 杀手选择其中一个进程(不一定是请求新页面的进程),然后终止它以恢复虚拟内存。
即使系统没有主动交换,例如,如果交换区域充满了休眠守护进程内存页面,也可能会发生这种情况。
在不过度使用内存的操作系统上永远不会发生这种情况。使用它们,不会杀死随机进程,但在虚拟内存耗尽时请求虚拟内存的第一个进程会错误地返回 malloc (或类似的)。因此,它有机会妥善处理这一情况。然而,在这些操作系统上,也可能会出现系统耗尽虚拟内存而仍有可用 RAM 的情况,这是相当令人困惑且通常会被误解的情况。
答案4
只是从其他答案中添加另一个观点,许多 VPS 在任何给定服务器上托管多个虚拟机。任何单个虚拟机都将拥有指定数量的 RAM 供自己使用。许多提供商提供“突发 RAM”,他们可以使用超出指定数量的 RAM。这仅适用于短期使用,超出此延长时间的用户可能会受到主机终止进程以减少 RAM 使用量的惩罚,这样其他人就不会受到影响主机超载。