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
timeal compresor:
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 min → sysrq-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¶
OnUnitActiveSecen 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óinactive1 h 43 min → 19 contenedores caídos 2 horas. → Para vigilantes usar SIEMPREOnCalendar+TimeoutStartSec.- Un
TimeoutStartSeccorto mata recuperaciones legítimas.recover_nfshaceMAX_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-bdel 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.