Operación
mcpd es un proceso que guarda un archivo. Eso hace que operarlo sea corto casi siempre, y hace que valga la pena conocer con exactitud las pocas cosas que importan.
Qué hay en el puerto
Una sola dirección lo sirve todo: la ingesta OTLP en /v1/traces,
/v1/logs y /v1/metrics, la ingesta nativa en
/v1/ingest, la API de consulta bajo /v1/api, la
interfaz de operación en /, y los tres endpoints operativos de
abajo. No hay sidecar ni proxy inverso que configurar, que es la mayor parte
de por qué el despliegue cabe en una línea.
La interfaz está compilada dentro del binario, así que no hay servidor de archivos estáticos ni un segundo volumen montado. La ingesta lleva el límite de cuerpo grande y ningún CORS. La API de consulta lleva CORS, para que una interfaz servida desde otro origen pueda leerla.
/healthz y /readyz no son la misma pregunta
Están separados porque responden ante consecuencias distintas, y conectar los dos al mismo manejador es una forma habitual de construir un bucle de reinicios.
| Endpoint | Toca el almacenamiento | Responde |
|---|---|---|
GET /healthz | No | Siempre 200 mientras el proceso esté sirviendo |
GET /readyz | Sí, una lectura de cada almacén | 200 cuando responden el almacén de señales y el de ejecuciones, 503 cuando uno no responde |
/healthz es la sonda de vida. A propósito no toca nunca el
almacenamiento: una base de datos lenta o bloqueada no debe hacer que maten
y reinicien el proceso, porque reiniciarlo no arregla un disco y el bucle de
reinicios además te cuesta los lotes en vuelo.
/readyz es la sonda de preparación. Ejecuta una consulta trivial
contra una conexión de lectura, para que a una instancia que no puede servir
se le deje de enviar tráfico. Cuando el almacenamiento es inalcanzable
responde 503 con un status de
degraded y un detail que nombra el fallo, en lugar
de un código de estado a secas que luego tienes que investigar a ciegas.
Los dos cuerpos llevan los mismos campos: status,
version, spec_version,
uptime_seconds, storage, y
detail cuando algo va mal. spec_version es la
versión del modelo de datos que implementa esta compilación, que es como un
cliente puede negarse a hablar con un servidor que no entiende.
$ curl -s localhost:4317/healthz{"status":"ok","version":"0.1.0","spec_version":"1.0.0", "uptime_seconds":86400,"storage":"sqlite"}$ curl -s -o /dev/null -w "%{http_code}\n" localhost:4317/readyz200$ curl -s localhost:4317/metrics | head -4# HELP mcpd_signals_accepted_total Signals written to storage.# TYPE mcpd_signals_accepted_total countermcpd_signals_accepted_total 184203# HELP mcpd_signals_rejected_total Signals refused, with a reason returned to the sender.
Ninguna de las dos sondas necesita una clave, y /metrics
tampoco. Si eso no es aceptable en tu red, restríngelos en tu ingress.
kubernetes
livenessProbe:
httpGet: { path: /healthz, port: 4317 }
periodSeconds: 10
readinessProbe:
httpGet: { path: /readyz, port: 4317 }
periodSeconds: 5
# SIGTERM is handled, so give in-flight ingest time to finish.
terminationGracePeriodSeconds: 30 Métricas
mcpd exporta sus propios contadores en /metrics, en el formato
de texto de Prometheus. No están detrás de ningún interruptor: una
herramienta de observabilidad que no se puede observar es un riesgo.
| Métrica | Tipo | Significado |
|---|---|---|
mcpd_signals_accepted_total | counter | Señales escritas en el almacenamiento |
mcpd_signals_rejected_total | counter | Señales rechazadas, con un motivo devuelto al emisor |
mcpd_batches_duplicate_total | counter | Lotes reconocidos como una repetición y omitidos |
mcpd_traces_sampled_out_total | counter | Trazas que descartó la política de muestreo |
mcpd_signals_over_quota_total | counter | Señales rechazadas por superar el tope por minuto de un proyecto |
mcpd_signals_unbounded_total | counter | Señales que llegaron al paso del tope para un proyecto que ningún tope cubre |
mcpd_ingest_requests_total | counter | Solicitudes de ingesta atendidas |
mcpd_query_requests_total | counter | Solicitudes de consulta atendidas |
mcpd_request_errors_total | counter | Solicitudes que fallaron con un error del servidor |
mcpd_audit_write_failures_total | counter | Filas de auditoría que no se pudieron escribir; la solicitud que describen sí tuvo éxito |
mcpd_uptime_seconds | gauge | Segundos desde que arrancó este proceso |
Cada contador dura lo que el proceso y vuelve a cero al reiniciar, así que
alerta sobre una tasa y no sobre un nivel. Los dos que vale la pena vigilar
son mcpd_signals_rejected_total, que significa que a los
emisores se les está diciendo que no y probablemente no están leyendo la
respuesta, y mcpd_request_errors_total, que por definición es
culpa del propio mcpd: un 4xx es de quien llama y ahí no se cuenta nunca.
Que mcpd_traces_sampled_out_total suba no es un fallo. Es el
número que explica un hueco en la interfaz cuando alguien pregunta por qué
falta su ejecución.
mcpd_signals_over_quota_total es un número de la misma forma
para la otra razón por la que una señal no aterriza: un proyecto llegó a su
tope por minuto. Esos rechazos se cuentan también en
mcpd_signals_rejected_total, así que vigila este para
distinguir una flota a la que se está limitando de una flota que envía datos
mal formados. Consulta
Configuración
para ver qué cubre el tope.
mcpd_signals_unbounded_total es el número contrario: señales
que se conservaron porque ningún tope cubría el proyecto que nombraron. Los
topes escritos solo bajo projects acotan los proyectos que
nombran, así que un emisor que nombra cualquier otra cosa pasa de largo.
Esas señales se cuentan en lugar de rechazarse, porque rechazar un proyecto
que nadie anotó tiraría los datos de un despliegue que se actualizó sin
cambiar su configuración. Que no sea cero significa que hay que fijar un
--quota-per-minute de nivel superior junto a los de cada
proyecto.
El tope cubre todas las puertas por las que puede entrar una fila: los
cuatro endpoints de ingesta, los endpoints de registro único de puntuación y
de feedback, y POST /v1/api/score, cuyas puntuaciones se
calculan y después se cargan contra el mismo contador antes de almacenarlas.
Lo que allí no cabe bajo el tope se informa puntuación a puntuación en la
lista skipped de la respuesta, en lugar de escribirse por
encima de él.
mcpd_audit_write_failures_total cuenta las filas de auditoría
que no se pudieron escribir. La solicitud que describía cada fila sí tuvo
éxito y sí devolvió sus datos, que es justo la idea: una escritura de
auditoría nunca hace fallar la lectura que describe. Que no sea cero
significa por tanto que hubo lecturas de payloads almacenados de las que el
registro de auditoría no puede dar cuenta, así que trátalo como una señal de
integridad y no de disponibilidad.
prometheus
scrape_configs:
- job_name: mcpd
static_configs:
- targets: ['mcpd:4317'] Parada
mcpd atiende SIGTERM y SIGINT. Con cualquiera de
los dos deja de aceptar conexiones nuevas y permite que terminen las
solicitudes en vuelo antes de que el proceso salga.
Atender SIGTERM en concreto es lo que hace que esto funcione
bajo un orquestador. Kubernetes envía SIGTERM y espera a que
pase el periodo de gracia antes de SIGKILL; a un contenedor que
solo escucha SIGINT lo matan en lugar de pararlo, y se pierde
el lote que estuviera a medio transaccionar.
mcpd 0.1.0 listening on http://0.0.0.0:4317 storage sqlite at /data/mcpd.dbretention removed 12480 rows older than 30 daysreceived termination signalshutting down
Lo que mcpd no puede salvar al salir es lo que siga en la cola de un emisor.
Los SDK guardan los spans en memoria y los exportan en un worker, así que un
proceso que sale sin llamar a shutdown() pierde lo que aún no
hubiera salido. Esa es la parada del agente, no la del daemon.
Copias de seguridad
Todo el almacén es un archivo SQLite en --data. Funciona en
modo WAL, así que en disco encontrarás mcpd.db junto a
mcpd.db-wal y mcpd.db-shm. Copiar el
.db por su cuenta mientras el proceso está en marcha te da un
archivo al que le falta todo lo que sigue en el registro de escritura
anticipada.
Haz la copia a través de SQLite, que lee una instantánea coherente mientras mcpd sigue escribiendo:
copia en caliente
sqlite3 /data/mcpd.db ".backup '/backup/mcpd-$(date +%F).db'"
Parar antes el proceso y copiar los tres archivos vale igual y no necesita
un binario sqlite3 en la imagen. En esta versión no hay
subcomando mcpd backup ni copias programadas: ese es el trabajo
de tus herramientas de instantáneas, y fingir lo contrario pondría una
herramienta a medio hacer en el camino crítico.
La retención borra filas pero no encoge el archivo. SQLite conserva las
páginas liberadas y las reutiliza, así que el uso de disco se estanca en
lugar de bajar después de un borrado grande. Ejecuta VACUUM
contra una instancia parada si de verdad necesitas recuperar el espacio.
Restauración
Devolver una copia a su sitio son cuatro pasos, y el orden importa por el registro de escritura anticipada.
- Para el daemon. Una restauración con el proceso en marcha es un segundo escritor sobre el mismo archivo.
- Copia la copia de seguridad encima del archivo en
--data. -
Borra los archivos
-waly-shmque están a su lado. Pertenecen a la base de datos que has sustituido, y si los dejas se reproducirían dentro de la que has restaurado. -
Arranca el daemon, y confirma que está leyendo el archivo restaurado:
/readyzresponde 200, y una lista de trazas del proyecto devuelve las ejecuciones que tenía la copia.
$ kill $(cat mcpd.pid) && cp /backup/mcpd-2026-09-01.db /data/mcpd.db$ rm -f /data/mcpd.db-wal /data/mcpd.db-shm$ mcpd --data /data/mcpd.db &$ curl -s -o /dev/null -w "%{http_code}\n" localhost:4317/readyz200$ curl -s "localhost:4317/v1/api/traces?project=default&limit=500" | jq ".traces | length"2 Actualizaciones
Sustituye el binario o la etiqueta de la imagen y vuelve a arrancarlo. El
esquema se aplica en el arranque, todas las veces, y está escrito de forma
que ejecutarlo contra un archivo ya al día no cambia nada. La versión
aplicada se anota en una tabla schema_version dentro del mismo
archivo, y esta compilación está en la versión 2.
Dos cosas que tener presentes. Avanza, no retrocedas: a un binario más
antiguo no se le promete que lea un archivo más nuevo, así que haz antes la
copia de seguridad. Y comprueba spec_version en
/healthz después de actualizar si fijas las versiones de los
SDK, porque ese es el número que compara un cliente.
Dónde se detiene una instancia
Ser honesto sobre esto es más útil que un número que no hemos medido. No publicamos cifras de rendimiento, porque no existe ninguna que podamos sostener.
- Un escritor. El almacén SQLite mantiene una sola conexión de escritura detrás de un mutex y cuatro conexiones de lectura. El modo WAL significa que una consulta de un panel nunca bloquea la ingesta, que es el fallo que sí importaría, pero todas las escrituras se serializan por esa única conexión.
- Una instancia por archivo. Dos procesos de mcpd apuntados al mismo archivo no es un despliegue soportado. Ejecútalo como una sola réplica con un volumen, no como un servicio escalado en horizontal.
- Los dos límites de arriba son del almacén incorporado. Dejan de aplicarse con ClickHouse, que va compilado dentro y se trata más abajo.
- Los payloads son grandes. Los prompts y las completions son kilobytes, no bytes, y son lo que hace que valga la pena ejecutar mcpd. El almacenamiento crece con el volumen de texto mucho más deprisa que con el número de spans, así que mide con tus propios payloads y no con una cifra de spans por segundo.
- La retención cubre los proyectos que anotaste. La pasada
horaria borra de cada proyecto del bloque
projects, cada uno con su propioretention_daysdonde lo fija y con la cifra global donde no, más el proyecto nombrado por--project. Un proyecto que un emisor se inventó y que no aparece en ninguno de los dos sitios no tiene ventana y no se recorta nunca, lo que conviene saber antes de apoyarse en la retención como control de privacidad.
Cuando el volumen le queda grande a la instancia, las palancas, en orden de
lo que te cuestan, son: --retention-days, que acota el archivo;
--sample, que acota el flujo de entrada y es determinista, así
que una traza conservada se conserva entera; y pasar a ClickHouse, que quita
a la vez el escritor único y la réplica única. Consulta
Muestreo y
Ocultación y retención.
Postgres, cuando ya tienes uno
Apunta mcpd a una cadena de conexión de Postgres y almacenará ahí en lugar de en el archivo incorporado. El esquema se crea al arrancar.
almacenar en Postgres
mcpd serve --postgres-url postgres://mcpd:$PGPASSWORD@db:5432/mcpd Esto quita los dos mismos límites que quita ClickHouse. Las escrituras ya no pasan por una sola conexión, así que mcpd se ejecuta como varias réplicas detrás de un servicio, y lo hace sobre una base de datos que la mayoría de los equipos ya operan, respaldan y vigilan.
La migración toma un bloqueo consultivo de Postgres y se omite por completo cuando el esquema ya está al día. Que varias réplicas arranquen a la vez es por tanto seguro, y un despliegue progresivo no toma un bloqueo exclusivo sobre tablas en las que otras réplicas están escribiendo.
La búsqueda de texto completo usa una columna tsvector generada
con un índice GIN, en la configuración simple: pasa a
minúsculas y parte por los límites de palabra, pero no reduce a la raíz, así
que buscar en un prompt el término literal que escribiste hace lo que
esperas.
Elige ClickHouse antes que Postgres cuando la retención se mida en miles de millones de spans. Por debajo de eso, la base de datos que ya tienes es la mejor respuesta.
ClickHouse, cuando el archivo ya no tiene la forma correcta
Apunta mcpd a un endpoint HTTP de ClickHouse y almacenará ahí en lugar de en el archivo incorporado. El esquema se crea al arrancar, así que no hay nada que aplicar a mano.
almacenar en ClickHouse
mcpd serve \
--clickhouse-url http://clickhouse:8123 \
--clickhouse-database mcpd \
--clickhouse-user default --clickhouse-password $CH_PASSWORD
Las dos restricciones de arriba se levantan a la vez. Las escrituras ya no
pasan por una sola conexión, así que mcpd se ejecuta como varias réplicas
detrás de un servicio, y los spans caen en un MergeTree
construido para la forma que estos datos tienen de verdad: mucha más
escritura que lectura, y consultas que recorren un rango de tiempo.
Elígelo cuando la retención se mida en meses de tráfico de producción y no en el disco de un portátil. En cualquier cosa menor, el archivo incorporado es más rápido de ejecutar, más rápido de respaldar y una cosa menos que operar. Quédate con el archivo hasta que duela.
Las trazas son un ReplacingMergeTree, porque la proyección de
una traza se recalcula a medida que llegan sus spans y la última escritura
es la correcta. Los spans que llegan fuera de orden, reintentados o
actualizados en su sitio convergen así en la respuesta correcta en lugar de
acumular duplicados.
Aquí hay una garantía más débil que en los otros dos almacenes, y es la que
quizá tengas que certificar. El borrado en ClickHouse es una mutación
asíncrona, así que una solicitud de borrado se acepta y se aplica después.
SQLite y Postgres borran dentro de la transacción que respondió. La
respuesta dice cuál de las dos cosas ocurrió, en
deletes_pending. Consulta
Ocultación y retención.
Comprobar un despliegue
mcpd doctor lee la configuración y el almacén e informa de lo
que está mal, nombrando el flag que arregla cada cosa. Existe porque los
fallos que de verdad muerden son los silenciosos: no se cae nada, y te
enteras semanas después.
comprobar un despliegue sin contactar con nada más que el almacén
mcpd doctor --data /data/mcpd.db --offline
Tres flags son propios del comando. --offline no contacta con
nada más que con el almacén, así que el endpoint de reenvío se informa como
omitido en lugar de comprobado. --read-only no escribe nada,
así que la comprobación de almacenamiento prueba que el almacén se puede
leer y no que se pueda escribir, y lo dice. --window-days es
cuántos días de historia leen las comprobaciones de actividad, de reloj y de
libro de precios, siete salvo que digas otra cosa, entre 1 y 36525. Todo
lo demás que toma es la configuración del propio daemon, así que califica la
invocación que arrancarías.
Se califican doce grupos, en este orden, y el informe termina con una cuenta de cada nivel:
| Grupo | Qué califica |
|---|---|
storage | Que el almacén responde a un ping, y que un span de prueba sobrevive a una escritura, una lectura y un borrado |
runs | Que el almacén de ejecuciones responde, cuántas versiones de definiciones y ejecuciones en marcha tiene, y si la superficie de ejecuciones necesita una clave en una dirección que otras personas alcanzan. Solo lectura: el doctor nunca reclama un paso ni dispara un plazo |
keys | Cada ámbito, con su herencia explícita, para que el caso en el que no hay clave de administración y por tanto toda clave de lectura también borra esté en la página y no en la documentación |
identity | La cabecera del sujeto de auditoría, y si el rango que puede fijarla es lo bastante estrecho como para creérselo |
projects | Los proyectos configurados frente a los proyectos que tienen ejecuciones. Uno que no ha recibido nunca un span es casi siempre una errata en un nombre |
retention | Hasta cuándo llegan hacia atrás los datos de cada proyecto, y si algo va a borrarlos alguna vez |
limits | Los topes de ingesta de cada proyecto y las fracciones de muestreo |
forwarding | El endpoint de reenvío, enviando un POST con un lote OTLP sin ningún span dentro |
judge | El bloque del juez, solo como configuración |
monitors | Los monitores y los objetivos, y las señales que leen |
clock | Las ejecuciones de los dos extremos de la historia de cada proyecto frente al reloj de esta máquina, porque un emisor cuyo reloj va atrasado se ordena por debajo de cada ejecución honesta y no aparecería nunca en una página de las más nuevas |
pricing | Los spans de modelo recientes frente al libro de precios, para nombrar un modelo al que nada puede poner precio |
identity y limits son los dos añadidos más
recientes, y los dos estaban sin calificar por la misma razón: no se cae
nada por su culpa. Un registro de auditoría sin sujeto anota qué se hizo y
nunca quién lo hizo. Una fracción de muestreo fuera de 0.0 a 1.0 se rechaza
en --sample y no se comprobaba nunca en el bloque de un
proyecto, así que un proyecto podía llevarla y el daemon la ejecutaba. Un
tope fijado por proyecto sin un tope de nivel superior a su lado deja sin
ningún tope a todos los proyectos que nadie anotó, y el nombre del proyecto
lo elige el emisor.
Omitido no es correcto
Una comprobación que no se ejecutó dice skipped, nunca ok. Esa distinción es la razón de ser del comando: un subsistema que falta en la configuración a la que lo apuntaste no es un subsistema con buena salud, e informar de los dos casos igual haría que el doctor estuviera más seguro justo cuando le han pasado el archivo equivocado. La línea de resumen cuenta aparte las comprobaciones omitidas y lo dice con palabras, para que una ejecución que verificó dos de sus catorce comprobaciones no se pueda leer como un certificado de buena salud.
$ mcpd doctor --data /data/mcpd.db | tail -42 ok, 9 skipped, 2 warnings, 1 faultA skipped check proved nothing: it was not run, or there was nothing thereto run it against. Only the 2 ok were verified.A fault is something broken or open. Exiting 2. Un almacén ocupado no es un almacén roto
La prueba de almacenamiento es la única comprobación que necesita el bloqueo de escritura. Todo lo que hay debajo lee, y los lectores no hacen cola detrás de un escritor. Una escritura de prueba que pierde la carrera por ese bloqueo se informa como skipped: se intenta tres veces, y si las tres esperan al bloqueo y se rinden, el informe dice que la escritura no se comprobó y por qué.
Esto importa porque el almacén incorporado admite un escritor, y un
despliegue bajo ingesta real tiene ese bloqueo tomado casi sin parar.
Informar de la contención como un fallo hacía que el comando tumbara un
despliegue justo cuando el despliegue estaba sano y ocupado. Los fallos para
los que existe la prueba se nombran cada uno a sí mismo y ninguno dice
«bloqueado»: un montaje de solo lectura, un disco lleno y un rol sin
INSERT siguen siendo fallos, y también lo es un bloqueo
corrupto, que dice «locking protocol» y es daño de verdad.
El código de salida
Sale distinto de cero ante un fallo, así que puede servir de puerta para un despliegue. Un aviso no es un fallo: sale con cero y espera que una persona lo lea. Una configuración es un fallo y no un aviso, y conviene saberlo antes de conectar esto a un pipeline: un ámbito de lectura abierto en una dirección que no es de loopback, que entrega cada prompt almacenado a cualquiera que alcance el puerto. Un ámbito de ingesta abierto en la misma dirección sigue siendo un aviso, porque las filas de basura son lo que quita la retención y una divulgación no se puede retirar. Un rango de identidad de confianza que contiene internet entero se califica igual, por la misma razón: cualquier dirección podría entonces fijar la cabecera y quedar registrada como quien diga que es.
Los ajustes que comprobar antes de que sea alcanzable
Cuando enlaza una dirección que no es de loopback, mcpd registra al arrancar un aviso por cada superficie abierta a quien no debería alcanzarla, y cada aviso nombra las opciones que hay que fijar. Avisa en lugar de negarse a arrancar, porque negarse rompería el camino desde nada hasta una primera traza. Sigue siendo un aviso sobre un agujero real: una API de lectura abierta entrega cada prompt almacenado a cualquiera que alcance el puerto.
mcpd --data /data/mcpd.db --ingest-key $INGEST_KEY --read-key $READ_KEY --execute-key $EXECUTE_KEY --worker-key $WORKER_KEY --admin-key $ADMIN_KEY --retention-days 30 Sin las tres últimas claves, la clave de ingesta y la clave de lectura también llegan a la superficie de ejecuciones, y la clave de lectura también borra. El detalle completo de cada clave, y de todo lo demás que puedes fijar, está en Configuración.