El error del primer cambio del día casi nunca es mala suerte
Por Dorian Chávez · fundador y CEO de Hábil y Fernando Mendez Carmona · Ingeniero backend de Hábil ·
Un error que aparece de vez en cuando, en el primer cambio del día, suele ser una conexión dormida. Cómo demostrarlo, arreglarlo y evitar datos rotos.
Un error que pasa «de vez en cuando, en el primer cambio del día» casi nunca es mala suerte. Suele ser una conexión que se quedó dormida, un caché que guardó algo que ya no sirve o un reloj que nadie recuerda. Y se puede demostrar antes de arreglarlo.
Imagine una mañana cualquiera en una tienda en línea. La persona que cuida el catálogo llega, abre el sistema y hace el primer cambio del día: corrige la descripción de un producto y lo guarda. Todo parece normal. Media hora después, alguien de ventas escribe preocupado: ese producto aparece en el buscador de la tienda sin precio y sin existencias. O peor, ya no aparece.
Nadie tocó el precio. Nadie borró nada. La persona del catálogo vuelve a abrir el producto y lo guarda otra vez sin cambiar nada. El producto regresa completo. Se anota como «un detalle» y se sigue con el día. La próxima vez que pasa, días o semanas después, alguien lo vuelve a «reparar a mano».
En el caso que contamos aquí, quienes lo vivían calculan que convivieron con esa falla mucho tiempo. Cuando por fin alguien la contó, en pruebas, salió varias veces en un solo mes. Y siempre tenía la misma forma: un solo producto, el primero que se editaba en el día.
Este artículo es para quien dirige operación o tecnología y para el equipo que mantiene los sistemas. Cubre cuatro cosas:
- Qué pasaba en realidad.
- Por qué estos errores sobreviven años.
- Cómo se demuestra y se corrige: reproducir la falla a propósito, arreglar la causa, impedir que un fallo escriba datos rotos y validar comparando lo viejo contra lo nuevo.
- Qué preguntar en su próximo comité y por dónde empezar esta semana.
Qué pasaba en realidad
El buscador de una tienda en línea casi nunca lee directo de la base de datos donde vive el catálogo. Lee de un índice de búsqueda: una copia del catálogo organizada para encontrar rápido. Entre las dos hay un proceso que copia cada cambio. Puede armarse, por ejemplo, con MariaDB como base, Logstash como el proceso que copia y Elasticsearch como el índice. Ese proceso trabaja todo el día: cada minuto pregunta «¿qué cambió?» y lleva los cambios al índice.
Para armar cada producto, el proceso tiene que hacer una segunda consulta a la base: traer sus variantes, es decir, las versiones de un mismo producto, cada una con su precio y su existencia. Para no abrir una conexión nueva cada vez, guarda una abierta y la reutiliza.
El problema empieza de noche. Nadie edita productos, así que esa conexión se queda quieta muchas horas. Y la base de datos, por diseño, cierra las conexiones que llevan mucho tiempo sin actividad. En MariaDB y en MySQL, para las conexiones de programas (no las de una persona en consola), ese tiempo se llama wait_timeout y, si nadie lo cambia, vale 28,800 segundos: ocho horas [1][2]. El manual de MySQL lo dice sin rodeos: por defecto, el servidor cierra la conexión después de ocho horas si no ha pasado nada [3].
La base cierra su lado, pero el proceso no se entera. Sigue guardando la conexión como si estuviera viva. A la mañana siguiente llega el primer cambio, el proceso usa esa conexión muerta y la consulta de variantes falla. El cliente de la base solo descubre que la conexión ya no existe cuando intenta usarla [3]. En Java, con el controlador de MySQL (la pieza de software que habla con la base en nombre del programa), el mismo evento sale como «Communications link failure». El propio controlador sugiere la causa: un tiempo sin uso más largo que el wait_timeout del servidor [4].
Hasta aquí sería un error que se reintenta y ya. Lo que lo vuelve caro es lo que pasa después.
El fallo que escribe encima del dato bueno
Cuando la consulta de variantes falla, un filtro como jdbc_streaming de Logstash no detiene el producto. Le agrega una etiqueta de error y le pone el valor por defecto en el campo de variantes. Si nadie configuró ese valor, el campo queda como una lista con un solo elemento vacío: el producto queda sin datos de variantes. Y el filtro lo deja seguir su camino [7]. El código del filtro lo muestra con claridad; la documentación solo lo menciona de pasada, al describir el caché (cache_expiration) [5].
El producto, ahora sin variantes, llega al índice de búsqueda. Y el índice lo guarda con el mismo identificador que ya tenía el producto bueno. Ese detalle, que normalmente es una virtud, aquí es el daño. Con la acción de guardado habitual (index) sobre un índice ordinario, guardar un documento con un identificador que ya existe actualiza el documento completo e incrementa su versión [15][16]. No queda un producto roto junto al bueno; queda el roto en lugar del bueno.
En el caso, el segundo cambio del día ya no fallaba: lo más probable es que, tras el error, se descartara la conexión muerta y se abriera otra. Eso depende del controlador y del administrador de conexiones, no del filtro; conviene confirmarlo en la bitácora. Por eso la siguiente edición «lo arreglaba» y el error solo le pegaba al primero.
Tres sospechosos de siempre: una conexión, un caché o un reloj
El caso tiene nombres propios, pero el patrón no. Cuando un error aparece «de vez en cuando» y casi siempre a la misma hora del día, o después de un fin de semana, conviene buscar algo que tenga un temporizador.
Una conexión dormida. El corte puede venir de tres lugares, y el cliente solo se entera cuando vuelve a usar la conexión:
- La base de datos, como en este caso: ocho horas por defecto en MariaDB y MySQL [1][2].
- La red. En Amazon Web Services (AWS), por ejemplo, la puerta de salida a internet de una red privada (NAT Gateway) da por expirada una conexión que pasa 350 segundos o más sin actividad. A quien intente seguir usándola le responde con un paquete de reinicio de TCP (RST), que corta la conexión [11]. El balanceador de red de AWS, que reparte las conexiones entre varios servidores, también corta a los 350 segundos por defecto. En conexiones TCP ese valor se puede configurar entre 60 y 6,000 segundos [12]. Un firewall, el equipo que filtra el tráfico entre dos redes, puede hacer lo mismo sin avisar a nadie. El Internet Storm Center de SANS documentó en 2007 un caso así: un firewall borraba las sesiones con una hora sin uso entre un servidor de aplicaciones y una base Oracle [13].
- El sistema operativo. El mecanismo que mantiene vivas las conexiones (keepalive de TCP) depende del sistema operativo y de cómo se abrió cada conexión. En el caso de SANS empezaba a las dos horas, demasiado tarde para el firewall [13].
Un caché. Un caché es una memoria de corto plazo: guarda una respuesta para no volver a pedirla. Si guarda una respuesta vacía o equivocada, la repite hasta que vence. Un filtro como jdbc_streaming, por ejemplo, con su caché encendido (viene así por defecto), puede hacerlo. Ante un error pasajero, guarda el resultado vacío para esos parámetros hasta que vence su tiempo, cinco segundos por defecto [5][7].
Un reloj. Muchos procesos de copia deciden qué leer con una hora: «tráeme lo que cambió después de la última vez». Esa hora se llama marca de agua [6], y tiene una trampa que veremos abajo: no siempre dice lo que uno cree.
Por qué estos errores sobreviven años
Un director puede preguntarse, con razón, cómo un error que todos han visto puede durar tanto. La respuesta casi nunca es descuido. Es que este tipo de error tiene cuatro rasgos que lo protegen:
- Se arregla solo. La siguiente edición deja el producto bien. Un error que se corrige solo destruye su propia evidencia: cuando alguien de tecnología llega a revisar, ya no hay nada que ver.
- Nadie lo reproduce. Para verlo hay que esperar una noche entera sin actividad y hacer un cambio justo después. Nadie hace eso en un día normal de trabajo. Así, el error nunca aparece cuando alguien lo está buscando.
- Se repara a mano y se olvida. Volver a guardar el producto toma un minuto. Sin reporte no hay conteo, y sin conteo no hay prioridad.
- El síntoma aparece lejos de la causa. Quien ve el problema es ventas, en el buscador. La causa está en un acuerdo de tiempos entre dos máquinas que nadie había escrito. Ventas no sabe que existe esa conexión, y quien la configuró no ve el buscador.
Cómo se demuestra y se corrige
La tentación, cuando por fin alguien sospecha la causa, es cambiar la configuración y esperar. Si el error deja de aparecer, se da por resuelto. El problema es que «dejó de aparecer» es exactamente lo que ya pasaba entre una falla y la siguiente. Una falla intermitente se demuestra antes de arreglarse. Estos son los cuatro pasos que seguimos.
1. Reproducirla a propósito
Si la falla depende de un reloj, se acorta el reloj. En un ambiente de prueba, el equipo baja el wait_timeout de la base a, digamos, 60 segundos. Hay un detalle que mucha gente pasa por alto: cada conexión toma ese valor en el momento en que se abre [1][2]. Bajar el valor general no afecta a las conexiones que ya existen. Por eso, lo práctico es reiniciar el proceso de copia, para que su conexión nazca con el valor corto.
Después se espera un par de minutos, se edita un producto y se mira. En la bitácora del proceso aparece la etiqueta de error del filtro; en el índice, el producto sin variantes. Una prueba de minutos sustituye a una espera de ocho horas.
La prueba que convence a todos es la del tiempo real. En una prueba controlada, después de más de 10 horas sin actividad, la primera edición falló y la siguiente salió bien. Era el síntoma exacto que el negocio veía, provocado a propósito y con hora.
2. Arreglar la causa, no el síntoma
Hay tres respuestas estándar a una conexión que se duerme, y son las mismas en casi cualquier tecnología: validarla antes de usarla, renovarla antes de que caduque o mantenerla despierta con tráfico periódico.
En un filtro como jdbc_streaming de Logstash, por ejemplo, la primera es una opción: jdbc_validate_connection, que viene apagada por defecto [5]. Al encenderla, el proceso revisa la conexión antes de usarla. Si está muerta, la descarta y abre otra [8][9].
Aquí está el matiz que casi nadie lee. La opción que la acompaña, jdbc_validation_timeout, viene documentada como «cada cuánto validar» y vale 3,600 segundos por defecto [5]. En realidad funciona distinto: la librería que maneja las conexiones solo revisa las que llevan más de ese tiempo sin usarse [9]. Una conexión que pasó la noche quieta lleva mucho más de una hora inactiva. Así que con el valor por defecto sí se revisa, y el corte de ocho horas de la base queda cubierto.
Pero el corte puede venir de la red y ser más corto, como los 350 segundos de un balanceador o de un NAT. Ahí una conexión con diez minutos sin uso no se revisaría y llegaría muerta. Contra ese corte hace falta otra medida: bajar ese umbral por debajo del corte, o mantener la conexión viva con tráfico más frecuente que el corte [11].
En Java, el principio es el mismo. HikariCP, un administrador de conexiones muy usado, recomienda que la vida máxima de una conexión sea unos segundos más corta que cualquier límite de la base o de la infraestructura. También ofrece mantenerlas vivas con una verificación periódica [10]. Y el controlador de MySQL para Java desaconseja su opción de reconexión automática [4]: reconectar a ciegas no es la respuesta oficial.
3. Impedir que un fallo escriba datos rotos
Arreglar la conexión evita el error de cada mañana. No evita el siguiente error que nadie ha imaginado. Por eso el segundo cambio importa tanto como el primero: si una parte del producto no se pudo traer, no se escribe nada.
Con Logstash, por ejemplo, eso se logra con una condición después del filtro. Si el producto trae la etiqueta de error de la consulta (_jdbcstreamingfailure), se descarta y no llega al índice [7]. El producto anterior, completo, se queda donde estaba.
Hay que distinguir dos casos que se parecen. El producto que falló lleva dos etiquetas: la de error y la de «se usó el valor por defecto» (_jdbcstreamingdefaultsused) [7]. Un producto que de verdad no tiene variantes lleva solo la segunda. Por eso la condición mira únicamente la de error: el producto sin variantes se sigue copiando, a propósito, y solo el que falló al consultarlas se detiene.
Esta decisión tiene un costo, y hay que decirlo. Si la consulta solo pide lo posterior a la marca de agua y la marca ya avanzó, el cambio descartado no se recupera solo. Así funcionaba en el caso; en su sistema, confírmelo con la consulta real. El producto queda con su versión anterior hasta que alguien lo vuelva a editar o se recargue el índice. Para un director, la regla se entiende sin código: mejor el precio de ayer que ningún precio. Un producto con el dato de ayer se vende; uno sin precio desaparece.
La lección de fondo, para el equipo técnico: usar el identificador del producto como identificador en el índice hace que repetir una carga sea inofensivo, porque el resultado final es el mismo [15]. Esa misma propiedad es la que hace daño cuando el documento viene incompleto. Que una carga se pueda repetir sin daño no la vuelve segura: también hace falta que no escriba cuando algo falta.
4. Revisar el reloj y validar contra lo viejo
El proceso de copia decide qué copiar con una marca de agua: «tráeme lo que cambió después de la última hora que vi» [6].
La trampa es que la hora que se guarda como «última modificación» no siempre es la hora en que el cambio quedó guardado. Dos términos ayudan. Una sentencia es una instrucción que se le da a la base, como «cambia este precio». Una transacción es un paquete de sentencias que se guarda todo junto o no se guarda; confirmarla es el momento en que su cambio queda guardado y lo pueden ver los demás.
En MySQL, NOW() devuelve la hora en que empezó la sentencia, no la hora en que se confirmó [14]. Suponga que la columna de última modificación se llena con NOW(), y que un cambio grande tarda en guardarse: empieza a las 10:00 y confirma a las 10:03. Sus filas dicen «10:00». Mientras tanto, el proceso de copia ya leyó cambios más nuevos y movió su marca más allá. Cuando el cambio grande por fin confirma, sus filas traen una hora anterior a la marca, y la siguiente lectura ya no las pide. Se pierden en silencio. Este riesgo es de la misma familia de límites que señala el equipo de Debezium sobre la captura de cambios por consulta periódica: puede perder cambios intermedios y no ve los borrados [17].
Hay dos formas de cerrar esa puerta:
- Un margen de tiempo. «Solo leo lo que tiene al menos un minuto de antigüedad.» La marca ya no avanza sobre segundos que todavía pueden recibir cambios. Reduce el riesgo, pero no es una garantía: una transacción que tarde más de un minuto en confirmar sigue pudiendo perderse.
- Leer la bitácora de la base en lugar de una columna con hora. Es lo que se llama captura de cambios por bitácora (CDC, por sus siglas en inglés). Un ejemplo es Debezium leyendo el registro binario de MariaDB o MySQL. Ese registro guarda las operaciones en el orden en que se confirmaron, incluidos los borrados [17][18]. Bien configurado, cierra esta ventana en particular, a cambio de más piezas que configurar y operar.
En el caso que contamos se eligió el margen de un minuto, con su límite escrito.
Queda la pregunta que todo director debería hacer: ¿cómo sabemos que el arreglo no rompió nada que antes funcionaba? La respuesta del caso fue una comparación completa. La consulta vieja y la nueva se corrieron sobre todo el catálogo, en pruebas, y dieron exactamente el mismo resultado. La consulta nueva solo cambió cuándo lee. Cualquier diferencia habría sido un error nuevo. No la hubo.
Y después, otra vez la prueba de la noche. En una prueba controlada, después de más de 16 horas sin actividad, la primera edición entró sin error: el caso exacto que antes fallaba. Además, los productos que habían quedado rotos se repararon, y al cierre de la revisión no quedaba ninguno roto en pruebas.
Cinco preguntas para su próximo comité
- ¿Qué errores «se arreglan solos» en nuestros sistemas? Pregunte a ventas, a atención a clientes y a quien opera el catálogo, no solo a tecnología. Las mejores pistas suelen estar en quien repara a mano.
- ¿Alguno pasa siempre a la misma hora, el lunes, o después de una noche sin uso? Si la respuesta es sí, hay un reloj detrás.
- ¿Sabemos cuánto tiempo aguanta sin uso cada conexión entre nuestros sistemas, y quién la corta primero: la base, la red o la nube? Si nadie lo tiene escrito, nadie lo ha decidido.
- Cuando un proceso no puede traer toda la información, ¿qué hace? ¿Deja lo que había o publica algo a medias?
- Cuando tecnología dice «ya quedó», ¿cómo lo demostró? ¿Provocó la falla antes del arreglo y la volvió a provocar después, o solo dejó de verla?
Si dos o más respuestas son «no sé», el problema no es de suerte: falta medir.
Por dónde empezar esta semana
No hace falta un proyecto. Hace falta una lista. Pida a las áreas que operan sus sistemas que anoten, durante dos semanas, cada vez que algo «se arregló solo» o «se reparó a mano»: qué fue, a qué hora y qué se hizo. Al final de las dos semanas, ordene la lista por hora. Si varias entradas se juntan a primera hora de la mañana o después de un fin de semana, ya tiene a su primer sospechoso. A su equipo técnico le basta una tarde para empezar a reproducirlo en un ambiente de prueba.
Qué hace Hábil
Cuando una falla se repite, el Equipo SWAT de Hábil busca la causa de fondo, no otro reinicio: la reproduce, la corrige y le entrega evidencia de cómo se verificó, para que usted la pueda revisar.
La primera conversación es sin costo: su objetivo es ayudarle a ubicar qué falla se repite, desde cuándo y cómo se puede demostrar. Escríbanos por WhatsApp.
Referencias
- MariaDB, Server System Variables,
wait_timeout(28,800 s por defecto; el valor de sesión se inicializa al arrancar la conexión, desde el global o desdeinteractive_timeout). https://mariadb.com/docs/server/server-management/variables-and-modes/server-system-variables#wait_timeout - MySQL 8.4 Reference Manual, Server System Variables,
wait_timeout(28,800 s; el valor de sesión se copia del global al iniciar cada conexión). https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html#sysvar_wait_timeout - MySQL 8.4 Reference Manual, MySQL server has gone away (cierre tras ocho horas sin actividad; errores
CR_SERVER_GONE_ERRORyCR_SERVER_LOST). https://dev.mysql.com/doc/refman/8.4/en/gone-away.html - MySQL Connector/J, archivo de mensajes (rama
release/9.x, commit1363d9b, consultado el 20-oct-2026) del controlador («Communications link failure»; sugerencia sobrewait_timeout;autoReconnectdesaconsejado). https://github.com/mysql/mysql-connector-j/blob/1363d9bf5412c9adb246756a8334ee2d584a3c8f/src/main/resources/com/mysql/cj/LocalizedErrorMessages.properties - Elastic, Jdbc_streaming filter plugin (
jdbc_validate_connection,jdbc_validation_timeout,tag_on_failure,default_hash; encache_expiration, que ante errores pasajeros el caché guarda resultados vacíos y se aplica eldefault_hash). https://www.elastic.co/docs/reference/logstash/plugins/plugins-filters-jdbc_streaming - Elastic, Jdbc input plugin (marca de agua con
:sql_last_value,tracking_column,schedule; recomendación de validar conexiones entre corridas). https://www.elastic.co/docs/reference/logstash/plugins/plugins-inputs-jdbc - logstash-integration-jdbc 5.6.6 (commit
f7586e6, consultado el 20-oct-2026), código del filtro y de su manejador de consultas (en un error: etiqueta de error, etiqueta de valor por defecto,default_hashy el evento sigue; caché del resultado vacío). https://github.com/logstash-plugins/logstash-integration-jdbc/blob/f7586e67912852d5c5a6ec0f4db5f4a4f30e7806/lib/logstash/filters/jdbc_streaming.rb · https://github.com/logstash-plugins/logstash-integration-jdbc/blob/f7586e67912852d5c5a6ec0f4db5f4a4f30e7806/lib/logstash/plugin_mixins/jdbc_streaming/statement_handler.rb - logstash-integration-jdbc 5.6.6 (commit
f7586e6), código del módulo de conexión (activa el validador de Sequel conjdbc_validation_timeout). https://github.com/logstash-plugins/logstash-integration-jdbc/blob/f7586e67912852d5c5a6ec0f4db5f4a4f30e7806/lib/logstash/plugin_mixins/jdbc_streaming.rb - Sequel, extensión
connection_validator(valida al sacar la conexión del grupo, solo si lleva inactiva más que el umbral). https://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/connection_validator_rb.html - HikariCP, README (
maxLifetime,keepaliveTime: verificación periódica conConnection.isValid()o una consulta de prueba en controladores viejos). https://github.com/brettwooldridge/HikariCP - AWS, Troubleshoot NAT gateways, conexiones que expiran tras 350 s sin actividad y respuesta con RST. https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-troubleshooting.html#nat-gateway-troubleshooting-timeout
- AWS, Update the idle timeout for your Network Load Balancer (350 s por defecto; configurable de 60 a 6,000 s solo en escuchas TCP; en TLS es fijo en 350 s). https://docs.aws.amazon.com/elasticloadbalancing/latest/network/update-idle-timeout.html
- SANS Internet Storm Center, Bojan Zdrnja, «Gone in 3600 seconds: story about TCP Keep-Alives» (8-nov-2007). https://isc.sans.edu/diary/3623
- MySQL 8.4 Reference Manual, Date and Time Functions,
NOW()(hora de inicio de la sentencia). https://dev.mysql.com/doc/refman/8.4/en/date-and-time-functions.html#function_now - Elastic, Elasticsearch output plugin (
document_idpara sobrescribir documentos con el mismo identificador). https://www.elastic.co/docs/reference/logstash/plugins/plugins-outputs-elasticsearch - Elastic, Index API (indexar con un identificador existente actualiza el documento completo e incrementa su versión). https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-index
- Gunnar Morling, «Five Advantages of Log-Based Change Data Capture», blog de Debezium (19-jul-2018). https://debezium.io/blog/2018/07/19/advantages-of-log-based-change-data-capture/
- Debezium, conectores de MySQL y de MariaDB (lectura del registro binario en orden de confirmación). https://debezium.io/documentation/reference/stable/connectors/mysql.html · https://debezium.io/documentation/reference/stable/connectors/mariadb.html
¿Su operación tiene estos desafíos?
¿Prefiere correo? Escríbanos a hola@habil.mx