Skip to main content
Taller42

Casos reales

Asesinato en el expreso «4KiB»

Publicado el 12 de septiembre de 2026

Una app inmóvil, una base SQLite bajo sospecha y demasiados actores alrededor del cadáver.

Todo empezó con una aplicación que parecía muerta.

Nextcloud Desktop para macOS no arrancaba correctamente. El proceso existía, pero el cliente no terminaba de aparecer, no sincronizaba y no ofrecía un error útil. No había un mensaje claro. No había una pista amable. No había una ventana diciendo: “mire usted, el problema está aquí”.

Naturalmente: los programas rara vez tienen esa educación.

El volumen sincronizado estaba en /Volumes/Nextcloud, dentro de una caja externa QNAP TR-004. La base local de sincronización era una SQLite llamada:

/Volumes/Nextcloud/.sync_2f4a44904d04.db

La lectura rápida del síntoma habría sido la de siempre:

Nextcloud se ha colgado.

Pero esa frase explicaba muy poco; un proceso puede estar muerto, bloqueado, esperando una respuesta, atrapado en una operación de entrada/salida o sencillamente haciendo algo tan lento que desde fuera parece congelado. En este caso, Nextcloud no estaba muerto.

Estaba esperando.

El síntoma

Nextcloud Desktop quedaba inmóvil al arrancar, no aparecía correctamente el icono, no sincronizaba y no daba un error útil. El proceso principal existía, pero desde fuera la sensación era la de una aplicación detenida en mitad del arranque.

La carpeta sincronizada no estaba en el disco interno del Mac, sino en un volumen externo. Ese detalle importaba.

El escenario era este:

  • macOS;
  • Nextcloud Desktop;
  • volumen /Volumes/Nextcloud;
  • caja externa QNAP TR-004;
  • base de sincronización .sync_2f4a44904d04.db;
  • disco externo como destino real de sincronización.

Podía haber empezado borrando la cuenta local de Nextcloud, recreando la sincronización, reinstalando la aplicación o culpando directamente al cliente.

Pero antes de tocar nada preferí mirar.

La pregunta correcta no era:

¿por qué Nextcloud está colgado?

La pregunta correcta era:

¿qué está esperando Nextcloud?

Primera autopsia: sample

Lo primero fue inspeccionar el proceso: el cliente principal aparecía vivo, en estado U. No era un cadáver limpio. Era más bien uno de esos pacientes que no se mueven, pero siguen consumiendo recursos y dejando al médico con cara de pocos amigos. Tomé una muestra del proceso con sample, una herramienta de macOS que permite observar durante unos segundos dónde está pasando el tiempo una aplicación.

La cadena relevante apuntaba a esto:

OCC::Application::setupAccountsAndFolders
OCC::FolderMan::setupFolders
OCC::Folder::startVfs
OCC::SyncJournalDb::open
OCC::SyncJournalDb::checkConnect
OCC::SqlDatabase::checkDb
OCC::SqlQuery::next
sqlite3_step
pread

La muestra no señalaba claramente a la interfaz gráfica, no parecía un problema del icono de menú, no estaba simplemente atascado en FinderSync.

El rastro llevaba a la fase de arranque de las carpetas de sincronización de Nextcloud. Más concretamente, al momento en que el cliente abría y comprobaba su journal SQLite. Y ahí aparecía la palabra que siempre entra en escena como sospechosa habitual:

SQLite.

SQLite entra en escena

La base sospechosa era:

/Volumes/Nextcloud/.sync_2f4a44904d04.db

Tenía un tamaño aproximado de 77 MB.

No era una monstruosidad de varios gigabytes. Tampoco era una base diminuta, pero su tamaño no justificaba por sí solo que Nextcloud Desktop pareciera congelado al arrancar. A primera vista, era fácil construir una explicación: Nextcloud se queda parado porque la base SQLite está corrupta, y era una hipótesis razonable.

Pero una hipótesis razonable no es una conclusión.

Y como siempre que aparece SQLite en una escena del crimen, medio Internet se levanta para acusarla antes de mirar el cadáver. Así que tocaba comprobar si la base realmente estaba rota o si simplemente era el componente que estaba pidiendo datos a un almacenamiento que no respondía como debía.

Hice consultas manuales con sqlite3, y una de ellas fue:

sqlite3 /Volumes/Nextcloud/.sync_2f4a44904d04.db "SELECT count(*) FROM metadata NOT INDEXED;"

La consulta funcionaba, pero eso no demostraba que todo estuviera perfecto, pero sí alejaba la idea de una base completamente ilegible o destruida. Más bien, lo interesante era otra cosa: los tiempos cambiaban muchísimo según el estado del sistema. En estado caliente, con datos aparentemente cacheados, la consulta podía tardar alrededor de:

0,027 s

En estado degradado podía irse aproximadamente a:

7,3 s

Y en estado muy malo la espera podía parecer de decenas de segundos o incluso minutos. A esta altura la base no parecía gritar “corrupción”; parecía gritar otra cosa:

entrada/salida lenta.

La trampa de PRAGMA integrity_check

Una reacción bastante natural habría sido lanzar:

PRAGMA integrity_check;

y usar eso como sentencia, pero en este caso no era una prueba limpia. La base de Nextcloud utiliza índices con funciones propias, como:

parent_hash(path)

El sqlite3 externo no conoce necesariamente esas funciones. Por tanto, pedirle que evalúe la integridad de una base que usa mecanismos propios del cliente podía producir resultados difíciles de interpretar. No quería convertir una herramienta de diagnóstico en una máquina de fabricar ruido, así que dejé esa vía con una conclusión prudente: no tenía una evidencia clara de corrupción. En cambio tenía una evidencia cada vez más fuerte de espera de E/S.

fs_usage: el disco empieza a confesar

La herramienta que cambió el caso fue fs_usage.

En macOS, fs_usage permite observar llamadas al sistema relacionadas con acceso a archivos. En este caso me interesaba ver exactamente qué hacía Nextcloud mientras parecía congelado; ahí apareció el dato central:

pread
B=0x1000
/Volumes/Nextcloud/.sync_2f4a44904d04.db

B=0x1000 significa 4096 bytes.

Es decir: 4 KiB.

Nextcloud, a través de SQLite, no estaba leyendo la base como un bloque grande y secuencial, estaba haciendo muchísimas lecturas pequeñas… Una detrás de otra, y las latencias individuales eran demasiado altas para operaciones tan pequeñas:

0,03 s
0,08 s
0,14 s
0,22 s
0,33 s

Una lectura de 4 KiB que tarda 100 milisegundos no parece un drama. Mil lecturas de 4 KiB a 100 milisegundos cada una ya son otra cosa: ahí estaba el “cuelgue”. No era un bloqueo.

Era una procesión.

Nextcloud estaba haciendo exactamente lo que debía hacer, sobre un almacenamiento que respondía como si cada página de SQLite estuviera enterrada en una provincia distinta.

Lecturas pequeñas, espera grande

Este punto es importante porque cambia el diagnóstico: una aplicación puede leer un archivo grande de forma secuencial y obtener un rendimiento aceptable incluso sobre un disco mecánico externo. El sistema operativo puede aprovechar lectura anticipada, caché y acceso continuo; pero SQLite no estaba leyendo un vídeo de principio a fin, una base SQLite es otra cosa.

SQLite trabaja con páginas, índices, árboles B, metadatos y saltos internos. Puede necesitar leer una página, luego otra, luego un índice, luego volver a otra zona del fichero. Un vídeo grande es una carretera recta. Una base SQLite vieja es una ciudad medieval: callejones, esquinas, escaleras, giros raros y una cabra bloqueando el paso; si el almacenamiento responde mal ante lecturas pequeñas y aleatorias, el coste se acumula. Desde fuera, lo único que se ve es una aplicación que no arranca. Desde dentro, lo que ocurre es más aburrido y más cruel:

4 KiB.

Después otros 4 KiB.

Después otros 4 KiB.

Y así hasta que uno empieza a mirar de reojo el cable USB como si fuera sospechoso de asesinato.

El dd: calentar la base y romper el hechizo

La prueba más reveladora fue bastante simple: mientras Nextcloud estaba trabado, leí secuencialmente la base con dd. Uno de los episodios dio este resultado:

77058048 bytes transferred in 23.526643 secs

Aproximadamente:

3,27 MB/s

No era una velocidad gloriosa para leer 77 MB, pero lo importante no fue la velocidad secuencial en sí. Lo importante fue lo que ocurrió después: Nextcloud terminó de arrancar. Y yo no reparé SQLite, no borré la configuración, no recreé la cuenta, tampoco reinicié el Mac, y menos aun sacrifiqué una cabra ante el altar de los periféricos USB, aunque por momentos habría parecido una estrategia razonable.

Simplemente forcé una lectura secuencial completa del fichero, y después de eso, las páginas de la base quedaron calientes en caché. Las lecturas aleatorias de 4 KiB que antes caían al disco empezaron a servirse desde RAM. El efecto era inmediato, pero no era una reparación.

El dd no arregló SQLite. Sólo calentó el fichero.

Y precisamente por eso era una prueba tan útil: demostraba que SQLite era sensible al problema, pero no necesariamente culpable.

Una segunda lectura, otro contexto

Después del cambio posterior a HFS+ y tras una tarea de Carbon Copy Cloner, repetí una lectura de la misma base, y el resultado fue muy distinto:

77058048 bytes transferred in 2.591070 secs

Aproximadamente:

29,7 MB/s

Otra vez, después del dd, Nextcloud terminó de arrancar inmediatamente.

El patrón se repetía:

  • Nextcloud parecía trabado;
  • la muestra apuntaba a SQLite;
  • fs_usage mostraba lecturas de 4 KiB;
  • leer secuencialmente la base calentaba el fichero;
  • el cliente terminaba de arrancar.

Eso reforzaba la interpretación: el problema no era simplemente “Nextcloud se cuelga”; el problema era que el arranque dependía de muchas pequeñas lecturas contra una base SQLite ubicada en un volumen externo, y esas lecturas podían volverse insoportablemente lentas según el estado del almacenamiento.

SQLite era el mensajero

SQLite quedó bajo sospecha desde la primera muestra, y esto me parecía normal: aparecía en la pila y estaba en mitad del proceso.

La base .sync_2f4a44904d04.db era el archivo leído una y otra vez, pero la aplicación que aparece en la muestra no siempre es la culpable. Muchas veces es simplemente la primera que exige al sistema justo aquello que el sistema hace mal. En este caso, SQLite necesitaba leer pequeñas páginas de su base. Nextcloud necesitaba abrir su journal de sincronización. El sistema de almacenamiento respondía con latencias demasiado altas.

SQLite no estaba necesariamente roto: estaba revelando el problema. O sea, era el mensajero.

Y ya se sabe lo que suele hacer la humanidad con los mensajeros: culparlos, fusilarlos o abrir un hilo en Reddit diciendo que SQLite es una mierda.

APFS → HFS+

Después de comprobar el patrón de espera, hice una prueba fuerte. El volumen Nextcloud estaba en APFS, lo reformateé como HFS+ Journaled, y Después restauré los datos con Carbon Copy Cloner.

Tras el cambio, el comportamiento mejoró de forma visible:

  • el volumen Nextcloud, ahora HFS+, aparecía casi inmediatamente en el escritorio;
  • los otros dos discos APFS de la misma caja QNAP TR-004 tardaban más en aparecer;
  • Nextcloud Desktop arrancó manualmente en segundos en condiciones normales.

Todos los discos compartían la misma caja QNAP TR-004 y el mismo enlace USB. La diferencia visible era el sistema de archivos del volumen usado por Nextcloud. Eso ponía APFS bajo sospecha, pero no permitía condenarlo.

El experimento no era puro

Aquí conviene no vender humo.

Sería muy fácil escribir:

APFS era el problema.

Pero no puedo afirmarlo con esa seguridad, porque el experimento no fue puro.

Cambiaron varias cosas a la vez:

  • el sistema de archivos pasó de APFS a HFS+ Journaled;
  • la copia y restauración con CCC pudo reorganizar físicamente los datos;
  • la base SQLite pudo quedar escrita de forma más favorable;
  • el volumen quedó reconstruido desde cero;
  • parte del comportamiento pudo deberse a una especie de “defrag” accidental.

Como experimento de laboratorio era impuro, pero como diagnóstico de campo, era oro.

Porque el objetivo no era publicar un paper con revisión por pares. El objetivo era entender por qué Nextcloud parecía muerto y conseguir que volviera a arrancar sin convertir cada inicio en una ceremonia de exorcismo tecnológico.

La mejora fue real, aunque la causa raíz seguía abierta.

La recaída: CCC entra en la escena del crimen

Días después, Nextcloud volvió a quedarse trabado; a primera vista parecía una recaída del problema original, pero esta vez había un actor nuevo en la escena: Carbon Copy Cloner.

Yo había lanzado manualmente Nextcloud sin fijarme en que CCC había estado ejecutando una tarea de backup sobre el volumen, y durante esa tarea, el volumen estaba prácticamente secuestrado. Ni siquiera el Finder podía visualizar correctamente el contenido. Después de terminar CCC, Finder volvió a navegar el volumen.

La muestra de Nextcloud volvió a apuntar al mismo tipo de cadena:

SyncJournalDb::open
sqlite3_step
pread

Y fs_usage volvió a mostrar lecturas de 4 KiB contra la base .sync_*.db.

Eso cambiaba la interpretación: no significaba necesariamente que HFS+ hubiera fracasado, significaba que Nextcloud seguía siendo vulnerable si intentaba abrir su journal SQLite justo después o durante una carga intensa de CCC. El volumen podía comportarse bien en condiciones normales y mal cuando otra aplicación lo había tenido bajo presión.

La escena del crimen se llenaba de actores.

Nextcloud, SQLite, APFS, HFS+, CCC, QNAP TR-004, macOS, caché fría y lecturas de 4 KiB.

Un reparto demasiado numeroso para resolverlo gritando “culpable” al primero que aparecía en la puerta.

Hipótesis A: APFS y QNAP TR-004 se llevan mal

La primera hipótesis viva es que APFS y la QNAP TR-004 no formen precisamente una pareja feliz. APFS utiliza copy-on-write y una gestión moderna de metadatos. En muchos contextos eso tiene ventajas claras, pero sobre discos mecánicos externos, con muchas operaciones pequeñas, una caja USB y un firmware intermedio, esa complejidad puede convertirse en coste.

Quizá APFS no era “malo” en abstracto, quizá era una mala combinación en este escenario concreto:

  • macOS;
  • APFS;
  • disco mecánico externo;
  • caja QNAP TR-004;
  • puente USB;
  • base SQLite;
  • lecturas pequeñas;
  • arranque de Nextcloud.

HFS+ pareció comportarse mejor: el volumen montaba más rápido, Nextcloud arrancaba en segundos en condiciones normales.

Esa señal es fuerte, pero sigue siendo una señal, no una sentencia.

Hipótesis B: la base SQLite envejece mal

La segunda hipótesis es que la base .sync_*.db pudiera degradarse con el uso.

Una base de sincronización de Nextcloud no es un archivo decorativo. Vive, cambia, registra estados, acumula operaciones, borra, añade, reorganiza y mantiene información sobre el árbol sincronizado. Con el tiempo puede acumular páginas libres, dispersión interna o una disposición poco favorable para ciertos patrones de lectura. La copia, el reformateo y la restauración pudieron actuar como una especie de reorganización accidental. Si esa hipótesis es correcta, HFS+ no habría “solucionado” el problema de raíz. Sólo habría coincidido con una reescritura más favorable de la base y del volumen. En ese caso, dentro de semanas o meses el problema podría volver. Y si vuelve sin CCC de por medio, habrá que mirar otra vez hacia el journal de sincronización y su evolución.

Hipótesis C: CCC satura el escenario

La recaída reciente sugiere una tercera hipótesis.

Puede que CCC, durante o después de su tarea de backup, deje el volumen en una situación poco favorable para el arranque inmediato de Nextcloud. Durante la copia, CCC recorre el volumen, lee, compara, escribe y genera carga intensa de E/S. Si Nextcloud intenta abrir su journal SQLite en ese momento o justo después, se encuentra pidiendo miles de pequeñas lecturas de 4 KiB a un almacenamiento que viene de estar bajo presión. El resultado visible vuelve a ser el mismo:

Nextcloud parece muerto.

Pero internamente sigue haciendo lo mismo de antes:

sqlite3_step
pread

No hay necesariamente un único asesino: hay una escena compartida donde cada actor puede poner una parte del desastre.

Hipótesis D: no hay un único culpable

La hipótesis más incómoda es también la más realista: quizá no existe un único culpable, quizá el problema aparece cuando coinciden varias condiciones:

  • base SQLite suficientemente grande o dispersa;
  • caché fría;
  • volumen externo;
  • lecturas aleatorias pequeñas;
  • latencia alta;
  • QNAP TR-004;
  • macOS;
  • sistema de archivos;
  • carga previa o simultánea de CCC.

Cuando todos esos factores se alinean, Nextcloud no falla de forma espectacular.

Simplemente espera.

Y espera.

Y sigue esperando.

El usuario lo llama “cuelgue”, el sistema lo llama “pread”.

La diferencia es que el usuario tiene razón emocionalmente y el sistema tiene razón técnicamente. Qué bonito todo. Dan ganas de imprimirlo en una camiseta y tirarla al mar.

Lo que sé

Después de la investigación puedo afirmar con bastante seguridad:

  • Nextcloud Desktop no estaba bloqueado en la interfaz gráfica.
  • El proceso principal seguía vivo.
  • El cliente aparecía en estado U.
  • La muestra apuntaba a la apertura y comprobación del journal SQLite de Nextcloud.
  • La cadena pasaba por sqlite3_step y pread.
  • La base de sincronización era .sync_2f4a44904d04.db.
  • La base estaba en /Volumes/Nextcloud/.
  • La base tenía unos 77 MB.
  • Las consultas manuales con sqlite3 funcionaban.
  • Los tiempos variaban muchísimo entre estado caliente, degradado y muy malo.
  • PRAGMA integrity_check no era una prueba limpia por las funciones propias usadas por Nextcloud, como parent_hash(path).
  • fs_usage mostró lecturas de 4 KiB contra la base.
  • Las latencias de esas lecturas pequeñas eran demasiado altas.
  • Una acumulación de muchas lecturas pequeñas explicaba el aparente cuelgue.
  • Leer secuencialmente la base con dd permitía desbloquear el arranque.
  • El dd no reparaba SQLite; calentaba el fichero en caché.
  • Tras pasar el volumen de APFS a HFS+ Journaled, el comportamiento normal mejoró.
  • El volumen HFS+ montaba más rápido.
  • Nextcloud Desktop arrancaba en segundos en condiciones normales.
  • Los otros discos APFS de la misma caja QNAP TR-004 tardaban más en aparecer.
  • Una tarea de CCC pudo reproducir el síntoma al generar carga intensa sobre el volumen.
  • SQLite no era necesariamente culpable: era el componente que revelaba el problema.

Lo que todavía no sé

No puedo afirmar todavía:

  • que APFS fuera el culpable principal;
  • que HFS+ haya solucionado definitivamente el problema;
  • que la mejora no viniera de la copia/restauración;
  • que la base SQLite no vuelva a degradarse con el tiempo;
  • que el firmware de la QNAP TR-004 no tenga parte de culpa;
  • que CCC simplemente saturara el volumen;
  • que CCC no dejara la caché o la cola de E/S en mal estado;
  • que una compactación controlada de la base evitara futuras recaídas;
  • que Nextcloud Desktop pudiera manejar mejor este tipo de arranque con E/S lenta;
  • que el problema esté en un único componente aislado.

Podría escribir una sentencia definitiva, que sería más cómoda que cierta: una conclusión honesta no siempre es una sentencia, a veces es simplemente un mapa mejor del campo de batalla.

Solución operativa actual

La solución operativa actual no es una reparación definitiva: es un desbloqueo; cuando Nextcloud queda trabado y el patrón confirmado vuelve a ser:

sqlite3_step
pread

con lecturas pequeñas contra la base .sync_*.db, puedo leer secuencialmente esa base con dd.

El resultado esperado es:

  • lectura completa de la base;
  • páginas calientes en caché;
  • reducción inmediata de las esperas aleatorias;
  • Nextcloud termina de arrancar.

La idea no es “arreglar SQLite”, sino es evitar que el arranque dependa de leer desde disco, una a una, páginas pequeñas que pueden tardar demasiado. Es una solución operativa, no una explicación final: conviene no confundir ambas cosas.

La informática ya tiene suficientes supersticiones como para añadir otra.

Plan de seguimiento

Como la causa raíz no está cerrada, conviene observar.

Tiene sentido medir periódicamente:

  • tamaño de la base .sync_*.db;
  • tiempo de arranque de Nextcloud;
  • tiempo de consulta sobre metadata;
  • page_count y freelist_count de SQLite, si decido medirlos;
  • estado del volumen después de una tarea reciente de CCC.

Si el problema reaparece, el orden correcto no es tocar primero y pensar después.

El orden correcto es medir.

Primero, tomar una muestra del proceso principal:

sudo sample <pid> 5 -file /tmp/nextcloud-hang.txt

Después, observar el acceso al sistema de archivos:

sudo fs_usage -w -f filesys -t 10 <pid>

Sólo después de confirmar el patrón tendría sentido leer la base secuencialmente:

dd if=/Volumes/Nextcloud/.sync_2f4a44904d04.db of=/dev/null bs=4m

También podría medir la base:

sqlite3 /Volumes/Nextcloud/.sync_2f4a44904d04.db "SELECT count(*) FROM metadata NOT INDEXED;"

Y, si decido seguir esa línea, observar:

sqlite3 /Volumes/Nextcloud/.sync_2f4a44904d04.db "PRAGMA page_count; PRAGMA freelist_count;"

No recomiendo convertir VACUUM en una receta alegre.

La base pertenece al cliente de Nextcloud. Manipularla sin medir antes sería cambiar una investigación por una superstición con sintaxis SQL. Si el problema reaparece, habrá que medir antes de tocar. Sólo entonces tendrá sentido valorar una compactación controlada de la base o una reconstrucción del journal siguiendo herramientas y criterios propios del cliente.

Qué aprendí de este caso

Este caso dejó varias conclusiones útiles.

Una aplicación que parece colgada puede estar simplemente esperando.

Desde fuera el síntoma es el mismo: no responde. Pero internamente no es igual un proceso muerto que un proceso esperando lecturas lentas.

La pila de llamadas no identifica automáticamente al culpable.

Que SQLite aparezca en la muestra no significa que SQLite esté roto.

Las lecturas pequeñas pueden ser devastadoras si la latencia es alta.

4 KiB parecen poca cosa. Repetidos miles de veces, con latencias de décimas de segundo, se convierten en minutos.

El caché puede ocultar o revelar el problema.

Después de leer la base con dd, Nextcloud arrancó porque las páginas estaban calientes en memoria. Eso no reparó nada, pero ayudó a entender el comportamiento.

Un experimento de campo puede ser impuro y aun así útil.

Cambiar de APFS a HFS+ no aisló una sola variable, pero produjo una mejora clara que no se podía ignorar.

Una recaída no siempre invalida la mejora anterior.

El episodio con CCC no demuestra que HFS+ haya fracasado. Puede indicar que el cliente sigue siendo vulnerable cuando intenta abrir su base justo después o durante una carga intensa del volumen.

Resolver el síntoma no siempre cierra la causa raíz.

Nextcloud volvió a arrancar. El volumen mejoró. El trabajo pudo continuar. Pero el mecanismo exacto sigue bajo observación.

Medir antes de tocar evita fabricar supersticiones técnicas.

Sin sample, fs_usage y las pruebas sobre la base, habría sido demasiado fácil culpar al primer componente sospechoso.

Continuará

Por ahora, el paciente ha despertado.

El volumen HFS+ monta rápido. Nextcloud Desktop arranca en segundos cuando el volumen no está bajo presión. La base SQLite, que durante días parecía el principal sospechoso, queda de momento como testigo protegido… Pero la historia no está cerrada.

Si dentro de unas semanas el arranque vuelve a degradarse sin CCC de por medio, sabré que HFS+ quizá sólo compró tiempo y que el verdadero enemigo puede ser la degradación progresiva del journal de sincronización.

Si no vuelve salvo cuando CCC secuestra el volumen, entonces el crimen coral tendrá otra lectura: no un asesino único, sino una escena compartida donde cada actor puso una parte del desastre.

De momento, la única conclusión segura es esta:

Nextcloud no estaba muerto.

Estaba esperando 4 KiB cada vez.

Continuará...