Aller au contenu
mcpd
aucun daemon
Source

Exploitation

mcpd est un seul processus qui tient un seul fichier. Cela rend l’essentiel de son exploitation court, et cela rend les rares points qui comptent dignes d’être connus exactement.

Ce qu’il y a sur le port

Une seule adresse sert tout : l’ingestion OTLP sur /v1/traces, /v1/logs et /v1/metrics, l’ingestion native sur /v1/ingest, l’API de requête sous /v1/api, l’interface d’exploitation sur /, et les trois endpoints d’exploitation ci-dessous. Il n’y a ni side-car ni proxy inverse à configurer, ce qui explique en grande partie que le déploiement tienne sur une ligne.

L’interface est compilée dans le binaire : il n’y a donc pas de serveur de fichiers statiques ni de second volume à monter. L’ingestion porte la grande limite de corps et pas de CORS. L’API de requête porte le CORS, pour qu’une interface servie depuis une autre origine puisse la lire.

/healthz et /readyz ne posent pas la même question

Ils sont séparés parce qu’ils répondent à des conséquences différentes, et câbler les deux sur le même gestionnaire est une façon courante de fabriquer une boucle de redémarrage.

Endpoint Touche au stockage Répond
GET /healthz Non Toujours 200 tant que le processus sert
GET /readyz Oui, une lecture de chaque stockage 200 quand le stockage des signaux et celui des exécutions répondent, 503 quand l’un ne répond pas

/healthz est la vivacité. Il ne touche volontairement jamais au stockage : une base lente ou verrouillée ne doit pas faire tuer et redémarrer le processus, parce que le redémarrer ne répare pas un disque et que la boucle de redémarrage vous coûte en plus les lots en vol.

/readyz est la disponibilité. Il lance une requête triviale sur une connexion de lecture, pour qu’une instance incapable de servir cesse de recevoir du trafic. Quand le stockage est injoignable, il répond 503 avec un status à degraded et un detail nommant la panne, plutôt qu’un simple code d’état qu’il vous faudrait ensuite instruire à l’aveugle.

Les deux corps portent les mêmes champs : status, version, spec_version, uptime_seconds, storage, et detail quand quelque chose ne va pas. spec_version est la version du modèle de données que cette version implémente, et c’est ainsi qu’un client peut refuser de parler à un serveur qu’il ne comprend pas.

sonder une instance en bonne santé
$ 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.

Aucune des deux sondes ne demande de clé, et /metrics non plus. Si cela n’est pas acceptable sur votre réseau, restreignez-les à votre ingress.

kubernetes

livenessProbe:
  httpGet: { path: /healthz, port: 4317 }
  periodSeconds: 10
readinessProbe:
  httpGet: { path: /readyz, port: 4317 }
  periodSeconds: 5

# SIGTERM est géré : laissez à l’ingestion en vol le temps de finir.
terminationGracePeriodSeconds: 30

Métriques

mcpd exporte ses propres compteurs sur /metrics, au format texte d’exposition Prometheus. Ils ne sont pas derrière une option de compilation : un outil d’observabilité que l’on ne peut pas observer est un risque.

Métrique Type Sens
mcpd_signals_accepted_total compteur Signaux écrits dans le stockage
mcpd_signals_rejected_total compteur Signaux refusés, avec un motif rendu à l’émetteur
mcpd_batches_duplicate_total compteur Lots reconnus comme un rejeu et sautés
mcpd_traces_sampled_out_total compteur Traces écartées par la politique d’échantillonnage
mcpd_signals_over_quota_total compteur Signaux refusés pour dépassement du plafond par minute d’un projet
mcpd_signals_unbounded_total compteur Signaux arrivés à l’étape du plafond pour un projet qu’aucun plafond ne couvre
mcpd_ingest_requests_total compteur Requêtes d’ingestion traitées
mcpd_query_requests_total compteur Requêtes de lecture traitées
mcpd_request_errors_total compteur Requêtes ayant échoué sur une erreur serveur
mcpd_audit_write_failures_total compteur Lignes d’audit qui n’ont pas pu être écrites ; la requête qu’elles décrivent a tout de même réussi
mcpd_uptime_seconds jauge Secondes écoulées depuis le démarrage de ce processus

Chaque compteur vaut pour la durée de vie du processus et retombe à zéro au redémarrage : alertez donc sur un taux plutôt que sur un niveau. Les deux qui méritent d’être suivis sont mcpd_signals_rejected_total, qui veut dire qu’on répond non à des émetteurs et qu’ils ne lisent probablement pas la réponse, et mcpd_request_errors_total, qui est par définition la faute de mcpd : un 4xx est celle de l’appelant et n’y est jamais compté.

Un mcpd_traces_sampled_out_total qui monte n’est pas une panne. C’est le nombre qui explique un trou dans l’interface quand quelqu’un demande pourquoi son exécution manque.

mcpd_signals_over_quota_total est un nombre de la même forme pour l’autre raison qu’un signal n’atterrisse pas : un projet a atteint son plafond par minute. Ces refus sont aussi comptés dans mcpd_signals_rejected_total : suivez donc celui-ci pour distinguer une flotte que l’on limite d’une flotte qui envoie des données mal formées. Voir Configuration pour ce que couvre le plafond.

mcpd_signals_unbounded_total est le nombre inverse : les signaux gardés parce qu’aucun plafond ne couvrait le projet qu’ils nommaient. Des plafonds écrits seulement sous projects bornent les projets qu’ils nomment, donc un émetteur qui en nomme un autre passe à côté. Ces signaux sont comptés plutôt que refusés, parce que refuser un projet que personne n’a écrit ferait tomber les données d’un déploiement mis à niveau sans changer sa configuration. Une valeur non nulle veut dire qu’il faut poser un --quota-per-minute de premier niveau à côté de ceux par projet.

Le plafond couvre toutes les portes par lesquelles une ligne peut entrer : les quatre endpoints d’ingestion, les endpoints à un seul enregistrement pour le score et le retour, et POST /v1/api/score, dont les scores sont calculés puis facturés sur le même compteur avant d’être stockés. Ce qui ne rentre pas sous le plafond y est signalé score par score dans la liste skipped de la réponse plutôt qu’écrit au-delà.

mcpd_audit_write_failures_total compte les lignes d’audit qui n’ont pas pu être écrites. La requête que chaque ligne décrivait a tout de même réussi et a tout de même rendu ses données, et c’est le principe : une écriture d’audit ne fait jamais échouer la lecture qu’elle décrit. Une valeur non nulle veut donc dire que des lectures de payloads stockés ont eu lieu dont le journal d’audit ne peut pas rendre compte : traitez-la comme un signal d’intégrité plutôt que de disponibilité.

prometheus

scrape_configs:
  - job_name: mcpd
    static_configs:
      - targets: ['mcpd:4317']

Arrêt

mcpd traite SIGTERM et SIGINT. Sur l’un comme sur l’autre, il cesse d’accepter de nouvelles connexions et laisse les requêtes en vol se terminer avant que le processus ne sorte.

Traiter SIGTERM en particulier est ce qui fait marcher cela sous un orchestrateur. Kubernetes envoie SIGTERM et attend la fin du délai de grâce avant SIGKILL ; un conteneur qui n’écoute que SIGINT est tué plutôt qu’arrêté, et le lot qui était au milieu d’une transaction est perdu.

un arrêt propre
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

Ce que mcpd ne peut pas sauver en sortant, c’est tout ce qui attend encore dans la file d’un émetteur. Les SDK gardent les spans en mémoire et les exportent sur un worker : un processus qui sort sans appeler shutdown() perd donc tout ce qui n’était pas encore parti. Cet arrêt-là est à réussir du côté de l’agent, pas du daemon.

Sauvegardes

Tout le stockage tient dans un fichier SQLite à --data. Il tourne en mode WAL : sur le disque, vous trouverez donc mcpd.db à côté de mcpd.db-wal et de mcpd.db-shm. Copier le .db seul pendant que le processus tourne vous donne un fichier auquel il manque tout ce qui est encore dans le journal d’écriture anticipée.

Prenez la sauvegarde par SQLite, qui lit un instantané cohérent pendant que mcpd continue d’écrire :

sauvegarde à chaud

sqlite3 /data/mcpd.db ".backup '/backup/mcpd-$(date +%F).db'"

Arrêter le processus d’abord et copier les trois fichiers est tout aussi valable et ne demande aucun binaire sqlite3 dans l’image. Il n’y a pas de sous-commande mcpd backup dans cette version, et pas de sauvegarde planifiée : c’est le travail de votre outillage d’instantanés, et prétendre le contraire mettrait un outil à moitié construit sur le chemin critique.

La conservation supprime des lignes mais ne réduit pas le fichier. SQLite garde les pages libérées et les réutilise : l’usage disque plafonne donc au lieu de baisser après une grosse suppression. Lancez un VACUUM sur une instance arrêtée si vous avez réellement besoin de récupérer la place.

Restauration

Remettre une sauvegarde en place tient en quatre étapes, et l’ordre compte à cause du journal d’écriture anticipée.

  1. Arrêtez le daemon. Une restauration sous un processus en marche est un second écrivain sur le même fichier.
  2. Copiez la sauvegarde par-dessus le fichier à --data.
  3. Supprimez les fichiers -wal et -shm à côté. Ils appartiennent à la base que vous venez de remplacer, et laissés en place ils seraient rejoués dans celle que vous avez restaurée.
  4. Démarrez le daemon, et confirmez qu’il lit le fichier restauré : /readyz répond 200, et une liste de traces sur le projet rend les exécutions que la sauvegarde tenait.
restauration, jouée sur un stockage de démonstration
$ 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

Mises à niveau

Remplacez le binaire ou l’étiquette d’image et relancez-le. Le schéma est appliqué au démarrage, à chaque fois, et il est écrit de façon à ne rien changer quand il tourne sur un fichier déjà à jour. La version appliquée est notée dans une table schema_version à l’intérieur du même fichier, et cette version en est à la version 2.

Deux choses à garder en tête. Avancez, ne reculez pas : il n’est pas promis qu’un binaire plus ancien lise un fichier plus récent, donc prenez d’abord la sauvegarde. Et vérifiez spec_version sur /healthz après la mise à niveau si vous figez les versions des SDK, parce que c’est le nombre auquel un client se compare.

Où s’arrête une seule instance

Être honnête là-dessus est plus utile qu’un chiffre que nous n’avons pas mesuré. Nous ne publions aucun chiffre de débit, parce qu’il n’en existe aucun dont nous puissions répondre.

  • Un seul écrivain. Le stockage SQLite tient une seule connexion d’écriture derrière un mutex et quatre connexions de lecture. Le mode WAL veut dire qu’une requête de tableau de bord ne bloque jamais l’ingestion, qui serait la panne qui compte, mais toutes les écritures sont sérialisées par cette unique connexion.
  • Une seule instance par fichier. Deux processus mcpd pointés sur le même fichier n’est pas un déploiement pris en charge. Exploitez-le en réplique unique avec un volume, pas en service mis à l’échelle horizontalement.
  • Les deux limites ci-dessus sont celles du stockage embarqué. Elles cessent de s’appliquer sur ClickHouse, qui est compilé dedans et traité plus bas.
  • Les payloads sont gros. Les prompts et les complétions se comptent en kilooctets, pas en octets, et ce sont eux qui rendent mcpd digne d’être exploité. Le stockage grossit avec le volume de texte bien plus vite qu’avec le nombre de spans : mesurez donc avec vos propres payloads plutôt qu’avec un chiffre de spans par seconde.
  • La conservation couvre les projets que vous avez écrits. La passe horaire supprime dans chaque projet du bloc projects, chacun sur son propre retention_days là où il en pose un et sur le chiffre global là où il n’en pose pas, plus le projet nommé par --project. Un projet inventé par un émetteur et qui ne figure ni dans l’un ni dans l’autre n’a pas de fenêtre et n’est jamais élagué, ce qui vaut d’être su avant de compter sur la conservation comme mesure de confidentialité.

Quand le volume dépasse l’instance, les leviers, du moins coûteux au plus coûteux, sont : --retention-days, qui borne le fichier ; --sample, qui borne le flux entrant et qui est déterministe, si bien qu’une trace gardée est gardée entière ; et le passage à ClickHouse, qui lève ensemble l’écrivain unique et la réplique unique. Voir Échantillonnage et Masquage et conservation.

Postgres, quand vous en exploitez déjà un

Pointez mcpd sur une chaîne de connexion Postgres et il stocke là plutôt que dans le fichier embarqué. Le schéma est créé au démarrage.

stocker dans Postgres

mcpd serve --postgres-url postgres://mcpd:$PGPASSWORD@db:5432/mcpd

Cela lève les deux mêmes limites que ClickHouse. Les écritures ne passent plus par une seule connexion, donc mcpd tourne en plusieurs répliques derrière un seul service, et il le fait sur une base que la plupart des équipes exploitent, sauvegardent et surveillent déjà.

La migration prend un verrou consultatif Postgres et est entièrement sautée quand le schéma est déjà à jour. Plusieurs répliques qui démarrent en même temps sont donc sans danger, et un déploiement progressif ne prend pas de verrou exclusif sur des tables où d’autres répliques écrivent.

La recherche plein texte utilise une colonne tsvector générée avec un index GIN, dans la configuration simple : elle met en minuscules et découpe aux frontières de mots, mais ne fait pas de racinisation, si bien que chercher dans un prompt le jeton littéral que vous avez tapé fait ce que vous attendez.

Choisissez ClickHouse plutôt que Postgres quand la conservation se mesure en milliards de spans. En dessous, la base que vous exploitez déjà est la meilleure réponse.

ClickHouse, quand le fichier n’a plus la bonne forme

Pointez mcpd sur un endpoint HTTP ClickHouse et il stocke là plutôt que dans le fichier embarqué. Le schéma est créé au démarrage : il n’y a donc rien à appliquer à la main.

stocker dans ClickHouse

mcpd serve \ --clickhouse-url http://clickhouse:8123 \ --clickhouse-database mcpd \ --clickhouse-user default --clickhouse-password $CH_PASSWORD

Les deux contraintes ci-dessus se lèvent d’un coup. Les écritures ne passent plus par une seule connexion, donc mcpd tourne en plusieurs répliques derrière un seul service, et les spans atterrissent dans un MergeTree bâti pour la forme réelle de ces données : beaucoup plus d’écriture que de lecture, et des requêtes qui balaient un intervalle de temps.

Choisissez-le quand la conservation se mesure en mois de trafic de production plutôt qu’en disque de portable. En dessous, le fichier embarqué est plus rapide à exploiter, plus rapide à sauvegarder, et c’est une chose de moins à faire tourner. Gardez le fichier tant qu’il ne fait pas mal.

Les traces sont un ReplacingMergeTree, parce qu’une projection de trace est recalculée à mesure que ses spans arrivent et que la dernière écriture est la bonne. Des spans arrivés dans le désordre, réessayés ou mis à jour sur place convergent donc vers la bonne réponse au lieu d’accumuler des doublons.

Une garantie est ici plus faible que sur les deux autres backends, et c’est celle que vous aurez peut-être à certifier. La suppression sur ClickHouse est une mutation asynchrone : une demande d’effacement est acceptée puis appliquée après coup. SQLite et Postgres suppriment à l’intérieur de la transaction qui a répondu. La réponse dit laquelle des deux a eu lieu, dans deletes_pending. Voir Masquage et conservation.

Vérifier un déploiement

mcpd doctor lit la configuration et le stockage et rapporte ce qui ne va pas, en nommant l’option qui répare chaque point. Il existe parce que les pannes qui mordent vraiment sont silencieuses : rien ne plante, et vous l’apprenez des semaines plus tard.

vérifier un déploiement sans rien contacter d’autre que le stockage

mcpd doctor --data /data/mcpd.db --offline

Trois options appartiennent à la commande elle-même. --offline ne contacte rien d’autre que le stockage : l’endpoint de transfert est donc rapporté comme sauté plutôt que vérifié. --read-only n’écrit rien : la vérification du stockage prouve donc qu’il peut être lu et non qu’il peut être écrit, et elle le dit. --window-days est le nombre de jours d’historique que lisent les vérifications d’activité, d’horloge et de tarifs, sept sauf indication contraire, entre 1 et 36525. Tout le reste de ce qu’elle prend est la configuration du daemon lui-même : elle note donc l’invocation que vous démarreriez.

Douze groupes sont notés, dans cet ordre, et le rapport se termine par un décompte de chaque niveau :

GroupeCe qu’il note
storage Le stockage répond à un ping, et un span de sonde survit à une écriture, une lecture et une suppression
runs Le stockage des exécutions répond, combien de versions de définition et d’exécutions en cours il tient, et si la surface d’exécution demande une clé sur une adresse que d’autres peuvent atteindre. En lecture seule : le doctor ne réclame jamais une étape ni ne déclenche une échéance
keys Chaque portée, avec son héritage rendu explicite, pour que le cas où aucune clé d’administration n’est posée et où chaque clé de lecture supprime donc aussi soit sur la page plutôt que dans la documentation
identity L’en-tête de sujet d’audit, et si la plage autorisée à le poser est assez étroite pour être crue
projects Les projets configurés face aux projets qui tiennent des exécutions. Un projet qui n’a jamais reçu de span est presque toujours une faute de frappe dans un nom
retention Jusqu’où remontent les données de chaque projet, et si quoi que ce soit les supprimera un jour
limits Les plafonds d’ingestion par projet et les fractions d’échantillonnage
forwarding L’endpoint de transfert, en y postant un lot OTLP sans aucun span dedans
judge Le bloc du juge, en configuration seulement
monitors Les moniteurs et les objectifs, et les signaux qu’ils lisent
clock Les exécutions des deux extrémités de l’historique de chaque projet face à l’horloge de cette machine, parce qu’un émetteur dont l’horloge retarde se classe sous chaque exécution honnête et n’apparaîtrait jamais sur une page des plus récentes
pricing Les spans de modèle récents face à la grille tarifaire, pour que soit nommé un modèle que rien ne sait tarifer

identity et limits sont les deux plus récemment ajoutés, et les deux n’étaient pas notés pour la même raison : rien chez eux ne plante. Un journal d’audit sans sujet enregistre ce qui a été fait et jamais qui l’a fait. Une fraction d’échantillonnage hors de 0.0 à 1.0 est refusée pour --sample et n’a jamais été vérifiée dans un bloc de projet, si bien qu’un projet pouvait en porter une et le daemon la faire tourner. Un plafond posé par projet sans plafond de premier niveau à côté laisse sans aucun plafond tout projet que personne n’a écrit, et c’est l’émetteur qui choisit le nom du projet.

Sauté ne veut pas dire ok

Une vérification qui n’a pas été jouée dit skipped, jamais ok. Cette distinction est tout l’intérêt de la commande : un sous-système absent de la configuration que vous lui avez donnée n’est pas un sous-système en bonne santé, et rapporter les deux de la même façon rendrait le doctor le plus sûr de lui précisément quand on lui aurait remis le mauvais fichier. La ligne de résumé compte les vérifications sautées à part et le dit en toutes lettres, si bien qu’une exécution ayant vérifié deux de ses quatorze constats ne peut pas se lire comme un certificat de bonne santé.

comment se termine une exécution sur un déploiement neuf
$ 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 stockage occupé n’est pas un stockage cassé

La sonde de stockage est la seule vérification qui a besoin du verrou d’écriture. Tout ce qui est en dessous lit, et les lecteurs ne font pas la queue derrière un écrivain. Une écriture de sonde qui perd la course à ce verrou est rapportée comme skipped : elle est tentée trois fois, et si les trois attendent le verrou et abandonnent, le rapport dit que l’écriture n’a pas été vérifiée, et pourquoi.

Cela compte parce que le stockage embarqué n’accepte qu’un écrivain, et qu’un déploiement sous ingestion réelle tient ce verrou presque en permanence. Rapporter la contention comme une faute faisait échouer un déploiement précisément quand le déploiement était sain et occupé. Les pannes que la sonde existe pour attraper se nomment chacune elles-mêmes et aucune ne dit « verrouillé » : un montage en lecture seule, un disque plein et un rôle sans INSERT restent des fautes, tout comme un verrou corrompu, qui dit « locking protocol » et qui est un vrai dégât.

Le code de sortie

Il sort avec un code non nul sur une faute, pour pouvoir bloquer un déploiement. Un avertissement n’est pas une faute : il sort avec zéro et attend qu’une personne le lise. Une configuration est une faute plutôt qu’un avertissement, et il vaut mieux le savoir avant de câbler cela dans un pipeline : une portée de lecture ouverte sur une adresse qui n’est pas de bouclage, qui remet chaque prompt stocké à quiconque peut atteindre le port. Une portée d’ingestion ouverte sur la même adresse reste un avertissement, parce que des lignes inutiles sont ce que la conservation enlève et qu’une divulgation ne se reprend pas. Une plage d’identité de confiance qui contient tout l’internet est notée de la même façon, pour la même raison : n’importe quelle adresse pourrait alors poser l’en-tête et être enregistrée comme la personne qu’il nomme.

Les réglages à vérifier avant qu’il soit joignable

Quand il écoute une adresse qui n’est pas de bouclage, mcpd journalise au démarrage un avertissement par surface ouverte à un appelant qui ne devrait pas l’atteindre, et chaque avertissement nomme les options à poser. Il avertit plutôt que de refuser de démarrer, parce que refuser casserait le chemin qui mène de rien à une première trace. Cela reste un avertissement sur un vrai trou : une API de lecture ouverte remet chaque prompt stocké à quiconque peut atteindre le port.

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

Sans les trois dernières clés, la clé d’ingestion et la clé de lecture atteignent aussi la surface des exécutions, et la clé de lecture supprime aussi. Le détail complet sur chaque clé, et sur tout le reste de ce que vous pouvez poser, est dans Configuration.