Skip to content

Deadlock de nfsd: causa raíz de los cuelgues del NAS (2026-07-26)

Investigación que cerró un problema abierto desde el 2026-07-14. Incluye las hipótesis falsas porque cada una costó tiempo y conviene no repetirlas.

La causa raíz

Deadlock del nfsd de Linux al destruir sesiones NFSv4. Capturado en vivo con pilas de kernel de ambos extremos:

NAS (servidor)
  nfsd                          wchan=__flush_workqueue
      __flush_workqueue  <-  nfsd4_destroy_session      (espera vaciar la cola de callbacks)
  kworker/u8:N+nfsd4_callbacks  wchan=rpc_shutdown_client
      rpc_shutdown_client <- nfsd4_process_cb_update <- nfsd4_run_cb_work   (trabajo EN esa cola)

208 (cliente)
  192.168.0.237-manager         wchan=rpc_wait_bit_killable
      rpc_call_sync <- nfs4_proc_destroy_session         (esperando al servidor)

Se esperan mutuamente. Cada episodio pierde un hilo nfsd para siempre (D-state, no se mata ni con kill -9). Se acumulan de uno en uno; cuando caen los 8, el NFS muere.

El estrés de E/S no cuelga el NAS directamente: hace que los clientes NFSv4 agoten su timeout, y es la reconexión la que dispara el bug. Por eso correlacionaba con el backup de PBS (06:40) y con el borrado de snapshots (03:00) sin que ninguno fuera la causa.

Kernel afectado: 6.1.120+ (TOS 6.0.794). TOS 7 lleva 6.12 → la actualización pasa a ser un arreglo dirigido, no especulativo.

Cómo reconocerlo

Señal Valor
load1 clavado en un entero plano (1 por hilo perdido)
disco / red / nfsd req cero
procs_blocked 0 ← la clave
procs_running 1

procs_blocked cuenta sólo iowait; el load cuenta toda tarea en D. Si el load es 10 y los bloqueados 0, hay diez tareas en sueño ininterrumpible que no esperan disco: esperan un cerrojo. Histórico: 26-jul load=2 (2 hilos), 14-jul load=10 (8 hilos + 2).

Hipótesis falsas (y por qué cayeron)

Hipótesis Cómo se descartó
Backup de appdata satura el nfsd con 3 h de escritura Medido: escribía 18 GB en 20 min; las otras 3 h leía agujeros de un fichero sparse. Ver abajo.
gzip es el cuello del backup 42 MB/s → explicaría 11 min de 209
El NFS es lento 111 MB/s, satura la gigabit
Falta de RAM en el 208 PSI de memoria ≈1 %; el swap estaba rancio, no activo
Muchos ficheros pequeños 25 641 ficheros / 6,2 GB en 55 s
btrfs subvolume delete / snapshots / fichero CoW de 875 GB Amplifican el estrés, no son la causa
Un trabajo semanal los lunes a las 07:30 Artefacto del método: min_over_time[30m] recortaba el inicio real
Scrub de BTRFS El último fue el 16-may y quedó aborted

Lección de método: una ventana de agregación puede fabricar patrones inexistentes. Para fechar el inicio de un episodio, consultar a paso de 60 s.

Hallazgo colateral: backup de appdata, 3h29m → 18m33s

appdata ocupa 28 GB reales pero 2,1 TB aparentes: appdata/tunarr/data.ms/indexes/*/data.mdb es la LMDB de Meilisearch con max_map_size de 2 TiB y 287 MB asignados. du cuenta bloques; tar leía el tamaño aparente y se tragaba 2 TiB de ceros cada noche.

Cura: tar -S/--sparse (GNU tar ≥1.29 usa SEEK_HOLE). Sobre tunarr solo: 2,3 s frente a horas. Verificado que restaura el fichero con sus agujeros intactos (aparente=2199023255552 bloques_512=586928, idénticos).

Ante un backup absurdamente lento, el primer comando no es time al compresor:

du -sh DIR ; du -sh --apparent-size DIR     # si difieren mucho -> sparse

También se corrigió un livelock de la API .backup() de sqlite: reinicia la copia cada vez que un escritor toca la DB origen; con Music Assistant escribiendo no terminaba nunca (3h20m atascado, .snapshot a 0 bytes, sin llegar al tar). Cura: VACUUM INTO + timeout.

Automatización añadida

Qué Dónde Detalle
Captura forense NAS /usr/local/bin/nas-hang-forensics.sh + cron 1 min Tareas en D con pila de kernel; dmesg incremental. Escribe en /root (persiste); nunca toca /Volume1 para no colgarse con él.
Auto-reboot por deadlock NAS nas-volume-selfheal.sh ≥50 % hilos nfsd en D sostenido 10 minsysrq-b. 1-2 hilos = sólo registra.
Reconciliación 208 nfs-health-monitor.sh Foto de contenedores NFS vivos → congelada al primer fallo → restaurados al volver el NFS. Inerte en operación normal.
Replicación de logs 208 pull-nas-forensics.sh cron 10 min Copia el log del NAS al 208 con timeout.

Por qué el "sostenido 10 min" no es negociable

El 26-jul a las 19:41 los 8 hilos entraron en D a la vez por una tormenta de E/S y 6 se recuperaron en un minuto. Un disparo instantáneo habría reiniciado el NAS sin necesidad. Probado en simulacro con 6 casos, incluido ese.

Por qué la reconciliación no levanta "todo lo parado"

Sablier para contenedores a propósito (lazy-start), y trip-planner está en la lista de contenedores NFS. Un reconciliador ingenuo se pelearía con él cada 2 minutos.

Trampas de systemd encontradas

  1. OnUnitActiveSec en un watchdog es una bomba. Se ancla a la ejecución anterior: si el servicio se cuelga (justo cuando hace falta), no hay ancla y al morir el timer se apaga y no vuelve. Caso real: 19:41:54 arrancó sobre un NFS colgado, systemd lo mató a las 20:04:57, y quedó inactive 1 h 43 min → 19 contenedores caídos 2 horas. → Para vigilantes usar SIEMPRE OnCalendar + TimeoutStartSec.
  2. Un TimeoutStartSec corto mata recuperaciones legítimas. recover_nfs hace MAX_RETRIES=30 × 20 s = hasta 10 min válidos. Poner 90 s (error cometido y corregido) las abortaría. Valor actual: 900 s.

Qué queda abierto

  • TOS 7 (kernel 6.12) es el arreglo de fondo. Lo automatizado convive con el bug.
  • El sysrq-b del vigilante nuevo nunca se ha ejecutado de verdad — probada la lógica de decisión, no el disparo completo.
  • El umbral del 50 % es un juicio, no un valor derivado de datos.
  • La reconciliación no se ha visto en una avería real, sólo simulada.
  • Prometheus retiene "30d o 5 GiB" y manda el tamaño: los datos del NAS sólo llegan a ~20 días.