![Linux에서 "RAM이 부족"할 수 있습니까?](https://rvso.com/image/50560/Linux%EC%97%90%EC%84%9C%20%22RAM%EC%9D%B4%20%EB%B6%80%EC%A1%B1%22%ED%95%A0%20%EC%88%98%20%EC%9E%88%EC%8A%B5%EB%8B%88%EA%B9%8C%3F.png)
웹에서 호스팅된 VPS가 너무 많은 RAM을 사용하여 프로세스를 예기치 않게 종료하는 것에 대해 불평하는 사람들이 있는 여러 게시물을 보았습니다.
이것이 어떻게 가능한지? 나는 모든 최신 OS가 물리적 RAM을 초과하는 모든 것에 대해 디스크 스왑을 사용하여 "무한 RAM"을 제공한다고 생각했습니다. 이 올바른지?
"RAM 부족으로 인해 프로세스가 종료"되면 어떻게 될까요?
답변1
"RAM 부족으로 인해 프로세스가 종료"되면 어떻게 될까요?
Linux는 기본적으로 애플리케이션 코드에서 더 많은 메모리를 요청하는 것을 절대 거부하지 않는다고 합니다 malloc()
. 1 이는 사실이 아닙니다. 기본값은 경험적 방법을 사용합니다.
주소 공간의 명백한 초과 커밋은 거부됩니다. 일반적인 시스템에 사용됩니다. 스왑 사용량을 줄이기 위해 오버커밋을 허용하는 동시에 심각한 할당 실패를 보장합니다.
출처 [linux_src]/Documentation/vm/overcommit-accounting
(모든 인용문은 3.11 트리에서 인용되었습니다). "심각한 와일드 할당"으로 간주되는 것이 정확히 무엇인지 명시되지 않았으므로 자세한 내용을 확인하려면 소스를 살펴봐야 합니다. 우리는 또한 각주 2(아래)의 실험 방법을 사용하여 경험적 방법을 어느 정도 반영해 볼 수 있습니다. 이를 바탕으로 제가 처음 경험적으로 관찰한 바에 따르면 이상적인 상황(== 시스템이 유휴 상태)에서 그렇지 않은 경우 스왑이 없으면 RAM의 절반 정도를 할당할 수 있으며, 스왑이 있는 경우 RAM의 절반과 스왑 전체를 더해 할당할 수 있습니다. 그것은 다소간프로세스당(단, 이 제한 사항에 유의하세요.~이다동적이며 상태에 따라 변경될 수 있습니다. 각주 5의 일부 관찰 내용을 참조하세요.
RAM의 절반과 스왑이 명시적으로 .NET의 "CommitLimit" 필드에 대한 기본값입니다 /proc/meminfo
. 이것이 의미하는 바는 다음과 같습니다. 실제로 방금 논의한 제한( 에서 [src]/Documentation/filesystems/proc.txt
)과 아무 관련이 없습니다.
커밋한도:오버커밋 비율('vm.overcommit_ratio')을 기준으로 현재 할당 가능한 총 메모리 양입니다.시스템에서.이 제한은 엄격한 오버커밋 계정이 활성화된 경우에만 준수됩니다('vm.overcommit_memory'의 모드 2). CommitLimit은 다음 공식으로 계산됩니다. CommitLimit = ('vm.overcommit_ratio' * 물리적 RAM) + 스왑 예를 들어 물리적 RAM이 1G이고 스왑이 7G이고 'vm.overcommit_ratio'가 30인 시스템에서는 다음과 같은 결과가 나옵니다. 7.3G의 CommitLimit.
이전에 인용된 초과 커밋 회계 문서에서는 기본값이 vm.overcommit_ratio
50이라고 명시하고 있습니다. 따라서 vm.covercommit_ratio(를 사용하여 )를 조정하고 결과를 확인할 sysctl vm.overcommit_memory=2
수 있습니다. 3 가 시행되지 않고 "명백한 주소 공간 오버커밋만 거부"되는 기본 모드 는 입니다 .sysctl
CommitLimit
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 Gilles가 고맙게도 나를 제거한 망상입니다. 댓글을 참조하세요.
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을 전혀 사용하지 않습니다.
4GB 시스템과 6GB 스왑을 갖춘 3.11 x86_64 시스템에서는 ~7400000kB에서 실패했습니다. 숫자는 변동하므로 아마도 상태가 요인일 수 있습니다. 이는 우연히 CommitLimit
in 에 가깝지만 /proc/meminfo
이 via를 수정해도 vm.overcommit_ratio
아무런 차이가 없습니다. 그러나 64MB의 스왑이 있는 3.6.11 32비트 ARM 448MB 시스템에서는 ~230MB에서 실패합니다. 첫 번째 경우에는 그 양이 RAM 양의 거의 두 배인 반면, 두 번째 경우에는 약 1/4이므로 스왑 양이 중요한 요소임을 강력하게 암시하므로 이는 흥미로울 수 있습니다. 이는 오류 임계값이 작은 ARM 상자와 매우 유사한 비율인 ~1.95GB로 내려갔을 때 첫 번째 시스템에서 스왑을 꺼서 확인되었습니다.
하지만 이것이 정말로 프로세스별로 이루어지는 걸까요? 그럴 것 같습니다. 아래의 짧은 프로그램은 사용자 정의 메모리 청크를 요청하고, 성공하면 반환을 누를 때까지 기다립니다. 이렇게 하면 여러 동시 인스턴스를 시도할 수 있습니다.
#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 이 시점에서 가상 주소 지정과 요구 페이징을 아직 이해하지 못했다면 우선 커널이 사용자 영역 프로세스에 할당하는 것이 물리적 메모리가 아니라는 점을 언급할 가치가 있습니다.가상 주소 공간. 예를 들어, 프로세스가 무언가를 위해 10MB를 예약하는 경우 이는 일련의 (가상) 주소로 배치되지만 해당 주소는 아직 실제 메모리와 일치하지 않습니다. 이러한 주소에 액세스하면 다음과 같은 결과가 발생합니다.페이지 오류그런 다음 커널은 실제 값을 저장할 수 있도록 이를 실제 메모리에 매핑하려고 시도합니다. 프로세스는 일반적으로 실제로 액세스하는 것보다 훨씬 더 많은 가상 공간을 예약하므로 커널이 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개의 프로세스만이 1GB를 할당하지 못했습니다. 이는 overcommit_memory = 0에 대한 경험적 방법임을 보여줍니다.~이다상태의 영향을 받습니다. 프로세스가 몇 개 있었나요? tmp.txt의 끝을 보면 아마도 100,000보다 클 것입니다. 이제 실제로 1GB를 어떻게 사용해야 할까요?
> 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
8개 - 당시에는 3GB RAM과 6GB의 스왑 공간이 있었기 때문에 다시 한 번 이해가 됩니다.
이 작업을 수행한 후 시스템 로그를 살펴보십시오. 무엇보다도 OOM 킬러 보고 점수가 표시되어야 합니다. 아마도 이것은 oom_badness
.
답변2
1G의 데이터만 메모리에 로드하는 경우에는 이런 일이 발생하지 않습니다. 훨씬 더 많은 것을로드하면 어떨까요? 예를 들어, 나는 R에 로드해야 하는 수백만 개의 확률이 포함된 대용량 파일을 자주 작업합니다. 여기에는 약 16GB의 RAM이 필요합니다.
내 노트북에서 위 프로세스를 실행하면 8GB RAM이 채워지자마자 미친 듯이 교체가 시작됩니다. 그러면 디스크에서 읽는 것이 RAM에서 읽는 것보다 훨씬 느리기 때문에 모든 것이 느려집니다. RAM이 2GB이고 여유 공간이 10GB만 있는 노트북이 있으면 어떻게 되나요? 프로세스가 모든 RAM을 사용하면 스왑에 쓰기 때문에 디스크도 가득 차고 더 이상 RAM도 없고 스왑할 공간도 없습니다. 사람들은 스왑을 파티션이 아닌 전용 파티션으로 제한하는 경향이 있습니다. 바로 그 이유 때문에 스왑 파일). OOM 킬러가 나타나서 프로세스를 죽이기 시작하는 곳입니다.
따라서 시스템에 실제로 메모리가 부족할 수 있습니다. 더욱이, 스와핑이 심한 시스템은 스와핑으로 인한 느린 I/O 작업 때문에 이러한 일이 발생하기 훨씬 전에 사용할 수 없게 될 수 있습니다. 일반적으로 가능한 한 교환을 피하고 싶어합니다. 빠른 SSD를 장착한 고급 서버에서도 성능이 확실히 저하됩니다. 클래식 7200RPM 드라이브가 장착된 내 노트북에서는 상당한 교체로 인해 기본적으로 시스템을 사용할 수 없게 됩니다. 많이 교환할수록 속도가 느려집니다. 문제가 되는 프로세스를 신속하게 종료하지 않으면 OOM 킬러가 개입할 때까지 모든 것이 중단됩니다.
답변3
더 이상 RAM이 없어도 프로세스는 종료되지 않으며, 다음과 같은 방식으로 속였을 때 종료됩니다.
- Linux 커널은 일반적으로 프로세스가 실제로 사용 가능한 것(RAM의 일부 + 모든 스왑 영역)보다 큰 가상 메모리 양을 할당(예: 예약)할 수 있도록 허용합니다.
- 프로세스가 예약한 페이지의 하위 집합에만 액세스하는 한 모든 것이 잘 실행됩니다.
- 일정 시간이 지난 후 프로세스가 자신이 소유한 페이지에 액세스하려고 시도하지만 더 이상 사용 가능한 페이지가 없으면 메모리 부족 상황이 발생합니다.
- OOM 킬러는 반드시 새 페이지를 요청한 프로세스가 아닌 프로세스 중 하나를 선택하고 해당 프로세스를 종료하여 가상 메모리를 복구합니다.
예를 들어 스왑 영역이 잠자는 데몬 메모리 페이지로 채워져 있는 경우와 같이 시스템이 적극적으로 스왑하지 않는 동안에도 이런 일이 발생할 수 있습니다.
메모리를 오버커밋하지 않는 OS에서는 이런 일이 발생하지 않습니다. 이를 사용하면 무작위 프로세스가 종료되지 않지만 가상 메모리가 소진된 동안 가상 메모리를 요청하는 첫 번째 프로세스에서 오류가 반환되는 malloc(또는 유사한)이 발생합니다. 따라서 상황을 적절하게 처리할 수 있는 기회가 주어집니다. 그러나 이러한 OS에서는 사용 가능한 RAM이 아직 남아 있는 동안 시스템에 가상 메모리가 부족해지는 일이 발생할 수도 있습니다. 이는 상당히 혼란스럽고 일반적으로 오해를 받는 일입니다.
답변4
다른 답변의 또 다른 관점을 추가하기 위해 많은 VPS는 특정 서버에서 여러 가상 머신을 호스팅합니다. 모든 단일 VM에는 자체 사용을 위해 지정된 양의 RAM이 있습니다. 많은 공급자는 지정된 양 이상으로 RAM을 사용할 수 있는 "버스트 RAM"을 제공합니다. 이는 단기간 사용만을 위한 것이며, 이 연장된 시간을 초과하는 사용자는 다른 사용자가 고통받지 않도록 사용 중인 RAM의 양을 줄이기 위해 호스트가 프로세스를 종료하여 불이익을 받을 수 있습니다. 호스트 시스템이 과부하되고 있습니다.