← Todos os posts
26 de jun. de 2026

Mounts SMB no Kubernetes Vão Travar Seu Nó — O Que Realmente Funciona

TL;DR: Mounts CIFS em modo hard (o padrão) colocam processos em TASK_UNINTERRUPTIBLE (estado D) quando o servidor some — estado D ignora SIGKILL, então containers não podem ser removidos, pods ficam presos em Terminating, e se acumularem o suficiente o watchdog de hung-task entra em panic no nó; opções estilo NFS (timeo, retrans, nofail) são argumentos inválidos para o módulo CIFS, liveness probes HTTP não detectam o travamento porque o app continua respondendo até tocar fisicamente no mount, e probes exec fazendo ls /caminho/do/mount bloqueiam pelo mesmo motivo. A solução é um sidecar que verifica a porta TCP 445 a cada 30 s via nc (sem acesso ao filesystem), grava um timestamp Unix em um emptyDir compartilhado em caso de sucesso, e uma liveness probe no container principal que lê esse timestamp e falha se estiver com mais de 90 s — assim a probe nunca pode bloquear em um mount travado; hung_task_panic=1 com panic=5 permanece como backstop caso um pod chegue ao estado D antes do sidecar intervir.

A abstração correta para detectar um mount CIFS travado não é acesso ao filesystem — é uma verificação TCP em nível de rede contra a porta 445. Qualquer probe que toque o mount em si vai bloquear em estado D junto com a carga de trabalho que deveria resgatar. Este post documenta todas as abordagens padrão que falham e por que falham no nível do kernel, depois constrói um padrão de sidecar que sobrevive a quedas do NAS sem tocar o filesystem. A opção de mount echo_interval delimita o tempo de reconexão, mas não é suficiente sozinha: pods que escrevem continuamente entrarão em estado D dentro dessa janela, então o sidecar deve matar o pod antes do timeout do echo expirar.

kill -9 deveria matar qualquer coisa. Não vai matar um processo bloqueado em um mount CIFS travado. Quando um NAS fica offline e os containers de um pod estão no meio de um I/O, esses processos entram em TASK_UNINTERRUPTIBLE — o estado D do kernel — e ficam invisíveis a todos os sinais, incluindo SIGKILL. O container runtime não consegue removê-los. O pod fica em Terminating indefinidamente. Multiplique isso por oito pods em um namespace de media server e o watchdog de hung-task do kernel dispara, entrando em panic no nó inteiro.

Esta é a situação em que esse cluster acabou: um homelab k3s com ~8 pods montando um compartilhamento SMB de 1 TB via driver SMB CSI. Quando o NAS ficou offline, o nó travou e precisou de segurar o botão de energia ou hung_task_panic=1 para disparar um reboot automático. A busca por uma liveness probe que pudesse capturar isso antes de escalar passou por todas as opções óbvias — e a maioria delas piora o problema, não melhora.

Esse é um travamento específico de CIFS, que não deve ser confundido com os travamentos por sobrecomprometimento de memória e cascata de NVMe, sem relação entre si, no mesmo cluster, cobertos em Travamento de Nó k3s: Stalls de iSCSI do Longhorn e Pressão de Memória — mesmo sintoma (nó não responde), mecanismo diferente.

Este post é para operadores que já conhecem liveness probes do Kubernetes e mounts CIFS e já enfrentaram (ou querem evitar) o modo de falha de mount travado. Assume familiaridade com specs de pod, health checks do Kubernetes e estados básicos de processos Linux.

Índice

  1. Por que processos em estado D ignoram SIGKILL — e por que isso trava nós inteiros?
  2. O que foi tentado, e por que falhou?
  3. O que funciona: sidecar em nível de rede com heartbeat em emptyDir
  4. Latência de detecção: cronograma do pior caso
  5. O backstop: quando um pod chega ao estado D mesmo assim
  6. Por que verificações de saúde em nível de rede são a abstração correta para recursos de filesystem?
  7. Referências

Por que processos em estado D ignoram SIGKILL — e por que isso trava nós inteiros?

Mounts CIFS hard colocam processos bloqueados em I/O em TASK_UNINTERRUPTIBLE (estado D), que ignora todo sinal, incluindo SIGKILL, e é invisível para o OOM killer. O kernel tenta novamente o I/O contra o servidor inalcançável indefinidamente, então o processo não pode ser forçado a sair até o mount se recuperar ou a máquina reiniciar. O Kubernetes não consegue coletar um processo em estado D, então o pod fica preso em Terminating, e se pods suficientes entrarem nesse estado ao mesmo tempo, o watchdog de hung-task do kernel entra em panic no nó inteiro. A opção soft é documentada como a saída, mas em kernels reais não previne hangs de estado D de forma confiável sob perda de conexão — a promessa do man page não bate com o comportamento observado. Trate mounts CIFS hard como capazes de travar um nó inteiro, não só o pod dono do mount, sempre que o servidor puder desaparecer.

Mounts CIFS são hard por padrão — o kernel tenta novamente o I/O indefinidamente até o servidor voltar. Um processo bloqueado em um mount hard fica em TASK_UNINTERRUPTIBLE, o que significa que ignora todos os sinais, incluindo SIGKILL. O OOM killer não consegue tocá-lo. kill -9 não faz nada. O processo ficará lá até o mount voltar ou a máquina reiniciar.

Este é o mesmo comportamento dos mounts NFS hard. O módulo CIFS tem uma opção soft (e ela é o padrão nominal segundo o man page), mas na prática não previne hangs de estado D de forma confiável na maioria dos kernels — processos ainda bloqueiam em I/O para um servidor inalcançável. É um bug de longa data: o man page diz que mounts soft não vão travar, mas o comportamento real do kernel contradiz isso em cenários de perda de conexão.

O que foi tentado, e por que falhou?

Toda abordagem padrão de health check do Kubernetes falha contra um mount CIFS travado, por motivos diferentes enraizados no kernel. Opções de mount estilo NFS (timeo, retrans, nofail) não são reconhecidas pelo módulo CIFS e fazem o mount falhar de imediato com mount error(22). echo_interval é uma opção real por-mount que limita o tempo de reconexão a aproximadamente 3 × echo_interval, mas echo_retries não existe mais como parâmetro ajustável em kernels modernos — está ausente de /sys/module/cifs/parameters/ no Debian 13 com kernel 6.12. Liveness probes HTTP continuam passando porque o app permanece responsivo até tocar o mount, então o travamento acontece depois que a probe já reportou saúde. Probes exec que rodam ls no caminho do mount bloqueiam em estado D exatamente como a carga de trabalho que deveriam proteger, adicionando processos presos em vez de capturar a falha. Nenhuma dessas abordagens opera abaixo da camada de filesystem, então nenhuma detecta o travamento antes de ele acontecer.

timeo, retrans, nofail — opções NFS que o módulo CIFS rejeita

Estas são opções de mount NFS. O módulo CIFS não as reconhece. O mount falha imediatamente:

mount error(22): Invalid argument

echo_interval e echo_retries — o que o módulo realmente suporta?

O módulo CIFS usa echo de servidor (keepalive) para detectar conexões mortas. echo_interval é uma opção válida por-mount — define o intervalo em segundos entre echos (padrão: 60s) e pode ser passada via mountOptions em uma spec de PV do Kubernetes. O timeout de reconexão é aproximadamente 3 × echo_interval, então echo_interval=5 dá um timeout de ~15 segundos antes de o kernel considerar o servidor morto.

O PV neste cluster usa echo_interval=30, dando um timeout de reconexão de 90 segundos — intencionalmente alinhado ao limiar de 90 segundos do heartbeat na probe de liveness do sidecar. Quando o NAS fica offline, ambos os mecanismos convergem para a mesma janela: o sidecar mata o pod antes do timeout de echo CIFS atuar em conexões ociosas, e o timeout de echo limita quanto tempo I/O em voo pode bloquear antes de o kernel desistir da conexão.

echo_retries é diferente: era um parâmetro de módulo em kernels mais antigos mas está ausente de /sys/module/cifs/parameters/ no Debian 13 com kernel 6.12:

$ ls /sys/module/cifs/parameters/
CIFSMaxBufSize  cifs_max_pending  cifs_min_rcv  cifs_min_small
dir_cache_timeout  disable_legacy_dialects  enable_gcm_256
enable_negotiate_signing  enable_oplocks  require_gcm_256
slow_rsp_threshold

Mesmo com echo_interval reduzido, qualquer I/O em voo quando o servidor desaparece ainda bloqueia até o timeout do echo expirar. Para pods que escrevem continuamente (como um scanner de mídia ou banco de dados), essa janela é suficiente para entrar em estado D e acumular.

Por que liveness probes HTTP passam enquanto o app dorme em estado D?

A stack *arr (Sonarr, Radarr, Bazarr, etc.) expõe endpoints de saúde HTTP. Uma probe de liveness contra /ping vai reiniciar o container se parar de responder. Mas se o compartilhamento SMB cair enquanto o app está ocioso, o endpoint HTTP continua retornando 200. A probe de liveness passa. O pod parece saudável. Na próxima vez que o app tentar escrever um registro de banco de dados ou escanear um diretório no mount, entra em estado D. Aí a probe não consegue mais ajudar — um processo em estado D também não vai responder ao sinal de reinicialização do container.

Probes HTTP são úteis para detectar crashes de app. Não detectam mounts travados.

Probes exec fazendo ls /mount/path também bloqueiam em estado D

Mesmo problema. ls em um mount CIFS travado entra em estado D. A própria probe trava. O Kubernetes marca a probe como timed out após timeoutSeconds, mas o processo ls bloqueado permanece vivo em estado D, acumulando a cada intervalo de probe. O pod eventualmente é reiniciado pelo failure threshold, mas os processos antigos em estado D podem impedir a remoção limpa do container.

O que funciona: sidecar em nível de rede com heartbeat em emptyDir

A correção é um container sidecar que verifica a porta TCP 445 com nc -z -w 5 <servidor> 445 a cada 30 segundos — sem acesso ao filesystem, então não pode entrar em estado D junto com a carga de trabalho. Em caso de sucesso, escreve um timestamp Unix em um volume emptyDir compartilhado; em caso de falha, registra o evento e para de escrever. A liveness probe do container principal lê esse arquivo de timestamp, não o mount CIFS, e falha quando ele tem mais de 90 segundos, dando uma verificação que nunca pode bloquear. terminationGracePeriodSeconds: 5 também importa: sem isso, o kubelet espera os 30 segundos padrão de shutdown gracioso antes de enviar SIGKILL, que um processo em estado D ignora de qualquer forma — um grace period curto permite que o container runtime destrua o cgroup à força mais rápido. A probe HTTP original vai para readinessProbe, mantendo os health checks de app separados dos health checks de mount.

O insight chave: qualquer processo que tocar um mount CIFS travado vai bloquear. A solução é nunca tocar o mount para verificar sua saúde. Em vez disso, verificar em nível de rede — se o servidor está inalcançável na porta TCP 445, o mount vai travar no próximo I/O.

O loop de verificação TCP do sidecar

Um container sidecar executa nc -z -w 5 <servidor> 445 a cada 30 segundos. Sem acesso ao filesystem. Em caso de sucesso, escreve um timestamp Unix em um volume emptyDir compartilhado com o container principal. Em caso de falha, registra o evento e para de escrever.

STATE=up
while true; do
  if nc -z -w 5 192.168.1.50 445 2>/dev/null; then
    date +%s > /healthcheck/alive
    if [ "$STATE" = "down" ]; then
      echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [RECOVERY] SMB acessível novamente"
      STATE=up
    fi
  else
    if [ "$STATE" = "up" ]; then
      echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [FAILURE] SMB inacessível — probe de liveness vai falhar em ~90s"
      STATE=down
    else
      echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [FAILURE] SMB ainda inacessível"
    fi
  fi
  sleep 30
done

Como a probe de liveness lê o heartbeat sem bloquear?

A probe de liveness do container principal lê o timestamp do emptyDir — não do mount CIFS, então nunca pode bloquear:

livenessProbe:
  exec:
    command:
      - /bin/sh
      - -c
      - "test $(( $(date +%s) - $(cat /healthcheck/alive 2>/dev/null || echo 0) )) -lt 90"
  initialDelaySeconds: 60
  periodSeconds: 30
  timeoutSeconds: 5
  failureThreshold: 3

Quando o NAS fica offline: o sidecar detecta em 30 segundos, para de escrever o heartbeat, registra [FAILURE]. Após 90 segundos de heartbeat stale, a probe de liveness falha. Após três falhas (mais 90 segundos), o pod é morto.

A spec do pod precisa de terminationGracePeriodSeconds: 5. Sem isso, se o app já está em estado D quando o pod é morto, o kubelet espera os 30 segundos padrão de shutdown gracioso antes de enviar SIGKILL. Com 5 segundos, o container runtime parte para force-kill mais rápido. Um processo em estado D ainda não vai sair no SIGKILL, mas o container runtime pode forçosamente destruir o cgroup e seguir em frente.

Conectando o sidecar à spec do pod

A adição completa à spec do pod, em Pulumi Python:

smb_healthcheck_volume = {
    "name": "smb-healthcheck",
    "emptyDir": {},
}

smb_healthcheck_sidecar = {
    "name": "smb-health-monitor",
    "image": "busybox:latest",
    "image_pull_policy": "IfNotPresent",
    "command": ["/bin/sh", "-c"],
    "args": [
        """
STATE=up
while true; do
  if nc -z -w 5 192.168.1.50 445 2>/dev/null; then
    date +%s > /healthcheck/alive
    if [ "$STATE" = "down" ]; then
      echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [RECOVERY] SMB acessível novamente"
      STATE=up
    fi
  else
    if [ "$STATE" = "up" ]; then
      echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [FAILURE] SMB inacessível — probe de liveness vai falhar em ~90s"
      STATE=down
    else
      echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [FAILURE] SMB ainda inacessível"
    fi
  fi
  sleep 30
done
"""
    ],
    "volume_mounts": [{"name": "smb-healthcheck", "mount_path": "/healthcheck"}],
}

smb_liveness_probe = {
    "exec": {
        "command": [
            "/bin/sh",
            "-c",
            "test $(( $(date +%s) - $(cat /healthcheck/alive 2>/dev/null || echo 0) )) -lt 90",
        ]
    },
    "initial_delay_seconds": 60,
    "period_seconds": 30,
    "timeout_seconds": 5,
    "failure_threshold": 3,
}

Cada deployment usando o PVC SMB recebe:

Latência de detecção: cronograma do pior caso

No pior caso, a configuração padrão — intervalo de sidecar de 30 segundos, limiar de heartbeat de 90 segundos, periodSeconds: 30, failureThreshold: 3 — leva cerca de 3 minutos e 5 segundos entre a queda do NAS e a terminação do pod. O sidecar precisa de até 30 segundos para notar a falha TCP, depois a liveness probe precisa de três falhas consecutivas espaçadas em 30 segundos antes de o Kubernetes matar o pod, mais alguns segundos para a própria terminação. Esse limite vem inteiramente de dois ajustes — o intervalo de sleep do sidecar e o limiar de idade do heartbeat na probe — e reduzir os dois juntos aperta a janela. Para um homelab que tolera uma queda ocasional de alguns minutos antes da recuperação, 3 minutos é uma troca razoável em relação a menos verificações TCP contra o NAS. Cargas de trabalho com menos tolerância a pods presos devem reduzir os dois ajustes e manter o limiar acima de echo_interval × 3.

Pior caso: SMB cai imediatamente após uma verificação do sidecar.

t=0    NAS offline
t=30   sidecar detecta falha, para de escrever heartbeat
t=60   probe de liveness: heartbeat com 30s → passa (< 90s de limiar)
t=90   probe de liveness: heartbeat com 60s → passa
t=120  probe de liveness: heartbeat com 90s → FALHA (1/3)
t=150  probe de liveness: FALHA (2/3)
t=180  probe de liveness: FALHA (3/3) → pod morto
t=185  pod terminado

Cerca de 3 minutos e 5 segundos. Aceitável para um homelab. Para limites mais apertados, reduza periodSeconds no sleep do sidecar e na probe de liveness, e reduza o limiar de 90 segundos proporcionalmente.

O backstop: quando um pod chega ao estado D mesmo assim

Se um pod chega ao estado D antes de o limiar de 90 segundos do sidecar disparar — porque havia I/O em voo quando o NAS desapareceu — nenhuma liveness probe consegue resgatá-lo, já que o próprio processo da probe bloquearia. A última linha de defesa é um ajuste de kernel: hung_task_panic=1 com panic=5 na linha de comando de boot. Quando o watchdog de hung-task do kernel detecta um processo preso em estado D além do seu limiar (120 segundos de hung tasks por padrão nessa configuração), ele entra em panic no nó, que então reinicia automaticamente em vez de ficar congelado até alguém achar e religar fisicamente a máquina. Isso não previne o travamento — converte um hang indefinido que exige intervenção física em recuperação automática dentro de poucos minutos. Mantenha-o ativado mesmo depois de implantar o sidecar; o sidecar reduz a frequência com que o backstop dispara, não elimina a necessidade dele.

Se o pod já está em estado D antes da probe de liveness matá-lo, hung_task_panic=1 com panic=5 na linha de comando do kernel é a última linha de defesa. O nó vai reinicializar automaticamente após 120 segundos de hung tasks. Isso não previne o travamento — só se recupera dele sem exigir intervenção física. O sidecar é para evitar que pods cheguem ao estado D em primeiro lugar.

Por que verificações de saúde em nível de rede são a abstração correta para recursos de filesystem?

Qualquer recurso que pode entrar em um bloqueio não-interruptível do kernel — CIFS, mounts NFS hard, iSCSI — precisa de um health check operando na camada de rede, nunca na camada de filesystem, porque uma probe que toca o recurso vira parte da queda em vez de detectá-la. Esse padrão de sidecar mais heartbeat não serve para todo caso: pule-o para containers privilegiados que precisam continuar ativos durante interrupções de mount, ou cargas de trabalho que toleram breves paralisações de I/O dentro da janela de reconexão do echo_interval. Nesses casos, hung_task_panic com um timeout panic curto funciona melhor como backstop isolado, já que não há pod para resgatar cirurgicamente antes de chegar ao estado D. Mantenha uma readiness probe HTTP rodando junto com esse padrão de qualquer forma — ela captura falhas de nível de app, como crashes ou deadlocks, que não têm nada a ver com o mount, então saúde do mount e saúde do app falham de forma independente em vez de mascarar uma à outra.

O padrão que isso estabelece: para qualquer recurso de filesystem que pode entrar em um bloqueio não-interruptível — CIFS, mounts NFS hard, iSCSI — a única probe que não pode ficar presa é a que opera na camada de rede. Uma probe que toca o filesystem torna-se parte do problema no momento em que o recurso fica indisponível.

Onde o padrão de sidecar não se aplica

A abordagem de sidecar não se aplica quando o próprio pod roda como container privilegiado que precisa permanecer ativo durante interrupções de mount, ou quando a carga de trabalho tolera breves paralisações de I/O e a janela de reconexão do echo_interval é suficiente para cobri-las. Nesses casos, hung_task_panic com um timeout panic curto é o backstop mais adequado — aceitar que o nó reinicializa em vez de tentar matar pods cirurgicamente antes de chegarem ao estado D.

Ajustando a janela de detecção

Para limites de latência mais apertados do que os 3 minutos do pior caso acima, os dois ajustes são o intervalo de sleep do sidecar e o limiar de idade do heartbeat na liveness probe. Reduzir ambos para 10 s / 30 s dá uma detecção no pior caso de cerca de 70 segundos, ao custo de três vezes mais tentativas de conexão TCP contra o NAS. Abaixo de echo_interval × 3 o sidecar não consegue mais superar de forma confiável o timeout do echo CIFS, então o limiar e o echo_interval devem ser ajustados juntos.

A readiness probe HTTP ainda vale a pena manter no container principal junto com esse padrão: ela captura falhas no nível do app (crash, deadlock, configuração errada) que não têm nada a ver com o mount. Separar as duas preocupações — saúde do mount via probe de rede, saúde do app via HTTP — significa que cada probe faz exatamente uma coisa e pode falhar de forma independente.

Referências


Breno Zanato Detomini
Breno Zanato Detomini

Engenheiro de sistemas embarcados e redes, baseado no Brasil.

← Todos os posts