![O Linux pode “ficar sem RAM”?](https://rvso.com/image/50560/O%20Linux%20pode%20%E2%80%9Cficar%20sem%20RAM%E2%80%9D%3F.png)
Eu vi vários posts na web de pessoas aparentemente reclamando sobre um VPS hospedado matando processos inesperadamente porque eles usavam muita RAM.
Como isso é possível? Achei que todos os sistemas operacionais modernos fornecessem "RAM infinita" apenas usando a troca de disco para tudo o que passa pela RAM física. Isso está correto?
O que pode estar acontecendo se um processo for "interrompido devido à pouca memória RAM"?
Responder1
O que pode estar acontecendo se um processo for "interrompido devido à pouca memória RAM"?
Às vezes é dito que o Linux, por padrão, nunca nega solicitações de mais memória do código do aplicativo - por exemplo malloc()
. 1 Isto não é de facto verdade; o padrão usa uma heurística pela qual
Supercomprometimentos óbvios de espaço de endereço são recusados. Usado para um sistema típico. Ele garante uma falha grave na alocação, ao mesmo tempo que permite overcommit para reduzir o uso de swap.
De [linux_src]/Documentation/vm/overcommit-accounting
(todas as citações são da árvore 3.11). Exatamente o que conta como uma “alocação seriamente selvagem” não é explicitado, então teríamos que consultar a fonte para determinar os detalhes. Poderíamos também usar o método experimental na nota de rodapé 2 (abaixo) para tentar obter alguma reflexão da heurística - com base nisso, minha observação empírica inicial é que em circunstâncias ideais (== o sistema está ocioso), se você não Se você não tiver nenhuma troca, você poderá alocar cerca de metade da sua RAM e, se tiver troca, receberá cerca de metade da sua RAM mais toda a sua troca. Isso é mais ou menospor processo(mas observe este limiteédinâmico e sujeito a mudanças devido ao estado, ver algumas observações na nota de rodapé 5).
Metade da sua RAM mais swap é explicitamente o padrão para o campo "CommitLimit" no arquivo /proc/meminfo
. Aqui está o que significa - e observe que na verdade não tem nada a ver com o limite que acabamos de discutir (de [src]/Documentation/filesystems/proc.txt
):
Limite de compromisso:Com base na taxa de overcommit ('vm.overcommit_ratio'), esta é a quantidade total de memória atualmente disponível para ser alocadano sistema.Este limite só é respeitado se a contabilidade estrita de supercomprometimento estiver habilitada (modo 2 em 'vm.overcommit_memory'). O CommitLimit é calculado com a seguinte fórmula: CommitLimit = ('vm.overcommit_ratio' * Physical RAM) + Swap Por exemplo, em um sistema com 1G de RAM física e 7G de swap com um 'vm.overcommit_ratio' de 30, renderia um CommitLimit de 7,3G.
O documento de contabilidade de overcommit citado anteriormente afirma que o padrão vm.overcommit_ratio
é 50. Portanto, se você sysctl vm.overcommit_memory=2
, poderá ajustar vm.covercommit_ratio (com sysctl
) e ver as consequências. 3 O modo padrão, quando CommitLimit
não é aplicado e apenas "comprometimentos excessivos óbvios de espaço de endereço são recusados", é quando vm.overcommit_memory=0
.
Embora a estratégia padrão tenha um limite heurístico por processo que evita a "alocação seriamente selvagem", ela deixa o sistema como um todo livre para ficar seriamente selvagem, em termos de alocação. 4 Isso significa que em algum momento ele pode ficar sem memória e ter que declarar falência para algum(s) processo(s) através doAssassino de OOM.
O que o assassino OOM mata? Não necessariamente o processo que pediu memória quando não havia nenhuma, já que esse não é necessariamente o processo verdadeiramente culpado e, mais importante, não necessariamente aquele que tirará o sistema mais rapidamente do problema em que se encontra.
Isto é citado deaquique provavelmente cita uma fonte 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)
*/
O que parece uma justificativa decente. No entanto, sem ser forense, o nº 5 (que é redundante do nº 1) parece uma implementação de venda difícil, e o nº 3 é redundante do nº 2. Portanto, pode fazer sentido considerar isso reduzido para 2/3 e 4.
Procurei uma fonte recente (3.11) e percebi que este comentário mudou nesse ínterim:
/**
* 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.
*/
Isso é um pouco mais explícito sobre o nº 2:"O objetivo é [eliminar] a tarefa que consome mais memória para evitar falhas subsequentes",e por implicação # 4 ("queremos eliminar a quantidade mínima de processos (um)).
Se você quiser ver o assassino OOM em ação, consulte a nota de rodapé 5.
1 Uma ilusão da qual Gilles felizmente me livrou, veja os comentários.
2 Aqui está um trecho simples de C que solicita pedaços cada vez maiores de memória para determinar quando uma solicitação de mais irá falhar:
#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;
}
Se você não conhece C, pode compilar gcc virtlimitcheck.c -o virtlimitcheck
e executar ./virtlimitcheck
. É completamente inofensivo, pois o processo não usa nenhum espaço solicitado - ou seja, ele nunca usa RAM.
Em um sistema 3.11 x86_64 com sistema de 4 GB e 6 GB de swap, falhei em ~ 7.400.000 kB; o número flutua, então talvez o estado seja um fator. Coincidentemente, isso está próximo de CommitLimit
in /proc/meminfo
, mas modificar esse via vm.overcommit_ratio
não faz nenhuma diferença. Em um sistema ARM 3.6.11 de 32 bits de 448 MB com 64 MB de swap, entretanto, falho em ~ 230 MB. Isto é interessante porque no primeiro caso a quantidade é quase o dobro da quantidade de RAM, enquanto no segundo é cerca de 1/4 disso - o que implica fortemente que a quantidade de swap é um fator. Isso foi confirmado ao desligar o swap no primeiro sistema, quando o limite de falha caiu para ~1,95 GB, uma proporção muito semelhante à pequena caixa ARM.
Mas isso é realmente por processo? Parece ser. O pequeno programa abaixo solicita um pedaço de memória definido pelo usuário e, se tiver sucesso, espera que você pressione Enter - desta forma você pode tentar várias instâncias simultâneas:
#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;
}
Cuidado, entretanto, que não se trata estritamente da quantidade de RAM e troca, independentemente do uso - consulte a nota de rodapé 5 para observações sobre os efeitos do estado do sistema.
3 CommitLimit
refere-se à quantidade de espaço de endereço permitido parao sistemaquando vm.overcommit_memory = 2. Presumivelmente, então, a quantia que você pode alocar deve ser aquela menos o que já foi confirmado, que aparentemente é o Committed_AS
campo.
Um experimento potencialmente interessante que demonstra isso é adicionar #include <unistd.h>
ao topo do virtlimitcheck.c (ver nota de rodapé 2) e um fork()
logo antes do while()
loop. Não é garantido que isso funcione conforme descrito aqui sem alguma sincronização tediosa, mas há uma boa chance de que funcione, 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.
Isso faz sentido - olhando tmp.txt em detalhes, você pode ver os processos alternando suas alocações cada vez maiores (isso é mais fácil se você colocar o pid na saída) até que um, evidentemente, reivindique o suficiente para que o outro falhe. O vencedor fica então livre para pegar tudo até CommitLimit
menos Committed_AS
.
4 Vale a pena mencionar, neste ponto, se você ainda não entende o endereçamento virtual e a paginação por demanda, que o que torna o comprometimento excessivo possível em primeiro lugar é que o que o kernel aloca para os processos da área do usuário não é memória física - éespaço de endereço virtual. Por exemplo, se um processo reserva 10 MB para algo, isso é apresentado como uma sequência de endereços (virtuais), mas esses endereços ainda não correspondem à memória física. Quando tal endereço é acessado, isso resulta em umfalha de páginae então o kernel tenta mapeá-lo na memória real para que possa armazenar um valor real. Os processos geralmente reservam muito mais espaço virtual do que realmente acessam, o que permite ao kernel fazer uso mais eficiente da RAM. No entanto, a memória física ainda é um recurso finito e quando toda ela tiver sido mapeada para o espaço de endereço virtual, algum espaço de endereço virtual deverá ser eliminado para liberar alguma RAM.
5 Primeiroum aviso: Se você tentar fazer isso com vm.overcommit_memory=0
, certifique-se de salvar seu trabalho primeiro e fechar todos os aplicativos críticos, porque o sistema ficará congelado por aproximadamente 90 segundos e alguns processos morrerão!
A idéia é executar umbomba de garfoque expira após 90 segundos, com os forks alocando espaço e alguns deles gravando grandes quantidades de dados na RAM, ao mesmo tempo reportando ao 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);
}
}
}
Compile isso gcc forkbomb.c -o forkbomb
. Primeiro, experimente sysctl vm.overcommit_memory=2
- você provavelmente obterá algo como:
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.
Nesse ambiente, esse tipo de bomba bifurcada não vai muito longe. Observe que o número em "diz N garfos" não é o número total de processos, é o número de processos na cadeia/ramificação que leva a esse.
Agora experimente com vm.overcommit_memory=0
. Se você redirecionar stderr para um arquivo, poderá fazer algumas análises brutas posteriormente, por exemplo:
> 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!
Apenas 15 processos não conseguiram alocar 1 GB – demonstrando que a heurística para overcommit_memory = 0éafetado pelo estado. Quantos processos foram? Olhando para o final do tmp.txt, provavelmente> 100.000. Agora, como posso realmente usar 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
Oito - o que novamente faz sentido, já que na época eu tinha cerca de 3 GB de RAM livres e 6 GB de swap.
Dê uma olhada nos logs do sistema depois de fazer isso. Você deverá ver as pontuações dos relatórios matadores do OOM (entre outras coisas); presumivelmente, isso está relacionado a oom_badness
.
Responder2
Isso não acontecerá com você se você carregar apenas 1G de dados na memória. E se você carregar muito mais? Por exemplo, costumo trabalhar com arquivos enormes contendo milhões de probabilidades que precisam ser carregados em R. Isso ocupa cerca de 16 GB de RAM.
Executar o processo acima em meu laptop fará com que ele comece a trocar loucamente assim que meus 8 GB de RAM forem preenchidos. Isso, por sua vez, tornará tudo mais lento porque a leitura do disco é muito mais lenta do que a leitura da RAM. E se eu tiver um laptop com 2 GB de RAM e apenas 10 GB de espaço livre? Depois que o processo tiver consumido toda a RAM, ele também preencherá o disco porque está gravando para swap e não tenho mais RAM nem espaço para fazer swap (as pessoas tendem a limitar o swap a uma partição dedicada em vez de uma swapfile exatamente por esse motivo). É aí que entra o assassino OOM e começa a matar processos.
Portanto, o sistema pode realmente ficar sem memória. Além disso, sistemas com troca intensa podem se tornar inutilizáveis muito antes de isso acontecer, simplesmente por causa das operações de E/S lentas devido à troca. Geralmente, deseja-se evitar a troca tanto quanto possível. Mesmo em servidores de última geração com SSDs rápidos, há uma clara diminuição no desempenho. No meu laptop, que possui uma unidade clássica de 7200 RPM, qualquer troca significativa essencialmente inutiliza o sistema. Quanto mais ele troca, mais lento fica. Se eu não encerrar o processo ofensivo rapidamente, tudo travará até que o assassino OOM intervenha.
Responder3
Os processos não são eliminados quando não há mais RAM, eles são eliminados quando foram enganados desta forma:
- O kernel Linux geralmente permite que os processos aloquem (isto é, reservem) uma quantidade de memória virtual que é maior do que a que está realmente disponível (parte da RAM + toda a área de troca)
- contanto que os processos acessem apenas um subconjunto das páginas reservadas, tudo funcionará bem.
- se depois de algum tempo um processo tentar acessar uma página de sua propriedade, mas nenhuma página estiver livre, ocorre uma situação de falta de memória
- O OOM killer seleciona um dos processos, não necessariamente aquele que solicitou uma nova página, e apenas o mata para recuperar a memória virtual.
Isso pode acontecer mesmo quando o sistema não estiver trocando ativamente, por exemplo, se a área de troca estiver preenchida com páginas de memória de daemons adormecidos.
Isso nunca acontece em sistemas operacionais que não sobrecarregam a memória. Com eles, nenhum processo aleatório é eliminado, mas o primeiro processo que solicita memória virtual enquanto está esgotado faz com que malloc (ou similar) retorne com erro. Assim, é dada a oportunidade de lidar adequadamente com a situação. No entanto, nesses sistemas operacionais, também pode acontecer que o sistema fique sem memória virtual enquanto ainda há RAM livre, o que é bastante confuso e geralmente mal compreendido.
Responder4
Apenas para adicionar outro ponto de vista das outras respostas, muitos VPS hospedam várias máquinas virtuais em qualquer servidor. Qualquer VM única terá uma quantidade específica de RAM para uso próprio. Muitos provedores oferecem "RAM burst", no qual podem usar RAM além da quantidade designada. Isso se destina apenas ao uso de curto prazo, e aqueles que ultrapassam esse período de tempo prolongado podem ser penalizados pelos processos de eliminação do host para reduzir a quantidade de RAM em uso, para que outros não sofram de a máquina host está sobrecarregada.