Corrigindo "Cannot allocate memory" (ENOMEM): Overcommit e Swap

Corrigindo "Cannot allocate memory" (ENOMEM): Overcommit e Swap

O que e "Cannot allocate memory"?

Conclusao: Uma chamada de sistema de alocacao de memoria (malloc/brk/mmap/fork) foi recusada pelo kernel e retornou ENOMEM. O processo nao e encerrado -- apenas a alocacao falha. E diferente do OOM killer, e pode aparecer mesmo quando free mostra espaco. A causa nem sempre e pouca RAM; geralmente e um "limite" como contabilidade de overcommit, ulimit -v ou max_map_count.

Cannot allocate memory e a mensagem padrao para o errno ENOMEM. Aparece em logs de aplicacoes, 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 e que isso se comporta de forma diferente de um kill forcado pelo OOM killer. O OOM killer encerra um processo apos a memoria se esgotar e deixa um log. ENOMEM simplesmente recusa a solicitacao no momento da alocacao, e o processo permanece vivo e recebe um erro. Por isso voce 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 alocacao Apos a memoria fisica 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/mmap falha com Cannot allocate memory
  • Inclui casos onde free parece ter espaco
  • Voce pode ler /proc/meminfo / ulimit / sysctl (configuracoes permanentes requerem sudo)

Por que aparece quando a RAM esta livre? (Como funciona o Overcommit)

Conclusao: O Linux contabiliza quanta memoria foi "committed" (prometida) na alocacao. Sob vm.overcommit_memory=2 (modo estrito), no momento em que o commit total excede CommitLimit, retorna ENOMEM. Essa "quantidade total prometida" (Committed_AS) e separada da memoria fisica livre, entao a alocacao pode ser recusada mesmo com RAM sobrando.

Quando um processo aloca memoria, paginas reais sao mapeadas apenas na primeira escrita (demand paging). O que o kernel rastreia no momento da alocacao e a "quantidade prometida para uso futuro" -- o commit. vm.overcommit_memory decide ate onde esse commit e permitido, em tres modos:

Modo Valor Comportamento
0 padrao Heuristica. Recusa apenas alocacoes 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) e visivel em /proc/meminfo:

$ grep -i commit /proc/meminfo
CommitLimit:     6029308 kB
Committed_AS:    5980124 kB

CommitLimit e o "total que voce pode prometer", e Committed_AS e "o que esta prometido atualmente". Conforme o segundo se aproxima do primeiro, novas alocacoes sao rejeitadas com ENOMEM. CommitLimit e calculado como:

CommitLimit = swap total + RAM fisica x (vm.overcommit_ratio / 100)

O vm.overcommit_ratio padrao e 50. Entao no modo 2, por padrao voce so pode prometer "swap + metade da RAM fisica". Mesmo com RAM livre, uma vez que esse limite contabil e atingido, a alocacao falha -- o classico padrao "livre mas ainda falha".

Mesmo no modo 0 (padrao), uma alocacao individual excessiva (ex: um mmap maior que RAM + swap) e recusada pela heuristica. Quando voce ve Cannot allocate memory, primeiro verifique em qual modo esta com cat /proc/sys/vm/overcommit_memory.

O que verificar primeiro? (Por onde comecar)

Conclusao: Verifique, em ordem: (1) qual operacao falhou (fork vs malloc/mmap), (2) se dmesg mostra OOM, (3) espaco real em free -h, (4) espaco contabil em grep -i commit /proc/meminfo, e (5) o limite do processo em ulimit -v. Esses cinco pontos determinam quase completamente se e exaustao real, contabilidade de overcommit ou limite de processo.

A primeira divisao e "a memoria realmente acabou, ou a solicitacao 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 decisao rapida:

Observacao Causa suspeita Va para
free available pequeno / log OOM presente Exaustao real de memoria Adicionar swap / reduzir uso
Modo 2 e Committed_AS ~ CommitLimit Limite contabil overcommit Ajustar overcommit
ulimit -v nao e unlimited Limite de mem virtual do processo ulimit / cgroup
Apenas mmap falha / processo com muitas areas max_map_count atingido max_map_count

Leia a coluna available de free (nao a coluna free). available e a memoria livre realista apos recuperar cache e e o sinal principal de exaustao real. Veja investigando pressao de memoria para detalhes.

A configuracao de overcommit e a causa? (vm.overcommit_memory)

Conclusao: Se o modo e 2 e Committed_AS esta grudado no CommitLimit, a contabilidade de overcommit e a culpada. Aumentar vm.overcommit_ratio ou adicionar swap amplia CommitLimit. Mas quanto mais flexivel a contabilidade, maior o risco real de OOM, entao a correcao raiz e revisar o uso de memoria.

Verifique o modo e ratio atuais.

$ cat /proc/sys/vm/overcommit_memory
$ cat /proc/sys/vm/overcommit_ratio
2
50

Ha 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 e deliberadamente usado onde voce "nunca quer que o OOM killer rode" (bancos de dados, embarcados, etc.). Aumentar o ratio cegamente enfraquece essa protecao de contabilidade estrita. Primeiro descubra por que Committed_AS esta grande (reservas de heap excessivas, processos demais); so se ainda faltar espaco, ajuste o ratio ou adicione swap.

Inversamente, se seu unico problema e "malloc retorna NULL", o modo 1 (sempre permitir) e uma opcao, mas as alocacoes so tem sucesso para enfrentar o OOM killer na escrita. O formato da falha apenas muda de "Cannot allocate memory" para "kill subito", entao evite mudancas casuais. Para comportamento do OOM, veja tratando eventos do OOM killer.

E um limite de processo ou usuario? (ulimit -v / cgroup)

Conclusao: Se ulimit -v (RLIMIT_AS) nao e unlimited, o espaco de endereco virtual desse processo esta limitado e retorna ENOMEM. Sob systemd, MemoryMax / limites de cgroup cortam a alocacao da mesma forma. O sistema como um todo pode ter bastante, mas o limite por processo e o que falha.

Verifique tanto o shell logado quanto o processo em execucao.

# 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 e menor do que a aplicacao precisa, essa e a causa direta. Se roda como servico systemd, verifique tambem os limites da unit.

$ systemctl show -p MemoryMax -p LimitAS myapp.service

Para dar mais memoria ao servico, configure um drop-in.

$ sudo systemctl edit myapp.service
[Service]
MemoryMax=4G
LimitAS=infinity
$ sudo systemctl daemon-reload
$ sudo systemctl restart myapp.service

Se voce limita ulimit -v no .bashrc ou similar, cada processo lancado desse shell herda o limite. Limites de container (Docker --memory) e limites de cgroup causam ENOMEM da mesma forma. Se "so acontece para um certo usuario ou servico", suspeite do limite herdado primeiro.

E se mmap retorna ENOMEM? (vm.max_map_count)

Conclusao: Se a memoria tem espaco mas apenas mmap falha com Cannot allocate memory, voce provavelmente atingiu vm.max_map_count (padrao 65530), o limite de quantas areas de mapa de memoria um processo pode manter. Isso acontece com Elasticsearch, muitas threads ou carregamento de muitas bibliotecas compartilhadas. Aumente o limite para corrigir.

Confirme que mmap e 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 esta 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 e o valor representativo que o Elasticsearch oficialmente requer. O valor certo depende da carga de trabalho, entao primeiro observe como a contagem de mapas (contagem de linhas de /proc/<pid>/maps) cresce e deixe margem acima. Aumenta-lo custa pouca memoria e tem efeitos colaterais minimos.

E se fork e o que falha?

Conclusao: fork: Cannot allocate memory acontece quando o filho precisa de uma reserva de commit tao grande quanto o pai, e a contabilidade de overcommit (modo 2) ou escassez real nao consegue reservar. Fork a partir de um processo grande e o gatilho. Mude para posix_spawn / vfork, ou revise overcommit e swap.

fork compartilha memoria copy-on-write, mas para contabilidade tenta reservar "um commit para as paginas graváveis do pai" em nome do filho. Se o pai ocupa varios GB, o commit pode exceder CommitLimit naquele instante e retornar ENOMEM.

$ ./big_parent
fork: Cannot allocate memory

Ha tres direcoes para corrigir:

  • Design: Nao faca fork+exec diretamente de um processo enorme; inicie filhos via posix_spawn ou um processo auxiliar pequeno
  • Contabilidade: Se overcommit esta no modo 2, aumente o ratio / adicione swap para ampliar CommitLimit
  • Flexibilizar: vm.overcommit_memory=1 (sempre permitir) remove a verificacao de reserva (um trade-off contra risco de OOM)

Para o mesmo fork, se o erro e Resource temporarily unavailable (EAGAIN) a causa e o limite de contagem de processos / threads, nao memoria, e a correcao e 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)

Conclusao: Cannot allocate memory se divide em duas familias: "escassez real" e "recusado por um limite". Separe-as pela existencia de log OOM, depois percorra contabilidade de overcommit (CommitLimit/Committed_AS) -> limites de processo (ulimit -v/cgroup) -> max_map_count. Flexibilizar contabilidade ou limites e sintomatico; a raiz e o equilibrio com o uso de memoria.

  • [ ] Verificou dmesg em busca de logs do OOM killer (se presente, familia de exaustao real)?
  • [ ] Verificou espaco real em free -h available?
  • [ ] Verificou o modo atual com cat /proc/sys/vm/overcommit_memory?
  • [ ] No modo 2, verificou se Committed_AS esta proximo de CommitLimit?
  • [ ] Verificou o limite de espaco de endereco em ulimit -v / /proc/<pid>/limits?
  • [ ] Verificou MemoryMax / LimitAS no systemd / cgroup / container?
  • [ ] Para falhas de mmap, comparou vm.max_map_count com a contagem de mapas?
  • [ ] Para falhas de fork, diferenciou ENOMEM de EAGAIN?
  • [ ] Antes de flexibilizar (ratio / swap / limites), investigou a causa real do alto uso de memoria?

Proximas leituras