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

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 retornou ENOMEM. O processo não é encerrado -- apenas a alocação falha. É diferente do OOM killer, e pode aparecer mesmo quando free mostra espaço. A causa nem sempre é pouca RAM; geralmente é um "limite" como contabilidade de overcommit, ulimit -v ou max_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/mmap falha com Cannot allocate memory
  • Inclui casos onde free parece ter espaço
  • Você pode ler /proc/meminfo / ulimit / sysctl (configurações permanentes requerem sudo)

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 excede CommitLimit, retorna ENOMEM. 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 (fork vs malloc/mmap), (2) se dmesg mostra OOM, (3) espaço real em free -h, (4) espaço contabil em grep -i commit /proc/meminfo, e (5) o limite do processo em ulimit -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_AS está grudado no CommitLimit, a contabilidade de overcommit é a culpada. Aumentar vm.overcommit_ratio ou adicionar swap amplia CommitLimit. 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 retorna ENOMEM. 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 mmap falha com Cannot allocate memory, você provavelmente atingiu vm.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 memory acontece 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 para posix_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+exec diretamente de um processo enorme; inicie filhos via posix_spawn ou 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 memory se 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 dmesg em busca de logs do OOM killer (se presente, família de exaustão real)?
  • [ ] Verificou espaço real em free -h available?
  • [ ] Verificou o modo atual com cat /proc/sys/vm/overcommit_memory?
  • [ ] No modo 2, verificou se Committed_AS esta próximo de CommitLimit?
  • [ ] Verificou o limite de espaço de endereço 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 memória?

Próximas leituras