Corrigindo "Cannot allocate memory" (ENOMEM): Overcommit e Swap
O que é "Cannot allocate memory"?
Conclusão: Uma chamada de sistema de alocação de memória (
malloc/brk/mmap/fork) foi recusada pelo kernel e retornouENOMEM. O processo não é encerrado -- apenas a alocação falha. É diferente do OOM killer, e pode aparecer mesmo quandofreemostra espaço. A causa nem sempre é pouca RAM; geralmente é um "limite" como contabilidade de overcommit,ulimit -voumax_map_count.
Cannot allocate memory é a mensagem padrão para o errno ENOMEM. Aparece em logs de aplicações, dmesg ou strace:
$ ./myapp fork: Cannot allocate memory $ python3 -c 'x = bytearray(8*1024**3)' MemoryError $ strace -f ./myapp 2>&1 | grep ENOMEM mmap(NULL, 1073741824, ...) = -1 ENOMEM (Cannot allocate memory)
O ponto chave é que isso se comporta de forma diferente de um kill forçado pelo OOM killer. O OOM killer encerra um processo após a memória se esgotar e deixa um log. ENOMEM simplesmente recusa a solicitação no momento da alocação, e o processo permanece vivo e recebe um erro. Por isso você pode ver Cannot allocate memory sem Out of memory: Killed process no dmesg.
| Aspecto | Cannot allocate memory (ENOMEM) | OOM killer |
|---|---|---|
| Quando ocorre | Na chamada de sistema de alocação | Após a memória física se esgotar |
| Processo | Sobrevive (recebe um erro) | Encerrado com SIGKILL |
| Log no dmesg | Nenhum | Out of memory: Killed process |
| Causa principal | Contabilidade de overcommit / limites | RAM + swap esgotados |
Premissas (ambiente alvo)
- SO: Ubuntu / Linux geral
- Sintoma:
malloc/fork/mmapfalha comCannot allocate memory - Inclui casos onde
freeparece ter espaço - Você pode ler
/proc/meminfo/ulimit/sysctl(configurações permanentes requeremsudo)
Por que aparece quando a RAM está livre? (Como funciona o Overcommit)
Conclusão: O Linux contabiliza quanta memória foi "committed" (prometida) na alocação. Sob
vm.overcommit_memory=2(modo estrito), no momento em que o commit total excedeCommitLimit, retornaENOMEM. Essa "quantidade total prometida" (Committed_AS) é separada da memória física livre, então a alocação pode ser recusada mesmo com RAM sobrando.
Quando um processo aloca memória, páginas reais são mapeadas apenas na primeira escrita (demand paging). O que o kernel rastreia no momento da alocação é a "quantidade prometida para uso futuro" -- o commit. vm.overcommit_memory decide até onde esse commit é permitido, em três modos:
| Modo | Valor | Comportamento |
|---|---|---|
| 0 | padrão | Heurística. Recusa apenas alocações obviamente excessivas |
| 1 | - | Sempre permite. malloc quase nunca retorna NULL (OOM pode disparar depois) |
| 2 | - | Contabilidade estrita. ENOMEM quando commit total excede CommitLimit |
O limite no modo estrito (2) é visível em /proc/meminfo:
$ grep -i commit /proc/meminfo
CommitLimit: 6029308 kB Committed_AS: 5980124 kB
CommitLimit é o "total que você pode prometer", e Committed_AS é "o que está prometido atualmente". Conforme o segundo se aproxima do primeiro, novas alocações são rejeitadas com ENOMEM. CommitLimit é calculado como:
CommitLimit = swap total + RAM fisica x (vm.overcommit_ratio / 100)
O vm.overcommit_ratio padrão é 50. Então no modo 2, por padrão você só pode prometer "swap + metade da RAM física". Mesmo com RAM livre, uma vez que esse limite contabil é atingido, a alocação falha -- o clássico padrão "livre mas ainda falha".
Mesmo no modo 0 (padrão), uma alocação individual excessiva (ex: um mmap maior que RAM + swap) é recusada pela heurística. Quando você vê Cannot allocate memory, primeiro verifique em qual modo está com cat /proc/sys/vm/overcommit_memory.
O que verificar primeiro? (Por onde começar)
Conclusão: Verifique, em ordem: (1) qual operação falhou (
forkvsmalloc/mmap), (2) sedmesgmostra OOM, (3) espaço real emfree -h, (4) espaço contabil emgrep -i commit /proc/meminfo, e (5) o limite do processo emulimit -v. Esses cinco pontos determinam quase completamente se é exaustão real, contabilidade de overcommit ou limite de processo.
A primeira divisão é "a memória realmente acabou, ou a solicitação foi recusada por um limite?"
# 1. Algum sinal de que o OOM killer rodou (se sim, e o lado de exaustao real) $ dmesg -T | grep -i -E 'out of memory|killed process' # 2. Espaco real de memoria fisica / swap $ free -h # 3. Espaco contabil de overcommit (importante no modo 2) $ grep -i -E 'commitlimit|committed_as' /proc/meminfo # 4. Limite de memoria virtual deste shell / processo $ ulimit -v
Tabela de decisão rápida:
| Observação | Causa suspeita | Vá para |
|---|---|---|
free available pequeno / log OOM presente |
Exaustão real de memória | Adicionar swap / reduzir uso |
Modo 2 e Committed_AS ~ CommitLimit |
Limite contabil overcommit | Ajustar overcommit |
ulimit -v não é unlimited |
Limite de mem virtual do processo | ulimit / cgroup |
Apenas mmap falha / processo com muitas áreas |
max_map_count atingido |
max_map_count |
Leia a coluna available de free (não a coluna free). available é a memória livre realista após recuperar cache e é o sinal principal de exaustão real. Veja investigando pressão de memória para detalhes.
A configuração de overcommit é a causa? (vm.overcommit_memory)
Conclusão: Se o modo é 2 e
Committed_ASestá grudado noCommitLimit, a contabilidade de overcommit é a culpada. Aumentarvm.overcommit_ratioou adicionar swap ampliaCommitLimit. Mas quanto mais flexível a contabilidade, maior o risco real de OOM, então a correção raiz é revisar o uso de memória.
Verifique o modo e ratio atuais.
$ cat /proc/sys/vm/overcommit_memory $ cat /proc/sys/vm/overcommit_ratio
2 50
Há duas formas de ampliar CommitLimit: aumentar o ratio ou adicionar swap.
# Aumentar o ratio para 80% (permitir prometer RAM x 80% + swap) $ sudo sysctl -w vm.overcommit_ratio=80 $ grep -i commitlimit /proc/meminfo # confirmar aplicacao
Tornar permanente.
$ echo 'vm.overcommit_ratio = 80' | sudo tee /etc/sysctl.d/99-overcommit.conf $ sudo sysctl --system
O modo 2 é deliberadamente usado onde você "nunca quer que o OOM killer rode" (bancos de dados, embarcados, etc.). Aumentar o ratio cegamente enfraquece essa proteção de contabilidade estrita. Primeiro descubra por que Committed_AS está grande (reservas de heap excessivas, processos demais); só se ainda faltar espaço, ajuste o ratio ou adicione swap.
Inversamente, se seu único problema é "malloc retorna NULL", o modo 1 (sempre permitir) é uma opção, mas as alocações só têm sucesso para enfrentar o OOM killer na escrita. O formato da falha apenas muda de "Cannot allocate memory" para "kill subito", então evite mudanças casuais. Para comportamento do OOM, veja tratando eventos do OOM killer.
É um limite de processo ou usuário? (ulimit -v / cgroup)
Conclusão: Se
ulimit -v(RLIMIT_AS) não éunlimited, o espaço de endereço virtual desse processo está limitado e retornaENOMEM. Sob systemd,MemoryMax/ limites de cgroup cortam a alocação da mesma forma. O sistema como um todo pode ter bastante, mas o limite por processo é o que falha.
Verifique tanto o shell logado quanto o processo em execução.
# Limite de memoria virtual do shell atual (KB; unlimited significa sem limite) $ ulimit -v # Verificar o limite de um processo em execucao diretamente (por PID) $ cat /proc/<pid>/limits | grep -i 'address space'
Max address space 2147483648 2147483648 bytes
Se ulimit -v é menor do que a aplicação precisa, essa é a causa direta. Se roda como serviço systemd, verifique também os limites da unit.
$ systemctl show -p MemoryMax -p LimitAS myapp.service
Para dar mais memória ao serviço, configure um drop-in.
$ sudo systemctl edit myapp.service
[Service] MemoryMax=4G LimitAS=infinity
$ sudo systemctl daemon-reload $ sudo systemctl restart myapp.service
Se você limita ulimit -v no .bashrc ou similar, cada processo lançado desse shell herda o limite. Limites de container (Docker --memory) e limites de cgroup causam ENOMEM da mesma forma. Se "só acontece para um certo usuário ou serviço", suspeite do limite herdado primeiro.
E se mmap retorna ENOMEM? (vm.max_map_count)
Conclusão: Se a memória tem espaço mas apenas
mmapfalha comCannot allocate memory, você provavelmente atingiuvm.max_map_count(padrão 65530), o limite de quantas áreas de mapa de memória um processo pode manter. Isso acontece com Elasticsearch, muitas threads ou carregamento de muitas bibliotecas compartilhadas. Aumente o limite para corrigir.
Confirme que mmap é a causa com strace.
$ strace -f -e trace=mmap ./myapp 2>&1 | grep ENOMEM
mmap(NULL, 262144, PROT_READ|PROT_WRITE, ...) = -1 ENOMEM (Cannot allocate memory)
Compare a contagem atual de mapas do processo com o limite.
# Numero atual de areas de mapa $ wc -l < /proc/<pid>/maps # Limite do sistema $ sysctl vm.max_map_count
65530 65530
Se a contagem está grudada no limite, aumente.
$ sudo sysctl -w vm.max_map_count=262144 $ echo 'vm.max_map_count = 262144' | sudo tee /etc/sysctl.d/99-max-map-count.conf $ sudo sysctl --system
262144 é o valor representativo que o Elasticsearch oficialmente requer. O valor certo depende da carga de trabalho, então primeiro observe como a contagem de mapas (contagem de linhas de /proc/<pid>/maps) cresce e deixe margem acima. Aumentá-lo custa pouca memória e tem efeitos colaterais mínimos.
E se fork é o que falha?
Conclusão:
fork: Cannot allocate memoryacontece quando o filho precisa de uma reserva de commit tão grande quanto o pai, e a contabilidade de overcommit (modo 2) ou escassez real não consegue reservar. Fork a partir de um processo grande é o gatilho. Mude paraposix_spawn/vfork, ou revise overcommit e swap.
fork compartilha memória copy-on-write, mas para contabilidade tenta reservar "um commit para as páginas graváveis do pai" em nome do filho. Se o pai ocupa vários GB, o commit pode exceder CommitLimit naquele instante e retornar ENOMEM.
$ ./big_parent fork: Cannot allocate memory
Há três direções para corrigir:
- Design: Não faça
fork+execdiretamente de um processo enorme; inicie filhos viaposix_spawnou um processo auxiliar pequeno - Contabilidade: Se overcommit está no modo 2, aumente o ratio / adicione swap para ampliar
CommitLimit - Flexibilizar:
vm.overcommit_memory=1(sempre permitir) remove a verificação de reserva (um trade-off contra risco de OOM)
Para o mesmo fork, se o erro é Resource temporarily unavailable (EAGAIN) a causa é o limite de contagem de processos / threads, não memória, e a correção é totalmente diferente. Para problemas de ulimit -u / TasksMax / pid_max, veja corrigindo "fork: Resource temporarily unavailable". Separe seu caminho pela mensagem final (Cannot allocate memory vs Resource temporarily unavailable).
Como corrigir de vez? (Checklist)
Conclusão:
Cannot allocate memoryse divide em duas famílias: "escassez real" e "recusado por um limite". Separe-as pela existência de log OOM, depois percorra contabilidade de overcommit (CommitLimit/Committed_AS) -> limites de processo (ulimit -v/cgroup) ->max_map_count. Flexibilizar contabilidade ou limites é sintomático; a raiz é o equilibrio com o uso de memória.
- [ ] Verificou
dmesgem busca de logs do OOM killer (se presente, família de exaustão real)? - [ ] Verificou espaço real em
free -havailable? - [ ] Verificou o modo atual com
cat /proc/sys/vm/overcommit_memory? - [ ] No modo 2, verificou se
Committed_ASesta próximo deCommitLimit? - [ ] Verificou o limite de espaço de endereço em
ulimit -v//proc/<pid>/limits? - [ ] Verificou
MemoryMax/LimitASno systemd / cgroup / container? - [ ] Para falhas de
mmap, comparouvm.max_map_countcom a contagem de mapas? - [ ] Para falhas de
fork, diferenciouENOMEMdeEAGAIN? - [ ] Antes de flexibilizar (ratio / swap / limites), investigou a causa real do alto uso de memória?