Este es el artículo 5/6 de la serie sobre la ruta de escritura en clústeres fragmentados de MongoDB.
Preguntas que responde este artículo: si el primary no termina ni una sola escritura, ¿por qué no ocurre el failover automático? ¿Por qué Flow Control y Storage Node Watchdog no detectan este estado? Entonces, ¿qué hay que vigilar?
Versión de referencia: MongoDB 4.2 (rama
v4.2de la documentación oficial, etiquetar4.2.12del código fuente del servidor). Lo que cambió en 7.0 se indica aparte en la sección correspondiente.
En MongoDB 4.2 hay tres mecanismos que reaccionan automáticamente a anomalías del primary. Los tres miran si el primary está vivo o si los secondary se han quedado atrás. Un primary que responde pero no termina las escrituras no cumple ninguno de sus criterios.
| Mecanismo | Qué mira | Ante un primary con las escrituras detenidas |
|---|---|---|
| Failover automático (elección) | Si el heartbeat responde | Como el heartbeat responde, no hay elección |
| Flow Control | El retraso (lag) del majority commit | El lag está cerca de 0 y lo considera normal |
| Storage Node Watchdog | Que el sistema de archivos no responda | El sistema de archivos está bien, así que no se activa |
Por eso el operador tiene que vigilar directamente el progreso (progress). La pregunta que hay que vigilar no es "¿responde?", sino "¿termina las escrituras?".
La causa de este estado está dentro del storage engine WiredTiger (en adelante, el motor). En concreto, es el livelock del bucle de pause del snapshot de WiredTiger. Un livelock es un estado en el que los hilos siguen dando vueltas usando CPU, pero las operaciones no terminan. Esta causa se trata en el artículo 3/6. Este artículo trata de por qué los tres mecanismos no reaccionan a este estado y de cómo diseñar la vigilancia.
Los miembros del replica set se envían heartbeats entre sí. La documentación de 4.2 lo dice así.
Replica set members send heartbeats (pings) to each other every two seconds. If a heartbeat does not return within 10 seconds, the other members mark the delinquent member as inaccessible.
| Elemento | Valor |
|---|---|
| Periodo del heartbeat | 2 segundos |
| Tiempo sin respuesta tras el cual se considera inaccesible | 10 segundos |
| Tiempo de desconexión que provoca una elección | electionTimeoutMillis, 10 segundos por defecto |
La elección empieza cuando un secondary pierde la conexión con el primary durante más de electionTimeoutMillis. La regla de elección tiene una sola entrada: "¿vuelve el heartbeat?". Si el primary procesa escrituras no forma parte de la entrada.
El código que procesa el comando heartbeat no toma ni el lock global ni un ticket. Un ticket es un pase de entrada que limita el número de operaciones que pueden entrar a la vez en el motor. La función que lo procesa es CmdReplSetHeartbeat::run de src/mongo/db/repl/repl_set_commands.cpp (r4.2.12). Esta función, tras parsear los argumentos, solo llama a processHeartbeatV1.
Las escrituras normales son distintas. insert·update·delete·findAndModify toman el lock global en modo IX (intención de escritura) y a la vez reciben un write ticket (src/mongo/db/concurrency/lock_state.cpp:262-266 (r4.2.12)). En 4.2, el valor por defecto de write tickets es 128 (src/mongo/db/storage/wiredtiger/wiredtiger_kv_engine.cpp:474-475 (r4.2.12)).
Aunque las operaciones que recibieron los 128 write tickets estén todas detenidas dentro del motor, el heartbeat no espera ningún ticket. Por eso, aunque el motor esté detenido, el heartbeat puede responder con normalidad. Esta conclusión es una inferencia hecha leyendo el código fuente. No se reprodujo directamente.
Por ejemplo, mientras no termina ni una sola escritura del servicio writer que escribe en la colección store_inventory, los secondary siguen recibiendo respuesta al heartbeat cada 2 segundos. La entrada de la regla de elección no cambia.
Flow Control es una función que llegó en 4.2. El retraso del majority commit es el tiempo que tarda una mayoría de miembros en confirmar una escritura que aplicó el primary. Flow Control ralentiza las escrituras del primary para mantener este retraso por debajo de cierto valor.
With flow control enabled, as the lag grows close to the flowControlTargetLagSeconds, writes on the primary must obtain tickets before taking locks to apply writes.
Los valores por defecto son enableFlowControl: true y flowControlTargetLagSeconds: 10. Funciona si la FCV (featureCompatibilityVersion) es 4.2 y el majority read concern está activado.
La fórmula del lag está en el código fuente (src/mongo/db/storage/flow_control.cpp:77-82 (r4.2.12)).
return static_cast<std::uint64_t>(durationCount<Milliseconds>(myLastApplied - lastCommitted));
myLastApplied es la hora de la última escritura que aplicó el primary. lastCommitted es la hora de la última escritura confirmada por una mayoría de miembros. Si esta diferencia es menor que el umbral (flowControlTargetLagSeconds), Flow Control lo considera normal y aumenta los tickets (mismo archivo, líneas 328-350). También lo considera normal cuando entre las dos horas casi no hay trabajo.
Si el primary no puede escribir, pasa esto.
myLastApplied se detiene.lastCommitted se iguala a myLastApplied.Este flujo es una inferencia deducida de la fórmula. Veámoslo con valores de ejemplo. El primary aplicó su última escritura a las 10:00:00. El secondary la replicó y lastCommitted también pasó a ser 10:00:00. Si después el primary no logra aplicar nada, el lag se queda fijo en 10:00:00 − 10:00:00 = 0초. Está muy por debajo del umbral de 10 segundos.
Aunque el lag crezca y Flow Control ralentice las escrituras, este estado no se deshace. Flow Control reduce la cantidad de escrituras que entran por la puerta del motor. No puede tocar las operaciones que ya recibieron un ticket y entraron en el motor.
Storage Node Watchdog (en adelante, Watchdog) existe también en la edición Community desde 4.2. Vigila que no deje de responder el sistema de archivos de los siguientes directorios.
--dbpathjournal dentro de --dbpath (si el journaling está activado)--logpath--auditPathSi el sistema de archivos no responde, Watchdog termina el mongod (código de salida 61). Si ese mongod era el primary, ocurre un failover.
Watchdog está desactivado por defecto. El valor por defecto de watchdogPeriodSeconds es -1 (desactivado) y, para activarlo, hay que darle un entero de 60 o más al arrancar mongod.
Aunque la CPU esté atada a un bucle dentro del motor, el disco y el sistema de archivos responden con normalidad. Por eso, aunque Watchdog esté activado, no se dispara en esta situación. Esta conclusión es una inferencia deducida de lo que la documentación dice que vigila.
Aparte de esto, en la documentación de 4.2 no se encontró ninguna función que degrade el primary cuando el motor no avanza. No se revisaron exhaustivamente la documentación ni los parámetros.
La estructura de "responde pero no termina el trabajo" también es frecuente en las liveness probe de Kubernetes. Supongamos que la probe solo comprueba /health. Si el hilo HTTP responde, la probe lo da por bueno aunque la cola de trabajo esté detenida. El heartbeat de MongoDB cumple el mismo papel que esa probe de /health.
Para ver el progreso se miran tres señales a la vez.
Si se mira solo una, hay falsos positivos. Si solo se mira el throughput, salta de madrugada, cuando de por sí hay pocas escrituras. Si solo se mira la cola, salta también ante picos momentáneos normales.
Las rutas de abajo son rutas del resultado de db.serverStatus(). Se leen en el primary de cada shard.
| Ruta de serverStatus | Significado | Forma esperada en un primary detenido |
|---|---|---|
wiredTiger.concurrentTransactions.write.available |
Número de write tickets restantes | Se queda cerca de 0 |
opcounters.insert·.update·.delete |
Número acumulado de operaciones procesadas | Deja de crecer o cae en picado |
opLatencies.writes.latency·.ops |
Latencia y número acumulados de escrituras | La latencia media del intervalo se dispara |
globalLock.currentQueue.writers |
Número de escrituras en espera | Sigue creciendo |
En el host se esperan tres formas.
Si iowait está cerca de 0, la CPU no espera al disco: se usa para ejecutar instrucciones. Los cambios de contexto bajan porque el bucle de pause no hace llamadas al sistema y los hilos no ceden la CPU por sí mismos. Estas dos interpretaciones son inferencias deducidas del análisis del código fuente del motor del artículo 3/6.
globalLock.currentQueue.writers cuenta juntas las esperas de lock y las esperas de ticket. En el código fuente r4.2.12, este valor es el número de clientes cuyo estado es kQueuedWriter (src/mongo/db/stats/lock_server_status_section.cpp:76-83). En lock_state.cpp hay tres casos en que se llega a este estado.
No hay que concluir que hay contención de locks porque este valor haya subido. Si se agotan los tickets, sube el mismo valor.
Lo siguiente es pseudocódigo. Los nombres de las métricas se escribieron como rutas de serverStatus. No se verificaron los nombres reales que publica el recolector (exporter), así que hay que adaptarlos al entorno. Todos los umbrales son valores de ejemplo.
# 의사 코드. 각 shard primary마다 평가한다. 임계치는 예시값.
ALERT MongoPrimaryWriteStalled
WHEN
# 1) 엔진 입구가 막혔다
wiredTiger.concurrentTransactions.write.available <= 2
# 2) 들어오려는 쓰기가 있다
AND globalLock.currentQueue.writers > 10
# 3) 처리량이 늘지 않는다 (1분 증가율이 지난주 같은 시각의 10% 미만)
AND rate(opcounters.insert + opcounters.update + opcounters.delete, 1m)
< 0.1 * baseline_same_hour_last_week
# 4) CPU는 명령 실행에 묶여 있다
AND host.cpu.user >= 90%
AND host.cpu.iowait < 5%
FOR 2m
SEVERITY page
NOTE "heartbeat가 정상이어도 자동 failover는 일어나지 않는다. 대응 절차 문서를 연다."
La latencia media de escritura de un intervalo se obtiene con la diferencia de los valores acumulados.
avg_write_latency_us(1m) =
delta(opLatencies.writes.latency, 1m) / delta(opLatencies.writes.ops, 1m)
Si no termina ni una sola escritura, el denominador es 0. En ese caso, se usa como señal el propio hecho de que el número de operaciones terminadas es 0.
Desde 7.0, la condición 1 no se puede usar tal cual. 7.0 ajusta dinámicamente el número de tickets. Por eso, la documentación de 7.0 dice que no considera señal de sobrecarga que el número de tickets restantes (ticket availability) esté en 0 durante mucho tiempo. En 6.0 o anteriores, la misma situación sí es señal de sobrecarga. En 7.0 o posteriores, hay que centrarse en las condiciones 2~4.
Las métricas de serverStatus son señales indirectas. Una probe que envía periódicamente una escritura pequeña y mide si termina es más directa. Abajo hay un ejemplo de mongo shell que no se ha verificado ejecutándolo.
// 예시: 외부 감시 프로세스가 30초마다 실행한다.
// ops_probe 데이터베이스의 probe 컬렉션에 문서 한 건을 갱신한다.
const started = new Date();
db.getSiblingDB("ops_probe").probe.updateOne(
{ _id: "writer-probe" },
{ $set: { at: started } },
{ upsert: true }
);
const elapsedMs = new Date() - started;
// elapsedMs가 예시 임계치(2000ms)를 넘거나 클라이언트 타임아웃으로 실패하면 알린다.
maxTimeMS del servidor es una interrupción cooperativa. La operación solo se detiene si comprueba por sí misma la petición de interrupción, así que no puede cortar una operación detenida dentro del motor (ver el artículo 4/6).A menudo la anomalía se ve antes en la aplicación que en las métricas del primary. En términos generales, el orden esperado es el siguiente.
La estructura por la que los reintentos de la aplicación acumulan más operaciones en espera en el servidor se trata en el artículo 2/6.
- No hay que descartar la DB como causa solo por el heartbeat y la luz verde de
rs.status(). Como el heartbeat no pasa por el motor, el estado del primary también sigue mostrándose normal (inferencia basada en el código fuente).- Cambiar la configuración de Flow Control no deshace este estado. Flow Control solo ralentiza las escrituras en la entrada cuando el lag es grande.
- Activar Watchdog no detecta este estado. Watchdog solo mira si el sistema de archivos deja de responder.
- Una alerta basada solo en la caída del throughput da muchos falsos positivos. Hay que combinarla con el crecimiento de la cola.
- No se revisaron exhaustivamente la documentación y los parámetros para confirmar que de verdad no existe en 4.2 una función que degrade el primary según el progreso del motor.
- La conclusión de que el heartbeat responde mientras el motor está detenido es una inferencia hecha leyendo el código fuente r4.2.12. No se reprodujo directamente.
- No se contrastaron las definiciones de campos de
opcounters·opLatencies.writescon el texto original de la documentación de 4.2.- No se confirmó el código ni el nombre exactos del error que da mongos cuando no logra obtener una conexión al shard.
- No se confirmaron los nombres reales de las métricas que usa el recolector (exporter).
- No se verificaron la forma concreta de tener una probe de escritura por shard en un clúster fragmentado ni sus efectos secundarios.
- No se verificó si en versiones posteriores a 7.0 cambiaron la ruta de procesamiento del heartbeat y la fórmula de Flow Control.