TL;DR: Duas causas raiz no mesmo cluster no mesmo dia: (1) overcommit de memória deixou o Committed_AS em 16,7 GB com apenas 1 GB de swap, causando um deadlock do kswapd no flush de inode ext4 — o inode precisava de memória para escrever, mas a recuperação de memória precisava do inode ser flushed primeiro; o ciclo nunca disparou o hung_task_panic de forma limpa, o kernel travou antes de conseguir gravar o crash dump; (2) um stall de 7 segundos no fdatasync do NVMe (Crucial P3 QLC com 162/200 TBW escritos, 451 entradas de erro no firmware P9CR30A) cascateou pelo consenso raft do etcd até um timeout da liveness probe do engine-image do Longhorn, derrubando todas as 20 sessões iSCSI simultaneamente e disparando um hung-task panic — confirmado pelo kdump, já que o journald sozinho mostrava apenas o efeito, não a cadeia de causa. Resolvido atualizando o firmware do NVMe para P9CR30D, elevando o engine-replica-timeout de 8 s para 30 s, reduzindo réplicas de 3 para 2 e concurrent-replica-rebuild-per-node-limit para 1, e adicionando um swapfile de 8 GB em cada nó.
Duas causas raiz travaram um cluster k3s no mesmo dia — overcommit de memória deadlockando o kswapd no flush de inode ext4, e um pico de fdatasync no NVMe cascateando pelo etcd até uma falha de liveness probe do Longhorn que derrubou todas as sessões iSCSI simultaneamente. Ambas produziram sintoma idêntico: um nó que não respondia a SSH, SysRq ou SIGKILL e precisava de reboot pelo botão de energia. O kdump foi a única ferramenta que as separou — o primeiro travamento não deixou nenhum registro de crash.
Três nós. Travamentos completos ocasionais. Sem SSH, sem ping, sem SysRq — a única saída era segurar o botão de energia. O próprio watchdog do kernel (hung_task_panic=1, panic=5) às vezes reinicializava o nó automaticamente, às vezes não. Sem padrão, sem gatilho consistente.
Este post cobre duas causas raiz encontradas no mesmo cluster no mesmo dia — pressão de memória e stalls de iSCSI do Longhorn — como foram distinguidas, o que foi tentado e o que foi aplicado. O lado do SMB da história está coberto separadamente em Mounts SMB no Kubernetes Vão Travar Seu Nó.
Índice
- O cluster
- Primeiro travamento: kswapd batendo numa parede
- Segundo travamento: o que o kdump confirmou?
- O gatilho real: stall de fdatasync no NVMe
- Saúde do NVMe e firmware
- Longhorn em 1 Gbps
- O travamento do SMB — um problema separado
- Swap
- Dois modos de falha, um sintoma, uma ferramenta
- Referências
O cluster
Três nós Lenovo ThinkCentre M720q (Intel i5-8500T, 16 GB RAM, 1 TB NVMe cada) rodando k3s no Debian 13. Longhorn para armazenamento persistente, driver SMB CSI para um volume de mídia compartilhado respaldado por um servidor Samba em um Raspberry Pi NAS. Todos os nós conectados via 1 Gbps.
Parâmetros de kernel já configurados antes desta investigação:
nvme_core.default_ps_max_latency_us=0
nvme_core.force_apst=0
pcie_aspm=off
intel_idle.max_cstate=1
processor.max_cstate=1
nmi_watchdog=1
softlockup_panic=1
hung_task_panic=1
panic=5
crashkernel=512M-:192M
A reserva crashkernel é o que tornou o kdump possível e acabou sendo essencial.
Primeiro travamento: kswapd batendo numa parede
Resposta direta: O primeiro travamento não deixou panic nem kdump —
journalctl -b -1mostrou boot limpo às 07:58, depois silêncio. A última mensagem foi um aviso de kernel às 17:11 mostrando okswapd0preso emshrink_node → shrink_slab → super_cache_scan → ext4_dirty_inode → __getblk_slow: tentando recuperar memória mas bloqueado esperando o ext4 fazer flush de um inode, que por sua vez precisava de memória para completar a escrita. Um deadlock clássico de pressão de memória. O/proc/meminfomostravaCommitted_AS: 16780620 kBcontra 16 GB de RAM e apenas 1 GB de swap — já em overcommit. Ohung_task_panic=1nunca disparou porque as tarefas bloqueadas não ficaram no estado qualificado por tempo suficiente antes do nó ficar sem resposta. Sem panic disparado, o kdump nunca ativou, então não houve registro de crash — só essa linha de aviso e depois nada até o reboot manual.
O aviso: kswapd0 travado recuperando memória
O primeiro travamento do dia não deixou nenhum panic nos logs. journalctl -b -1 mostrou o sistema iniciando normalmente às 07:58, depois nada até o próximo boot. A última mensagem do kernel antes do silêncio foi este aviso às 17:11:
kernel: WARNING: CPU: 5 PID: 80 at mm/page_alloc.c:4304 __alloc_pages_slowpath.constprop.0+0xb93/0xdc0
kernel: CPU: 5 UID: 0 PID: 80 Comm: kswapd0 ...
O call trace mostrou kswapd0 descendo para shrink_node → shrink_slab → super_cache_scan → ext4_dirty_inode → __getblk_slow — tentando recuperar memória mas bloqueado esperando o ext4 fazer flush de um inode, que por sua vez precisava de memória para fazer o I/O. Um deadlock de pressão de memória.
O sistema tinha 16 GB de RAM e /proc/meminfo mostrava Committed_AS: 16780620 kB — já em overcommit com apenas 1 GB de swap. O kswapd ficou sem margem de manobra e bloqueou de um jeito que não satisfez o hung_task_panic rápido o suficiente antes que outra coisa falhasse. O nó ficou sem resposta e precisou de segurar o botão de energia.
Por que o hung_task_panic nunca disparou
hung_task_panic=1 não disparou porque as tarefas não estavam bloqueando no estado certo por tempo suficiente antes da cascata. O kernel estava travado mas tecnicamente não atingia o limiar de hung task. Sem kdump, não havia registro do crash.
Segundo travamento: o que o kdump confirmou?
Resposta direta: Três minutos depois do reboot manual, o nó travou de novo — desta vez o
hung_task_panicdisparou e o kdump capturou um coredump. O dmesg mostroujbd2/sdf-8(a thread de journal ext4 de uma PVC respaldada pelo Longhorn) e um processopostgresbloqueados por mais de 120 segundos, seguido de um hung-task panic. A conexão iSCSI entre o nó e sua réplica Longhorn havia estagnado; a thread de journal não conseguia escrever, o postgres não conseguia fazer fsync, e ambos ficaram em estado D até o limiar de 120 segundos disparar o panic configurado. Opanic=5então reinicializou o nó automaticamente e o kdump já tinha capturado o estado. O contraste com o primeiro travamento: aquele degradou gradualmente e nunca atingiu o limiar de hung task de forma limpa; este bloqueou dois processos além do limiar no mesmo instante, motivo pelo qual só este travamento produziu um crash dump utilizável.
A captura do dmesg: jbd2 e postgres bloqueados em estado D
Três minutos depois que o nó voltou do reboot manual, travou de novo. Desta vez o hung_task_panic disparou e o kdump capturou um coredump antes do reboot. O dmesg do capture kernel em /var/crash/202606261724/dmesg.202606261724:
[ 565.689850] systemd-journald[1]: Main process exited, code=killed, status=6/ABRT
[ 604.960363] INFO: task jbd2/sdf-8:19456 blocked for more than 120 seconds.
[ 604.960592] INFO: task postgres:20923 blocked for more than 120 seconds.
[ 604.961048] Kernel panic - not syncing: hung_task: blocked tasks
jbd2/sdf-8 é a thread de journal ext4 para /dev/sdf. lsblk mostrou que sdf era um disco virtual Longhorn (PVC pvc-95ce3628, 10 GB) montado dentro de um pod rodando PostgreSQL. A conexão iSCSI entre o nó e a réplica Longhorn havia estagnado. A thread de journal não conseguia escrever, o postgres não conseguia fazer fsync, ambos entraram em estado D (sleep não-interruptível), e após 120 segundos o kernel entrou em panic como configurado.
Desta vez panic=5 reinicializou o nó automaticamente após 5 segundos, e o kdump capturou o estado primeiro. A diferença do primeiro travamento: o primeiro foi pressão de memória que degradou gradualmente sem atingir o limiar de 120 segundos de hung task de forma limpa; o segundo foi um timeout duro de iSCSI que bloqueou dois processos além do limiar ao mesmo tempo.
Por que este travamento disparou panic e o primeiro não?
Por que o primeiro travamento precisou de reboot físico? Mais provavelmente porque o próprio alocador de memória do kernel estava envolvido no caminho de falha — quando o kswapd em si está bloqueado, a maquinaria de panic não consegue alocar memória para escrever o crash dump ou executar a sequência de reboot de forma limpa.
O gatilho real: stall de fdatasync no NVMe
Resposta direta: O kdump capturou o efeito (stall de iSCSI, hung-task panic), mas não a causa raiz. Voltando mais nos logs do boot de ~9h, apareceu a cadeia real: um
fdatasyncdo WAL do etcd travou por 7 segundos no NVMe. Essa única escrita lenta parou o consenso raft do etcd por ~1,8 segundos, o que fez o liveness probe do podengine-imagedo Longhorn estourar seu timeout de 4 segundos, o que fez o kubelet matar o container, o que fez o podinstance-managerperder seus targets iSCSI, o que derrubou todas as 20 sessões iSCSI do nó simultaneamente. Todo processo com filesystem ext4 sobre esses discos iSCSI agora inexistentes entrou em estado D, e 120 segundos depois o kernel entrou em panic. A falha não começou no Longhorn nem na rede — começou com o NVMe não completando uma escrita rápido o suficiente, e o efeito cascateou por cinco camadas antes de aparecer como um nó travado.
O kdump capturou o efeito, mas não a causa. Voltando mais nos logs do boot de ~9h (07:58–17:14), ficou claro o que iniciou a cadeia:
17:11:57 k3s[1046]: slow fdatasync: took 7.112218614s expected-duration: 1s
17:11:59 ExecSync timeout 4s: /data/longhorn version --client-only ← liveness probe FALHOU
17:11:59 ExecSync timeout 4s: ls /data/longhorn && /data/longhorn version
O fdatasync do WAL do etcd travou por 7 segundos no NVMe. Essa única pausa de I/O causou:
- Consensus raft do etcd parou por ~1,8 segundos em todos os requests
- O liveness probe do pod
engine-imagedo Longhorn (/data/longhorn version --client-only, timeout de 4s) estourou o prazo - O kubelet matou o container
engine-image - Sem o binário do engine, o pod
instance-managerperdeu seus targets iSCSI - Todas as 20 sessões iSCSI caíram simultaneamente
- Processos com filesystems ext4 sobre esses discos iSCSI entraram em estado D
- 120 segundos depois:
hung_task_panic
O stall de iSCSI não era um problema do Longhorn ou de rede na raiz — era o NVMe não respondendo rápido o suficiente para uma escrita, o que cascateou pelo etcd, pelo liveness probe do Longhorn e eventualmente travou o nó.
Saúde do NVMe e firmware
Resposta direta: O
smartctlno Crucial CT1000P3SSD8 mostrou 162 TB escritos contra um nominal de 200 TBW (81% do endurance), 451 entradas de log de erro e 198 eventos de throttling térmico. O P3 é flash QLC — perto do teto de endurance de escrita, throttling térmico e picos de latência no fdatasync são o comportamento esperado do controlador, não uma falha. O firmware eraP9CR30A; a versão atualP9CR30Dmenciona especificamente melhorias no tratamento de erros e fixes de enumeração BIOS da Lenovo, ambos diretamente relevantes. A Crucial não publica um download.binpara esse modelo na página de suporte, mas sua API de verificação de firmware retorna uma URL direta. A atualização se aplica vianvme-cliao único slot de firmware comaction=3(ativa imediatamente, sem reset necessário) — sem downtime, mas também sem slot de fallback se o download vier corrompido, então verificar o hash do arquivo antes de atualizar importa mais que o normal.
Dados SMART: estresse térmico perto do teto de endurance
smartctl -a no Crucial CT1000P3SSD8 revelou que o disco estava sob estresse térmico:
Percentage Used: 70% # 162 TB escritos dos 200 TBW nominais
Unsafe Shutdowns: 167 # cada crash adiciona um
Error Information Log Entries: 451 # todos "Invalid Field in Command"
Warning Comp. Temperature Time: 2481 # minutos acima de 85°C (threshold de warning)
Thermal Temp. 1 Transition Count: 198 # eventos de throttling térmico
162 TBW em um drive QLC com nominal de 200 TBW. O P3 é flash QLC. Quando um drive QLC se aproxima do teto de endurance e começa a fazer throttling térmico, picos de latência no fdatasync são exatamente o que acontece — o controlador desacelera para proteger o NAND, e qualquer escrita síncrona pendente espera.
O firmware em ambos os nós Crucial era P9CR30A. A versão atual é P9CR30D, que a API de atualização da Crucial descreve como incluindo “melhorias nos mecanismos de tratamento de erros” — relevante dadas as 451 entradas de log de erros, e o fato de que esses são Lenovo ThinkCentre M720q (o changelog também menciona fixes de enumeração BIOS da Lenovo).
Atualizando o firmware sem reiniciar
A atualização no Linux requer nvme-cli. A Crucial não distribui arquivos .bin standalone na página de suporte, mas a API de firmware retorna uma URL de download direto:
# consultar a API para obter a URL de download
curl 'https://www.orderingmemory.com/firmware/firmware.aspx?key=P3&fw=P9CR30A&fwType=CR'
# retorna: manualurl → https://www.micron.com/content/dam/micron/global/public/ssdtool/firmware/p3/p9cr30d.zip
apt-get install -y nvme-cli unzip
wget https://www.micron.com/content/dam/micron/global/public/ssdtool/firmware/p3/p9cr30d.zip
unzip p9cr30d.zip
# verificar slot atual
nvme fw-log /dev/nvme0
# carregar firmware no buffer do controlador
nvme fw-download --fw=P9CR30D/1.bin /dev/nvme0
# commit no slot 1, action=3 = ativar imediatamente (sem reset necessário)
nvme fw-commit --slot=1 --action=3 /dev/nvme0
# verificar
nvme fw-log /dev/nvme0
O campo Firmware Updates (0x12): 1 Slot, no Reset required no SMART significa que action=3 funciona sem reiniciar. A atualização é aplicada no slot de firmware ativo in-place. Ambos os nós Crucial do cluster foram atualizados assim com zero downtime.
Um slot significa uma chance. Se o download estiver corrompido ou o binário errado, não há slot de fallback. Verifique o hash do zip antes de atualizar, e não interrompa o comando fw-download após iniciá-lo.
Longhorn em 1 Gbps
Resposta direta: Depois do segundo reboot, todos os 28 volumes Longhorn apareceram degradados — não um volume ruim isolado, pressão de replicação em todo o cluster. Os 3 nós compartilhavam um único link de 1 Gbps para tráfego de pods, control-plane do Kubernetes e réplicas do Longhorn ao mesmo tempo. Rebuilds de réplica saturavam esse link, heartbeats iSCSI davam timeout, e o engine marcava réplicas como falhas com apenas 8 segundos de timeout para se recuperar de bursts momentâneos — apertado demais para um link congestionado. Três configurações resolveram:
engine-replica-timeoutelevado de 8 s para 30 s,concurrent-replica-rebuild-per-node-limitreduzido para 1, edefault-replica-countreduzido de 3 para 2 (com os 28 volumes existentes atualizados em seguida). O trade-off é real: com 2 réplicas, um volume fica em risco se um nó falhar durante um rebuild. Para um homelab num link de 1 Gbps compartilhado sem rede de armazenamento dedicada, esse é o equilíbrio certo.
Causa raiz: rebuild de réplica saturando o link compartilhado
Após o segundo reboot, todos os 28 volumes Longhorn apareceram como degradados. O stall de iSCSI não foi uma anomalia de um único volume — era pressão de replicação em todo o cluster.
A configuração tinha 3 réplicas por volume em 3 nós, todos compartilhando um link de 1 Gbps que também carrega tráfego de pods e comunicação do control-plane do Kubernetes. Quando um ou mais volumes precisavam de rebuild de réplica, o tráfego de rebuild saturava o link, os heartbeats iSCSI excediam o timeout e o engine marcava réplicas como falhas. O timeout do engine para réplicas era de 8 segundos — curto demais para um link de 1 Gbps congestionado se recuperar de bursts momentâneos.
As configurações que resolveram
Três configurações foram alteradas:
kubectl patch settings.longhorn.io engine-replica-timeout \
-n longhorn-system --type merge -p '{"value":"30"}'
kubectl patch settings.longhorn.io concurrent-replica-rebuild-per-node-limit \
-n longhorn-system --type merge -p '{"value":"1"}'
kubectl patch settings.longhorn.io default-replica-count \
-n longhorn-system --type merge -p '{"value":"2"}'
Nota: engine-replica-timeout aplica-se apenas a engines v1. O intervalo aceito é de 8 a 30 segundos.
Depois, todos os 28 volumes existentes foram atualizados de 3 para 2 réplicas:
for vol in $(kubectl get volumes.longhorn.io -n longhorn-system -o jsonpath='{.items[*].metadata.name}'); do
kubectl patch volumes.longhorn.io $vol -n longhorn-system --type merge -p '{"spec":{"numberOfReplicas":2}}'
done
O trade-off: com 2 réplicas, um volume fica em risco se um nó falhar durante um rebuild ativo. Para um homelab em 1 Gbps sem rede de armazenamento dedicada, este é o equilíbrio certo.
Uma rede de armazenamento dedicada (a feature “Storage Network” do Longhorn via Multus CNI) isolaria o tráfego de replicação do tráfego de pods completamente, mas requer uma segunda NIC ou VLAN em cada nó. Não disponível aqui.
O travamento do SMB — um problema separado
Durante a investigação do problema do Longhorn, os logs de boot também mostraram erros CIFS de mais cedo no dia. O namespace do media server monta um compartilhamento SMB de 1 TB via driver SMB CSI, e quando o NAS fica offline qualquer pod fazendo I/O naquele mount entra em estado D — o que com pods suficientes simultâneos é suficiente para disparar hung_task_panic por conta própria.
A correção e tudo o que não funciona (opções soft do estilo NFS, echo_retries como opção de mount, probes de liveness HTTP) estão cobertos em detalhes em Mounts SMB no Kubernetes Vão Travar Seu Nó.
Swap
Ambos os travamentos foram precedidos ou acompanhados por pressão de memória. Os três nós tinham apenas ~1 GB de swap (uma partição restante do instalador do Debian). Um swapfile de 8 GB foi adicionado a cada nó:
dd if=/dev/zero of=/swapfile bs=1M count=8192
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw,pri=-1 0 0' >> /etc/fstab
Isso não corrige as causas raiz mas dá ao kernel mais margem antes que o Committed_AS transborde e o kswapd comece a bater nas paredes de memória durante a recuperação.
Errata (14/08/2026): Isto estava errado. O swapfile não só deixou de corrigir a causa raiz — ele estava mascarando ela ativamente, e depois causou uma segunda rodada, pior, de travamentos totais nos nós. O
kube-reservednesses nós estava em 1 GB enquanto o k3s server (com etcd) media 1,4–1,6 GB de RSS, então o kubelet estava agendando como se o control plane usasse menos memória do que realmente usava. Oeviction-hardnão tinhaeviction-minimum-reclaimpareado, então a eviction podia disparar e retriggar imediatamente. Com swap na jogada, em vez do kubelet evictar pods proativamente no limiar configurado, o kernel paginava sob pressão e entrava em thrashing — que é exatamente como um nó totalmente sem resposta (sem SSH, sem ping) se comporta, de forma mais confiável do que um OOM-kill limpo. O swap dava mais margem aoCommitted_AS, mas o mecanismo que ele deveria ajudar — a recuperação dokswapdsob pressão — é o mesmo mecanismo que depois travava o nó.Correção:
swapoff -a, swap removido do/etc/fstab(tanto o swapfile de 8 GB deste post quanto a partição residual de ~1 GB do instalador), e as reservas do kubelet corrigidas —kubelet-arg: - "system-reserved=cpu=500m,memory=1Gi" - "kube-reserved=cpu=500m,memory=2Gi" - "eviction-hard=memory.available<750Mi" - "eviction-minimum-reclaim=memory.available=500Mi"A própria orientação do Kubernetes é rodar com swap desligado — o gerenciamento de memória do kubelet assume isso. Não faça o que este post fez; dimensione
kube-reserved/system-reservedpelo que é realmente medido, pareieeviction-hardcomeviction-minimum-reclaim, e deixe o swap desabilitado.
Dois modos de falha, um sintoma, uma ferramenta
Resposta direta: Os dois travamentos pareciam idênticos por fora — sem SSH, sem ping, sem SysRq, botão de energia como única saída — mas tinham causas raiz sem relação, ambas exigindo correção independente.
crashkernel=512M-:192Mfoi o que tornou o segundo travamento diagnosticável; o primeiro mostra o limite real do kdump, já que ele exige que o subsistema de memória do kernel ainda esteja funcional o suficiente para gravar um dump, o que não é garantido quando a falha está justamente no subsistema de memória. Atualização de firmware do NVMe e ajuste de timeout de iSCSI tratam a cascata disco-etcd-Longhorn; swap e limites de overcommit de memória tratam a pressão separada que deixou o kernel sem margem sob carga. Nenhum dos dois fixes toca o caminho de falha do outro. O Crucial P3 em 162/200 TBW segue como o risco em curso —Percentage UsedeError Information Log Entriessão as métricas para acompanhar semanalmente, com substituição planejada antes dos 90%, não depois do próximo stall.
Os dois travamentos se apresentaram de forma idêntica por fora: sem SSH, sem ping, sem SysRq, botão de energia como única saída. Sem kdump, ambos teriam sido atribuídos à mesma causa vaga e tratados com intervenções igualmente vagas. crashkernel=512M-:192M na linha de parâmetros do kernel é o que tornou o segundo travamento diagnosticável. O primeiro travamento, onde o próprio alocador de memória falhou antes que o kdump conseguisse capturar qualquer coisa, demonstra os limites: o kdump exige que o kernel seja funcional o suficiente para gravar o dump — o que não é garantido quando a falha está no próprio subsistema de memória.
Os dois fixes não interagem. Atualização do firmware do NVMe e ajuste de timeout iSCSI tratam a cascata do disco para o etcd e para o engine do Longhorn. Swap e limites de overcommit de memória tratam a pressão separada que deixou o kernel sem margem de manobra sob carga. Ambos precisam estar em vigor — qualquer um sozinho deixa o outro modo de falha intacto.
O Crucial P3 em 162/200 TBW é o risco remanescente. Endurance de flash QLC degrada de forma não-linear próxima ao teto nominal, e o comportamento de throttling térmico do controlador vai piorar antes que o disco falhe definitivamente. Percentage Used e Error Information Log Entries no SMART são as métricas certas para acompanhar semanalmente. Planejar substituição antes de chegar a 90% — não depois do próximo stall de fdatasync.
Referências
- Longhorn Settings Reference —
engine-replica-timeout,concurrent-replica-rebuild-per-node-limit,default-replica-count - Longhorn: Adjust iSCSI timeout and engine-to-replica timeout (issue #4491) — contexto sobre ajuste do timeout de réplica
- Longhorn Storage Network via Multus CNI — opção de rede dedicada para replicação
- Linux kernel: hung_task_panic e sysctl panic —
hung_task_panic,panic,hung_task_timeout_secs - Linux kernel: memory reclaim e kswapd — comportamento de swap e pressão de memória
- kdump-tools Debian wiki — configuração de captura de crash dump
- Crucial P3 NVMe SSD Support — página de firmware (diz “sem atualizações no momento” mas a API abaixo funciona)
- API de firmware da Crucial — retorna JSON com versão atual e URL de download direto
- Atualização de firmware Crucial NVMe no Linux (gist) — processo baseado em nvme-cli