Tu pod de MySQL “parece estar bien” hasta que el nodo se reinicia, Kubernetes hace lo que fue diseñado para hacer, y de repente la base de datos entra en un bucle de reinicios con un volumen que se niega a montarse. Mientras tanto, el equipo de aplicaciones lo llama “un problema de Kubernetes” y el equipo de plataforma lo considera “un problema de base de datos”. Felicidades: ahora eres el adulto en la sala.
Esta es una guía práctica, un poco opinada, para ejecutar MySQL y MariaDB en Kubernetes sin convertir probes en un denegación de servicio, reinicios en corrupción, ni la “mentalidad sin estado” en pérdida de datos. Hablaremos de qué se rompe, por qué se rompe y los comandos que realmente ejecutarás a las 2 a.m.
Lo que Kubernetes realmente le hace a tu base de datos (y por qué duele)
Kubernetes es constante en una cosa: es indiferente. Reiniciará tu proceso si el probe falla. Moverá tu pod si el nodo se drena. Matará contenedores cuando los recursos sean escasos. También hará esto mientras tu base de datos está en medio de un flush, en recuperación o en medio de una transacción, porque no tiene el concepto de “esto es intocable y tiene un write-ahead log”.
Las bases de datos son máquinas de estado con invarianzas costosas. Las invarianzas de InnoDB giran en torno a los logs redo/undo, doublewrite (dependiendo de la configuración) y fsync duradero. MariaDB y MySQL comparten mucho ADN aquí, pero la presión de Kubernetes expone los bordes de forma diferente—especialmente alrededor del tiempo de arranque, comportamiento de probes y pilas de replicación que puedas usar (replicación asíncrona, semi-sync o Galera).
Los tres comportamientos de Kubernetes que más importan para servicios con estado:
- Los probes son compuertas de tráfico y botones de muerte. Readiness decide si recibes tráfico. Liveness decide si sigues vivo. Startup existe porque liveness puede comportarse como un matón.
- Los reinicios son “normales”. No puedes tratarlos como excepciones. Tu base de datos debe poder reiniciarse de forma segura y rápida, repetidamente.
- El almacenamiento se adjunta y desapega por humanos (y controladores). Retrasos en el montaje de volúmenes, adjuntos obsoletos y fsync lento pueden dominar todo.
Si solo recuerdas una verdad operativa: para sistemas con estado, Kubernetes no es “autocurativo”, es “reintenta por sí mismo”. Tu trabajo es hacer que los reintentos sean seguros.
MySQL vs MariaDB: lo que importa en Kubernetes
MySQL y MariaDB están lo suficientemente cerca como para adormecer a los equipos y hacerles asumir equivalencia operativa. Esa suposición es la semilla de varios incidentes muy costosos.
Comportamiento del motor central: mayormente similar, pero vigila los valores por defecto y casos extremos
En despliegues típicos de Kubernetes ejecutarás InnoDB en ambos. La recuperación tras crash existe en los dos. Ambos pueden recuperarse de una terminación abrupta. Pero el diablo está en:
- Variación en la duración del arranque. Tras un cierre no limpio, el tiempo de recuperación de InnoDB puede oscilar entre segundos y “tu liveness probe ahora es un arma”. La recuperación depende del tamaño del redo log, la edad del checkpoint, el rendimiento de IO y el comportamiento de fsync en tu StorageClass.
- Diferencias en las herramientas cliente. Algunas imágenes incluyen
mysqladmin, otras incluyen wrappersmariadb-admin, y algunas traen clientes mínimos. Los probes y scripts init que hardcodean una herramienta tienden a fallar en la otra en el peor momento. - Detalles de GTID y replicación. El GTID de MariaDB no es el GTID de MySQL; no son compatibles por defecto. Si estás migrando o mezclando herramientas, el “es solo GTID” se convierte en una noche larga.
Pilas de replicación: asíncrona vs Galera cambia cómo deben comportarse los probes
“MySQL en Kubernetes” suele significar un primario más réplicas (replicación asíncrona), o un clúster gestionado por un operador. “MariaDB en Kubernetes” puede significar lo mismo, o Galera (wsrep) porque es fácil vender “multi-primario” a la gerencia.
Los probes para replicación asíncrona pueden ser simples: ¿está mysqld vivo y aceptando conexiones y (para readiness) la replicación está razonablemente al día? Los probes para Galera deben considerar el estado del clúster: un nodo puede aceptar TCP y aún así ser inseguro para servir escrituras (o incluso lecturas) según si está en estado donor/desync.
Operadores e imágenes: la preparación para Kubernetes es una decisión de producto
Tu experiencia operativa a menudo está determinada más por la elección del operador/imagen que por la marca de la base de datos. Algunos operadores implementan startupProbes sensatas, hooks de terminación graceful y gates de recuperación. Otros traen valores optimistas por defecto y te permiten aprender con fuego.
Mi opinión: si ejecutas servicios stateful sin un operador con opinión (o un chart interno cuidadosamente mantenido), efectivamente has elegido “base de datos mascota sobre orquestación de ganado”. Eso no es una estrategia; es una vibra.
Hechos interesantes y contexto histórico (las partes que la gente olvida)
- MariaDB se creó en 2009 como un fork comunitario después de que Oracle adquiriera Sun Microsystems (y por tanto MySQL).
- El conjunto de caracteres por defecto de MySQL cambió con el tiempo (notablemente hacia
utf8mb4en versiones modernas), y esto impacta la compatibilidad de esquemas y la longitud de índices en actualizaciones reales. - La implementación de GTID de MariaDB es diferente de la de MySQL; las herramientas que asumen la semántica de GTID de MySQL pueden diagnosticar mal la salud de la replicación en MariaDB.
- La replicación Galera se convirtió en una “feature destacada” de MariaDB para muchas empresas, pero es un modelo de fallos distinto a la replicación asíncrona: la pertenencia y el quórum son requisitos operativos, no meros extras.
- Kubernetes añadió startupProbe relativamente tarde (comparado con liveness/readiness), en gran parte porque demasiadas cargas reales tenían arranques lentos pero correctos que estaban siendo asesinadas.
- El costo de recuperación de InnoDB no es lineal con el tamaño de los datos; está ligado al volumen de redo y al comportamiento de checkpoints, por eso una “base de datos pequeña” puede reiniciarse dolorosamente lento tras stalls de IO.
- El comportamiento de fsync cambió entre generaciones de almacenamiento en la nube; lo que era “aceptable” en un SSD local puede convertirse en una tormenta de reinicios en volúmenes con red y mayor variación de latencia.
- MySQL 8 introdujo un diccionario de datos transaccional, lo que mejoró la consistencia pero también hizo que algunos comportamientos de actualización/reversión y recuperación fueran distintos respecto a líneas antiguas de MySQL y a MariaDB.
Readiness, liveness y startup probes que no te saboteen
Los probes son donde Kubernetes toca tu base de datos cada pocos segundos. Bien hechos, evitan que el tráfico llegue a un pod enfermo. Mal hechos, crean la enfermedad.
Reglas prácticas (opinadas, porque las necesitas)
- Nunca uses liveness para comprobar “si la base de datos está lógicamente sana”. Liveness debe responder: “¿Está el proceso atascado más allá de la recuperación?” Si haces que liveness dependa de lag de replicación o una consulta SQL compleja, matarás pods perfectamente recuperables.
- Readiness puede ser estricto. Readiness es el lugar correcto para impedir tráfico basado en “¿puede servir consultas ahora?” y (opcionalmente) “¿está suficientemente al día?”.
- Usa startupProbe para ventanas de recuperación. Si no lo haces, liveness asesinará tu recuperación InnoDB. Entonces reiniciará, volverá a entrar en recuperación y será asesinado de nuevo. Ese bucle es un clásico.
- Prefiere probes exec usando el socket local cuando sea posible. Las sondas TCP pueden pasar mientras SQL está bloqueado. Las sondas SQL pueden fallar cuando el DNS es lento. Las comprobaciones basadas en socket local reducen las piezas móviles.
Cómo debe ser un buen check de readiness
Readiness debe ser barato y determinista. Un patrón común: conectarse localmente y ejecutar SELECT 1. Si eres una réplica, opcionalmente verifica el lag de replicación. Si estás en Galera, comprueba el estado wsrep.
Evita consultas “costosas de verdad” como escanear tablas grandes o sondear metadatos que bloquean. Quieres “puede aceptar una consulta trivial y devolver rápido”. Eso se correlaciona bien con la experiencia del usuario y no colapsará tu pod bajo la carga de probes.
Cómo debe ser un buen check de liveness
Liveness debe ser conservador. Para MySQL/MariaDB, un liveness decente es un ping local usando herramientas de administración con un timeout corto. Si no puede responder a un ping durante N comprobaciones consecutivas, algo está atascado.
Startup probes: gana tiempo para la recuperación
Tras un cierre no limpio, InnoDB legítimamente puede tardar minutos en recuperarse. Tus probes deben conceder ese tiempo. Esto no es “ser indulgente”. Es corrección.
Broma #1: Un liveness probe que mata a mysqld durante la recuperación por crash es como comprobar el pulso de un paciente desconectando el ventilador.
Timeouts de probe: el asesino silencioso
Muchas fallas de probes no son “base de datos caída”, son “timeout de probe demasiado corto para la latencia ocasional del almacenamiento”. Si tu PVC está respaldado por almacenamiento en red, un timeout de 1 segundo es un generador de fallos.
Reinicios, semántica de apagado y recuperación de InnoDB
La terminación en Kubernetes es una señal más una fecha límite. Envía SIGTERM, espera terminationGracePeriodSeconds, luego SIGKILL. Tu base de datos necesita suficiente gracia para flush, actualizar estado y salir limpiamente.
Un apagado ordenado te compra un arranque más rápido
Apagado limpio: menos redo logs que aplicar, arranque más rápido, menos mensajes de log alarmantes, menos tiempo en “no listo”. Apagado no limpio: tiempo de recuperación que depende del IO, que no puedes controlar durante un evento de nodo.
Los bucles de reinicio suelen ser errores de política de probes
Si un pod se reinicia repetidamente y los logs muestran que la recuperación por crash se inicia cada vez, tus probes son demasiado agresivos o no usas startupProbe. Kubernetes no está “curando” tu BD; está interrumpiendo la curación.
Una idea de confiabilidad (parafraseada)
“La esperanza no es una estrategia.” — idea parafraseada comúnmente atribuida a ingenieros y operadores en círculos de confiabilidad. Trátala como guía: diseña para fallos, no los desees fuera.
Almacenamiento y seguridad de datos: PVCs, sistemas de archivos y el impuesto fsync
Si quieres que una base de datos sea durable, debes pagar por escrituras durables. Kubernetes no cambia eso. Solo hace que la factura sea más confusa porque la parte lenta ahora es un StorageClass.
Semánticas de PVC que importan
- Modo de acceso (RWO vs RWX): La mayoría de volúmenes de bases de datos deben ser RWO. RWX en sistemas de archivos de red puede funcionar, pero el rendimiento y las semánticas de bloqueo varían; no pongas MySQL en NFS a la ligera esperando felicidad.
- Política de reclaim: “Delete” tiene su lugar, pero no deberías descubrirlo en producción después de borrar un StatefulSet.
- Retrasos en attach/mount: Un pod puede programarse rápido pero esperar mucho tiempo para el attach de volumen. Probes y timeouts deben tener esto en cuenta.
fsync y afines
Los controles de durabilidad (como innodb_flush_log_at_trx_commit y sync_binlog) interactúan con el almacenamiento subyacente. En SSD local de baja latencia, fsync por commit es manejable. En un volumen de red ocupado con picos de latencia, puede convertirse en un acantilado de rendimiento.
Bajar durabilidad para “arreglar rendimiento” es una decisión de negocio, aunque finjas que es técnica. Si relajas fsync, estás eligiendo cuánto dato puedes tolerar perder en un fallo de nodo.
Sistema de archivos y opciones de montaje
ext4 y XFS son las opciones habituales. Lo que importa más es la consistencia y la observabilidad: necesitas saber realmente qué estás ejecutando. Además, si usas sistemas de archivos overlay de formas raras, detente. Las bases de datos quieren almacenamiento en bloque aburrido.
Broma #2: Hay dos tipos de almacenamiento: el que benchmarkeas, y el que te benchmarkea en producción.
Tareas prácticas: comandos, salida esperada y la decisión que tomas
Estas son tareas reales que puedes ejecutar desde una máquina admin con acceso a kubectl, además de algunas comprobaciones dentro del contenedor. Cada una incluye lo que implica la salida y qué decisión debes tomar a continuación.
Tarea 1: Ver si estás ante un problema de probe o un crash
cr0x@server:~$ kubectl -n prod get pod mysql-0 -o wide
NAME READY STATUS RESTARTS AGE IP NODE
mysql-0 0/1 CrashLoopBackOff 7 18m 10.42.3.17 node-7
Significado: CrashLoopBackOff con múltiples reinicios sugiere que el proceso sale o liveness lo mata. Aún no hay suficiente información.
Decisión: Inspecciona events y logs previos; no ajustes configuraciones de MySQL al azar todavía.
Tarea 2: Leer eventos del pod para detectar si liveness/readiness te están matando
cr0x@server:~$ kubectl -n prod describe pod mysql-0 | sed -n '/Events:/,$p'
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Unhealthy 2m (x12 over 16m) kubelet Liveness probe failed: command timed out: context deadline exceeded
Normal Killing 2m (x12 over 16m) kubelet Container mysql failed liveness probe, will be restarted
Significado: El contenedor está siendo matado por timeout de liveness, no necesariamente se está colapsando por sí mismo.
Decisión: Añade/ajusta startupProbe y aumenta timeoutSeconds del probe. También revisa la latencia del almacenamiento antes de culpar a MySQL.
Tarea 3: Comparar logs actuales vs previos del contenedor
cr0x@server:~$ kubectl -n prod logs mysql-0 -c mysql --previous --tail=60
2025-12-31T01:12:17.812345Z 0 [Note] InnoDB: Starting crash recovery from checkpoint LSN=187654321
2025-12-31T01:12:46.991201Z 0 [Note] InnoDB: 128 out of 1024 pages recovered
2025-12-31T01:12:48.000901Z 0 [Note] mysqld: ready for connections.
2025-12-31T01:12:49.103422Z 0 [Warning] Aborted connection 12 to db: 'unconnected' user: 'healthcheck' host: 'localhost' (Got timeout reading communication packets)
Significado: Realmente alcanzó “ready for connections” y luego fue matado/falló comprobaciones por timeouts.
Decisión: Probablemente la consulta del probe es demasiado lenta o el timeout es muy corto bajo presión de IO. Arregla los probes primero; luego mide IO.
Tarea 4: Confirmar las definiciones de probe (te sorprendería saber cuántas están mal)
cr0x@server:~$ kubectl -n prod get pod mysql-0 -o jsonpath='{.spec.containers[0].livenessProbe.exec.command}{"\n"}{.spec.containers[0].readinessProbe.exec.command}{"\n"}{.spec.containers[0].startupProbe.exec.command}{"\n"}'
[mysqladmin ping -h 127.0.0.1 -uroot -p$(MYSQL_ROOT_PASSWORD)]
[mysql -h 127.0.0.1 -uroot -p$(MYSQL_ROOT_PASSWORD) -e SELECT 1]
[]
Significado: No hay startupProbe configurado. Liveness usa expansión de contraseña de forma que puede no funcionar según el manejo del shell.
Decisión: Añade startupProbe y prefiere un script wrapper que maneje secretos de forma segura. También evita poner contraseñas directamente en los argumentos del comando.
Tarea 5: Comprobar terminationGracePeriod (el apagado limpio importa)
cr0x@server:~$ kubectl -n prod get pod mysql-0 -o jsonpath='{.spec.terminationGracePeriodSeconds}{"\n"}'
30
Significado: 30 segundos suele ser demasiado corto para una instancia InnoDB ocupada que necesita apagarse limpiamente.
Decisión: Aumenta este valor (a menudo 120–300s según la carga) y asegúrate de que tu contenedor maneje SIGTERM correctamente.
Tarea 6: Verificar el PVC y si el volumen está atascado adjuntándose
cr0x@server:~$ kubectl -n prod get pvc -l app=mysql
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
data-mysql-0 Bound pvc-3d5b2c9a-6c62-4a9c-a0c2-1f2a2e6b9b8a 200Gi RWO fast-ssd
Significado: El PVC está Bound; el attach aún puede ser lento, pero al menos el aprovisionamiento no está fallando.
Decisión: Si el pod está Pending, revisa eventos de attach; si está en ejecución pero lento, pasa a diagnóstico de IO.
Tarea 7: Comprobar presión a nivel de nodo que dispare evictions o throttling
cr0x@server:~$ kubectl describe node node-7 | sed -n '/Conditions:/,/Addresses:/p'
Conditions:
Type Status LastHeartbeatTime Reason Message
---- ------ ----------------- ------ -------
MemoryPressure False 2025-12-31T01:15:10Z KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure True 2025-12-31T01:15:10Z KubeletHasDiskPressure kubelet has disk pressure
PIDPressure False 2025-12-31T01:15:10Z KubeletHasSufficientPID kubelet has sufficient PID available
Significado: DiskPressure True puede causar evictions y correlacionarse con stalls de IO.
Decisión: Arregla la presión de disco del nodo (GC de imágenes, limpieza de logs, disco raíz más grande). No ajustes MySQL para “resolver” la inanición de nodo.
Tarea 8: Ver reinicios del contenedor con códigos de salida (crash del proceso vs kill)
cr0x@server:~$ kubectl -n prod get pod mysql-0 -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}{" "}{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}'
137 Error
Significado: El código de salida 137 normalmente significa SIGKILL (a menudo kill por liveness u OOM kill).
Decisión: Revisa eventos para OOMKilled; de lo contrario trátalo como problema de probes/termination-grace.
Tarea 9: Comprobar si estás OOM-killing mysqld durante la recuperación
cr0x@server:~$ kubectl -n prod describe pod mysql-0 | grep -E 'OOMKilled|Reason:|Last State' -n
118: Last State: Terminated
119: Reason: OOMKilled
Significado: El límite de memoria es demasiado bajo para la carga o la fase de recuperación; InnoDB puede asignar picos (buffer pool, caches, buffers de orden según configuración).
Decisión: Aumenta límites de memoria, reduce el buffer pool de InnoDB y asegúrate de no usar buffers por conexión agresivos con muchas conexiones máximas.
Tarea 10: Inspeccionar rápidamente el estado vivo de MySQL/MariaDB (dentro del pod)
cr0x@server:~$ kubectl -n prod exec -it mysql-0 -c mysql -- bash -lc 'mysqladmin -uroot -p"$MYSQL_ROOT_PASSWORD" ping && mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW GLOBAL STATUS LIKE '\''Uptime'\'';"'
mysqld is alive
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Uptime | 43 |
+---------------+-------+
Significado: El servidor está arriba y responde. Si readiness sigue fallando, el probe de readiness está equivocado o es demasiado estricto.
Decisión: Alinea la lógica de readiness con lo que acabas de probar: conectividad básica más barreras mínimas de corrección.
Tarea 11: Comprobar indicadores de recuperación InnoDB y ajustes de durabilidad
cr0x@server:~$ kubectl -n prod exec -it mysql-0 -c mysql -- bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES WHERE Variable_name IN (\"innodb_flush_log_at_trx_commit\",\"sync_binlog\",\"innodb_doublewrite\");"'
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| innodb_doublewrite | ON |
| innodb_flush_log_at_trx_commit | 1 |
| sync_binlog | 1 |
+------------------------------+-------+
Significado: Este es el modo de durabilidad máxima. Bueno para seguridad de datos, potencialmente brutal en almacenamiento lento.
Decisión: Si la latencia es inaceptable, arregla el almacenamiento primero. Solo relaja estos parámetros si el negocio acepta explícitamente el riesgo de pérdida de datos.
Tarea 12: Medir lag de replicación (caso réplica asíncrona)
cr0x@server:~$ kubectl -n prod exec -it mysql-1 -c mysql -- bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW SLAVE STATUS\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running"'
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 187
Significado: La réplica está retrasada. Puede estar viva pero no lista para servir lecturas si tu app espera datos frescos.
Decisión: Usa readiness para bloquear tráfico según un umbral de lag apropiado para tu app. No uses liveness para esto.
Tarea 13: Medir estado wsrep (caso MariaDB Galera)
cr0x@server:~$ kubectl -n prod exec -it mariadb-0 -c mariadb -- bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW STATUS LIKE '\''wsrep_%'\'';" | egrep "wsrep_local_state_comment|wsrep_cluster_status|wsrep_ready"'
wsrep_cluster_status Primary
wsrep_local_state_comment Synced
wsrep_ready ON
Significado: El nodo está en el componente Primary, sincronizado y listo. Este es el estado que quieres antes de declarar readiness.
Decisión: Si wsrep_ready está OFF o el estado es Donor/Joining, mantén el pod NotReady para evitar lecturas/escrituras inconsistentes.
Tarea 14: Ver si el sistema de archivos es realmente lo que crees
cr0x@server:~$ kubectl -n prod exec -it mysql-0 -c mysql -- bash -lc 'df -T /var/lib/mysql | tail -n +2'
/dev/nvme1n1 ext4 205113212 83412228 111202456 43% /var/lib/mysql
Significado: ext4 sobre un dispositivo de bloque es típico. Si ves nfs o algo sorprendente, ajusta expectativas y probes.
Decisión: Si es un sistema de archivos de red, sé mucho más conservador con timeouts y considera cambiar de StorageClass para OLTP de producción.
Tarea 15: Vigilar síntomas de fsync lento en el estado de MySQL
cr0x@server:~$ kubectl -n prod exec -it mysql-0 -c mysql -- bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW GLOBAL STATUS LIKE \"Innodb_os_log_fsyncs\"; SHOW GLOBAL STATUS LIKE \"Innodb_log_waits\";"'
+---------------------+--------+
| Variable_name | Value |
+---------------------+--------+
| Innodb_os_log_fsyncs| 983211 |
+---------------------+--------+
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Innodb_log_waits | 1249 |
+------------------+-------+
Significado: Innodb_log_waits indica contención en el flush de logs; a menudo se correlaciona con latencia de IO o archivos/logs demasiado pequeños.
Decisión: Investiga latencia de almacenamiento y considera cambios de configuración de logs. Si corres en volúmenes ruidosos de red, arregla eso primero.
Manual de diagnóstico rápido: encuentra el cuello de botella antes de culpar al equivocado
Cuando un pod de base de datos está flapping o lento en Kubernetes, puedes perder horas debatiendo “BD vs plataforma”. No lo hagas. Sigue una secuencia que rápidamente te diga dónde está la realidad.
Primero: determina si Kubernetes lo está matando, o si se está colapsando
- Eventos del pod: fallos de liveness, OOMKilled, eviction, problemas de montaje de volumen.
- Código de salida: 137 (kill) vs una traza de crash de mysqld.
- Logs previos: ¿alcanzó “ready for connections”?
Segundo: determina si el almacenamiento es el cuello de botella
- Condiciones del nodo: DiskPressure, indicios de saturación de IO.
- Tiempo de arranque/recuperación: si solo falla durante ventanas de recuperación por crash, sospecha IO lento y probes demasiado agresivos.
- Indicadores de la base de datos: log waits, checkpoints bloqueados, lag de replicación que se correlaciona con carga de escritura.
Tercero: determina si tu lógica de probes está sobreajustada
- ¿La readiness depende del lag de replicación? Bien. ¿Pero el umbral es sensato para tu app?
- ¿La liveness depende del lag de replicación? Mal. Cámbialo.
- ¿El probe invoca al cliente correcto? Las imágenes de MariaDB y MySQL no siempre son simétricas.
Cuarto: verifica la política de apagado y reinicio
- ¿terminationGracePeriodSeconds lo bastante largo?
- ¿preStop hook para dejar de aceptar tráfico antes de SIGTERM?
- ¿startupProbe existe y está afinado para el peor caso de recuperación?
Tres micro-historias del mundo corporativo (todas muy reales)
Micro-historia 1: El incidente causado por una suposición equivocada
Una empresa SaaS mediana migró de VMs a Kubernetes por fases. Los primeros servicios movidos eran sin estado y todo salió bien. La confianza creció. Luego migraron su “réplica simple de MySQL” usada para reporting. El plan era copiar la configuración de la VM antigua, ponerla en un StatefulSet y añadir un readiness probe que comprobara el lag de replicación para proteger los dashboards de datos obsoletos.
La suposición equivocada: “Si la replicación está atrasada, el pod está enfermo.” El readiness probe se implementó por accidente como liveness—el mismo script, copiado en la sección equivocada. Funcionó en staging porque el dataset era pequeño y el lag rara vez excedía unos segundos.
En producción había ráfagas diarias de escrituras y un job de analytics que generaba picos de IO periódicos. Durante la ráfaga, el lag creció. El liveness falló. Kubernetes reinició la réplica. MySQL inició la recuperación por crash, que fue más lenta bajo la misma presión de IO. El liveness falló de nuevo. Bucle de reinicio. Los dashboards se rompieron, luego la aplicación empezó a fallar porque había estado usando silenciosamente esa réplica para algunas rutas de lectura.
La solución fue aburrida: mover las comprobaciones de lag a readiness, añadir startupProbe con un presupuesto generoso y dejar de enrutar lecturas críticas a una réplica sin tolerancia explícita a la desactualización. La lección incómoda del postmortem: la “réplica de reporting” nunca estuvo realmente aislada. Solo estaba aislada socialmente.
Micro-historia 2: La optimización que salió mal
Una gran empresa quería mayor throughput de escritura en MariaDB sobre Kubernetes. Ejecutaban ajustes durables y se quejaban de latencia de commits en horas punta. Alguien propuso relajar durabilidad: poner innodb_flush_log_at_trx_commit=2 y sync_binlog=0, porque “el almacenamiento es redundante de todos modos”.
El throughput mejoró. Las gráficas de latencia parecían mejores. La gente celebró y siguió adelante. Unas semanas después ocurrió un fallo de nodo durante un periodo de escritura intensa. Kubernetes reprogrameó el pod, MariaDB arrancó, la recuperación por crash completó rápido—porque había menos estado durable que reconciliar.
Luego vino la parte silenciosa: un conjunto de transacciones recientes faltaban. No estaban corruptas. Faltaban. La aplicación se comportó de forma inconsistente porque ciertos eventos de negocio nunca ocurrieron. Los logs no gritaron. Susurraron. La empresa pasó días reconciliando datos usando streams upstream y tickets de soporte al cliente. Fue el peor tipo de fallo: la base de datos estaba “sana”, pero el negocio no.
La política final: si relajas durabilidad, debes documentar exactamente la ventana de pérdida de datos aceptada y construir controles compensatorios (writes idempotentes, event sourcing, jobs de reconciliación). De lo contrario, mantén 1/1 y paga por almacenamiento real. La optimización “funcionó” hasta que se convirtió en un problema de auditoría financiera.
Micro-historia 3: La práctica aburrida pero correcta que salvó el día
Una fintech ejecutaba MySQL en Kubernetes con una disciplina estricta: cada StatefulSet tenía un startupProbe afinado al peor caso de recuperación, termination grace suficiente para apagado limpio y un preStop hook que marcaba el pod NotReady antes de SIGTERM. También realizaban pruebas periódicas de restauración desde backups en un namespace scratch. A nadie le encantaba hacerlo. No era glamuroso.
Una tarde, una actualización de clúster rutinaria drenó nodos más rápido de lo esperado debido a un presupuesto de disrupción mal configurado en otro lugar. Varios pods MySQL fueron terminados mientras estaban bajo carga. Algunos tuvieron que hacer recuperación por crash al reiniciar. No fue bonito, pero fue controlado: los pods pasaron a NotReady antes de la terminación, el tráfico se movió y el periodo de gracia permitió que la mayoría de instancias se apagaran limpiamente.
La verdadera salvación: una réplica arrancó con un directorio de datos dañado debido a un fallo no relacionado del backend de almacenamiento. En lugar de improvisar, el on-call siguió el runbook que habían practicado: cordon al nodo afectado, detach del volumen, aprovisionar un PVC nuevo y restaurar desde el último backup conocido bueno. El servicio degradó pero se mantuvo arriba. Nadie tuvo que “eliminar el pod y ver”.
Su ventaja no fue tener mejores ingenieros. Fue tener menos sorpresas. Habían ensayado los movimientos aburridos hasta que fueron automáticos.
Errores comunes: síntomas → causa raíz → solución
1) CrashLoopBackOff durante la recuperación por crash
Síntomas: El pod se reinicia cada 30–90 segundos; los logs muestran que la recuperación InnoDB inicia repetidamente.
Causa raíz: El liveness probe falla durante una recuperación legítima; no hay startupProbe; timeout demasiado corto; picos de latencia de almacenamiento.
Solución: Añade startupProbe con suficiente failureThreshold×periodSeconds para cubrir el peor caso. Aumenta timeout de liveness. Mantén liveness barato (ping de admin), no consultas SQL pesadas.
2) El pod está Running pero nunca Ready
Síntomas: STATUS Running, pero READY 0/1; la app no ve endpoints.
Causa raíz: Readiness comprueba lag de replicación con un umbral irrealmente estricto; o el probe se conecta vía DNS/servicio que no está listo; o credenciales/utilidad equivocada en la imagen.
Solución: Usa socket localhost; asegúrate de que el binario del probe exista; afloja criterios de readiness a lo que la app puede tolerar; separa “servir lecturas” vs “servir escrituras” si hace falta.
3) “server has gone away” aleatorio durante drains de nodo
Síntomas: Picos cortos de errores de cliente durante despliegue/upgrade; logs muestran desconexiones abruptas.
Causa raíz: No hay preStop para quitar el pod de los endpoints antes de la terminación; termination grace muy corto; no se implementó draining de conexiones.
Solución: Añade preStop para cambiar readiness (o dormir después de marcar NotReady). Aumenta termination grace. Considera una capa proxy para draining de conexiones.
4) Réplicas se quedan atrás permanentemente tras un reinicio
Síntomas: Seconds_Behind_Master crece y nunca se recupera; hilos IO/SQL corren pero lentos.
Causa raíz: Throughput de almacenamiento insuficiente para la tasa de apply; límites de CPU causando throttling; apply single-threaded; o transacciones largas en el primario.
Solución: Arregla StorageClass/IOPS primero; aumenta límites de CPU; ajusta paralelismo de apply donde aplique; reduce transacciones largas.
5) Corrupción del directorio de datos después de “redeploy simple”
Síntomas: mysqld falla al iniciar; errores sobre tablespaces faltantes o mismatch de redo log.
Causa raíz: Dos pods montaron el mismo volumen RWO debido a intervención manual o controlador roto; o la imagen cambió permisos del datadir; o init containers re-inicializaron accidentalmente.
Solución: Haz cumplir identidad de StatefulSet y claims de volumen; evita hacks manuales de “adjuntar en otro lado”; asegura init logic para que nunca borre un datadir existente.
6) “Está lento” pero solo en Kubernetes
Síntomas: La misma configuración es rápida en VMs, lenta en k8s; los commits son espigados.
Causa raíz: Variación de latencia en almacenamiento en red; throttling por límites de CPU bajos; efecto de vecino ruidoso; buffer pool mal dimensionado para el límite de memoria.
Solución: Haz benchmark del StorageClass; reserva CPU/memoria apropiadamente; asegura que el buffer pool quepa dentro del límite de memoria; mantiene las opciones de durabilidad estables salvo que aceptes pérdida.
Listas de verificación / plan paso a paso
Checklist A: Diseño de probes para MySQL/MariaDB (haz esto antes de producción)
- Usa startupProbe para proteger la recuperación por crash (presupuesta para el peor caso, no el promedio).
- Liveness: ping local simple sin SQL con timeouts conservadores.
- Readiness: conexión local +
SELECT 1; opcionalmente comprobar lag de réplica o wsrep readiness. - Evita secretos en argv cuando sea posible; prefiere vars de entorno o archivo de configuración legible solo por el usuario mysql.
- Configura timeouts de probes para tu almacenamiento, no para tu optimismo.
Checklist B: Seguridad en reinicios y corrección de apagado
- terminationGracePeriodSeconds dimensionado para flush/apagado bajo carga.
- preStop hook que deje de enrutar tráfico antes de SIGTERM (readiness gate o proxy drain).
- PDBs que impidan que múltiples pods críticos sean interrumpidos a la vez.
- Anti-affinity de pods para no poner primario y réplica en el mismo dominio de fallo.
Checklist C: Postura de seguridad de datos (decide explícitamente)
- Ajustes de durabilidad: elige
innodb_flush_log_at_trx_commitysync_binlogsegún la ventana de pérdida aceptable. - Storage class: elige una clase con latencia de fsync predecible; pruébala con el mismo patrón de acceso.
- Backups: automatizados, cifrados, con drills de restauración en un namespace scratch.
- Migraciones de esquema: escalonadas y reversibles cuando sea posible; no ejecutes migraciones largas y bloqueantes en pico.
Preguntas frecuentes
1) ¿Debo usar MySQL o MariaDB en Kubernetes?
Si necesitas máxima compatibilidad con el ecosistema de herramientas de MySQL (y funciones de MySQL 8), elige MySQL. Si estás comprometido con características de MariaDB como ciertos patrones basados en Galera, elige MariaDB. Operativamente, ambos pueden ejecutarse de forma segura—pero solo si diseñas correctamente probes y almacenamiento.
2) ¿Necesito un operador?
Si ejecutas escrituras en producción, sí—o un operador maduro o un chart interno muy disciplinado con runbooks. La fiabilidad de stateful viene de la automatización de los bordes feos: bootstrap, failover, backups y upgrades seguros.
3) ¿Puede la liveness probe comprobar el lag de replicación?
No. Eso es una preocupación de readiness. Que liveness mate una réplica atrasada convierte “temporalmente detrás” en “muerta permanentemente”.
4) ¿Por qué a veces mi reinicio de base de datos tarda tanto?
Un cierre no limpio dispara recuperación por crash. El tiempo de recuperación depende del redo por aplicar y del rendimiento/latencia de IO de tu volumen. En almacenamiento lento o variable puede oscilar mucho.
5) ¿Es seguro ejecutar MySQL/MariaDB en almacenamiento en red?
Puede ser seguro si el almacenamiento ofrece latencia predecible y semánticas correctas, pero el rendimiento y la latencia cola a menudo sufren. Para OLTP intenso, SSD local o block storage de alta calidad con buenas características de fsync suele ser la opción más segura.
6) ¿Debo poner innodb_flush_log_at_trx_commit=2 por rendimiento?
Solo si el negocio acepta perder hasta ~1 segundo de transacciones en un crash (y entiendes también la durabilidad del binlog). Si no puedes tolerarlo, no “optimices” eso. Arregla almacenamiento y aislamiento de recursos en su lugar.
7) ¿Cómo debe comportarse readiness para nodos Galera (MariaDB)?
Bloquea readiness según el estado wsrep: wsrep_cluster_status Primary, wsrep_local_state_comment Synced y wsrep_ready=ON. Si no, enrutarás tráfico a nodos que están joining o donating y obtendrás comportamiento cliente extraño.
8) ¿Cuál es la causa #1 de pérdida de datos en setups de bases de datos en Kubernetes?
Decisiones humanas disfrazadas de defaults: durabilidad relajada sin aceptación explícita, borrar StatefulSets con política de reclaim “Delete”, o disciplina de backups/restore rota. Kubernetes rara vez borra tus datos por accidente; las personas lo hacen.
9) ¿Los probes generan carga en la base de datos?
Sí. Si sondas con demasiada frecuencia, ejecutas consultas pesadas o usas muchas conexiones concurrentes de healthcheck, puedes causar la latencia que falla el probe. Mantén las comprobaciones baratas y con límite de tasa.
10) ¿Cuál es el enfoque de probe más simple y seguro?
StartupProbe: generoso con mysqladmin ping. Liveness: el mismo ping con timeout y periodo conservadores. Readiness: conexión local + SELECT 1, más gate opcional de replicación/wsrep.
Próximos pasos que puedes hacer esta semana
Si actualmente ejecutas MySQL o MariaDB en Kubernetes, aquí tienes una secuencia práctica que mejora la seguridad sin requerir una migración o un nuevo operador mañana:
- Añade startupProbe a cada pod de base de datos, afinada al peor caso de recuperación en tu StorageClass.
- Audita liveness probes y elimina cualquier comprobación que chequee salud lógica (lag de replicación, wsrep, SQL largo).
- Aumenta termination grace y añade un preStop hook para dejar de enrutar tráfico antes de la terminación.
- Mide la latencia cola del almacenamiento durante el pico y correlaciónala con fallos de probes y latencia de commits; si es inestable, arregla almacenamiento antes de tocar knobs de MySQL.
- Haz un drill de restauración en un namespace scratch. Si es doloroso, no es un backup; es un deseo.
- Escribe tu postura de durabilidad (1/1 vs relajada), con la aceptación explícita del owner del negocio si eliges riesgo de pérdida de datos.
La meta no es “nunca reiniciar”. La meta es “los reinicios son soportables, previsibles y aburridos”. Kubernetes seguirá reintentando. Asegúrate de que esté reintentando algo seguro.