本文へスキップ
mcpd
デーモンなし
ソース

運用

mcpd は 1 つのファイルを抱えた 1 つのプロセスです。そのおかげで運用の大半は短く済み、そのぶん数少ない大事な点は正確に知っておく価値があります。

ポートにあるもの

1 つのアドレスがすべてを提供します。/v1/traces、 /v1/logs、/v1/metrics での OTLP 取り込み、 /v1/ingest でのネイティブ取り込み、/v1/api の下の クエリ API、/ の運用者向けインターフェース、そして下に挙げる 3 つの運用エンドポイントです。設定すべきサイドカーもリバースプロキシも ありません。デプロイが 1 行に収まる理由の大半はここにあります。

インターフェースはバイナリに組み込まれているので、静的ファイルサーバー も 2 つ目のボリュームマウントも要りません。取り込みは大きなボディの上限を 持ち、CORS はありません。クエリ API は CORS を持つので、別のオリジンから 配信されたインターフェースでも読めます。

/healthz と /readyz は同じ問いではありません

この 2 つが分かれているのは、答えの先にある結果が違うからです。両方を同じ ハンドラーにつなぐのは、再起動ループを作るよくあるやり方です。

エンドポイント ストレージに触れるか 答えるもの
GET /healthz いいえ プロセスが応答している間は常に 200
GET /readyz はい、各ストアに 1 回の読み取り シグナルのストアと実行のストアが答えれば 200、どちらかが答えなければ 503

/healthz は liveness です。意図的にストレージには一切触れ ません。遅いデータベースやロックされたデータベースのせいでプロセスが 落とされて再起動されてはいけないからです。再起動してもディスクは直り ませんし、再起動ループのぶんだけ処理中のバッチも失います。

/readyz は readiness です。読み取り用の接続に対して単純な クエリを 1 つ投げるので、提供できないインスタンスにはトラフィックが 送られなくなります。ストレージに届かないときは 503 を返し、 status は degraded、detail には 何が失敗したかが入ります。素のステータスコードだけを見て、そこから 手探りで調べる必要はありません。

どちらの本文も同じ項目を持ちます。status、 version、spec_version、 uptime_seconds、storage、そして異常があるときの detail です。spec_version はこのビルドが実装 しているデータモデルのバージョンで、クライアントが理解できないサーバーと 話すのを断れるようにするための値です。

正常なインスタンスを叩く
$ 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.

どちらのプローブにもキーは要りません。/metrics も同じです。 自分のネットワークでそれが許されないなら、ingress の側で制限してください。

kubernetes

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

# SIGTERM は処理されるので、処理中の取り込みが終わるまでの時間を与えます。
terminationGracePeriodSeconds: 30

メトリクス

mcpd は自分自身のカウンターを /metrics に Prometheus の テキスト形式で書き出します。フィーチャーフラグの後ろには置いていません。 観測できない可観測性ツールは負債だからです。

メトリクス 種類 意味
mcpd_signals_accepted_total counter ストレージに書かれたシグナル
mcpd_signals_rejected_total counter 拒否したシグナル。理由は送信側に返されます
mcpd_batches_duplicate_total counter 再送と判定して飛ばしたバッチ
mcpd_traces_sampled_out_total counter サンプリングポリシーが捨てたトレース
mcpd_signals_over_quota_total counter プロジェクトの毎分の上限を超えて拒否したシグナル
mcpd_signals_unbounded_total counter どの上限も及ばないプロジェクトのまま、上限の判定を通過した シグナル
mcpd_ingest_requests_total counter 処理した取り込みリクエスト
mcpd_query_requests_total counter 処理したクエリリクエスト
mcpd_request_errors_total counter サーバーエラーで失敗したリクエスト
mcpd_audit_write_failures_total counter 書けなかった監査行。その行が説明するリクエスト自体は成功して います
mcpd_uptime_seconds gauge このプロセスが起動してからの秒数

どのカウンターもプロセスの生存期間ぶんで、再起動するとゼロに戻ります。 水準ではなく変化率でアラートを組んでください。見る価値がとくに高いのは 2 つです。mcpd_signals_rejected_total は送信側に「だめだ」と 言い続けていて、相手はおそらく返事を読んでいないという意味です。 mcpd_request_errors_total は定義上 mcpd 自身の落ち度です。 4xx は呼び出し側の落ち度で、こちらには数えられません。

mcpd_traces_sampled_out_total が増えるのは障害ではありません。 自分の実行が見当たらないと誰かが言い出したとき、インターフェースの 抜けを説明する数字です。

mcpd_signals_over_quota_total は、シグナルが着地しない もう 1 つの理由に対する同じ形の数字です。プロジェクトが毎分の上限に 達した、という理由です。この拒否は mcpd_signals_rejected_total にも数えられるので、上限で 絞られているフリートと、不正なデータを送っているフリートを見分けるには こちらを見てください。上限が何を覆うかは 設定にあります。

mcpd_signals_unbounded_total は逆向きの数字です。名指しした プロジェクトにどの上限も及ばなかったために保存されたシグナルの数です。 projects の下だけに書かれた上限は、そこに書かれた プロジェクトしか縛りません。それ以外の名前を送る送信元は、上限の横を 素通りします。これらのシグナルは拒否ではなく計数にとどめてあります。 誰も書いていないプロジェクトを拒否すると、設定を変えずにアップグレード したデプロイのデータを落としてしまうからです。ゼロでなければ、 プロジェクトごとの上限の横にトップレベルの --quota-per-minute を置いてください。

上限は、行が入りうるすべての入口を覆います。4 つの取り込み エンドポイント、単一レコードのスコアとフィードバックのエンドポイント、 そして POST /v1/api/score です。最後のものはスコアを計算 したうえで、保存する前に同じカウンターに課金します。そこで上限に 収まらなかったぶんは、上限を越えて書かれるのではなく、レスポンスの skipped リストにスコアごとに報告されます。

mcpd_audit_write_failures_total は、書けなかった監査行を 数えます。その行が説明していたリクエストは成功し、データも返っています。 そこが要点で、監査の書き込みが、それが説明する読み取りを失敗させること はありません。したがってゼロでないということは、保存済みペイロードの 読み取りが起きたのに監査ログがそれを説明できない、ということです。 可用性の指標ではなく、完全性の指標として扱ってください。

prometheus

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

停止

mcpd は SIGTERM と SIGINT を処理します。 どちらでも、新しい接続の受け付けをやめ、処理中のリクエストを終わらせて からプロセスを終了します。

とくに SIGTERM を処理していることが、オーケストレーターの 下でこれを成り立たせています。Kubernetes は SIGTERM を送り、 猶予期間を待ってから SIGKILL します。SIGINT しか待っていないコンテナは、停止されるのではなく殺され、トランザクション の途中だったバッチは失われます。

きれいな停止
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

終了時に mcpd が救えないのは、送信側のキューにまだ残っているものです。 SDK はスパンをメモリーに保持してワーカーで送り出すので、 shutdown() を呼ばずに終了したプロセスは、まだ送っていない ぶんを失います。これはデーモンではなく、エージェント側で正しく行う停止 処理です。

バックアップ

ストアの全体は --data にある 1 つの SQLite ファイルです。 WAL モードで動くので、ディスク上には mcpd.db と並んで mcpd.db-wal と mcpd.db-shm があります。 プロセスが動いている間に .db だけをコピーすると、 先行書き込みログに残っているものがすべて欠けたファイルになります。

バックアップは SQLite 経由で取ってください。mcpd が書き続けている間も、 一貫したスナップショットを読み出します。

ホットバックアップ

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

先にプロセスを止めて 3 つのファイルをすべてコピーする方法も同じくらい 正しく、イメージの中に sqlite3 のバイナリが要りません。 このビルドに mcpd backup というサブコマンドはなく、定期 バックアップの仕組みもありません。これはあなたのスナップショット基盤の 仕事であり、そうでないふりをすれば、半端な仕組みが重要な経路に居座る ことになります。

保持期間の処理は行を削除しますが、ファイルは小さくなりません。SQLite は 解放したページを保持して再利用するので、大きな削除の後もディスク使用量は 減らずに横ばいになります。容量を本当に取り戻したいなら、止めた インスタンスに対して VACUUM を実行してください。

リストア

バックアップを戻す手順は 4 つで、先行書き込みログがあるために順序が重要 です。

  1. デーモンを止めます。動いているプロセスの下でのリストアは、同じ ファイルに対する 2 人目の書き手になります。
  2. --data のファイルにバックアップを上書きします。
  3. その横の -wal と -shm のファイルを削除します。 これらは置き換えた側のデータベースのものであり、残しておくと、戻した ほうのデータベースに再生されてしまいます。
  4. デーモンを起動し、戻したファイルを読んでいることを確認します。 /readyz が 200 を返し、そのプロジェクトのトレース一覧に バックアップが持っていた実行が返ってきます。
デモ用ストアに対して実行したリストア
$ 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

アップグレード

バイナリかイメージのタグを差し替えて、また起動するだけです。スキーマは 毎回起動時に適用され、最新のファイルに対して実行しても何も変わらない ように書かれています。適用されたバージョンは同じファイルの中の schema_version テーブルに記録され、このビルドはバージョン 2 です。

覚えておくことが 2 つあります。前へ進めるだけで、戻さないこと。古い バイナリが新しいファイルを読める保証はないので、先にバックアップを 取ってください。もう 1 つは、SDK のバージョンを固定しているなら、 アップグレード後に /healthz の spec_version を 確認することです。クライアントが突き合わせるのはその数字だからです。

1 台のインスタンスの限界

ここは正直に書くほうが、測っていない数字を出すより役に立ちます。 スループットの数値は公開しません。自信をもって示せるものが存在しない からです。

  • 書き手は 1 つ。SQLite のストアは、ミューテックスの 後ろに書き込み接続を 1 つ、読み取り接続を 4 つ持ちます。WAL モードなの でダッシュボードのクエリが取り込みを止めることはなく、そこが本当に 困る失敗なのですが、書き込みはすべてその 1 つの接続で直列化されます。
  • ファイル 1 つにインスタンス 1 つ。同じファイルを指した mcpd のプロセスが 2 つある状態は、サポートされたデプロイではありません。 水平にスケールするサービスとしてではなく、ボリュームを付けた 1 台の レプリカとして動かしてください。
  • 上の 2 つはどちらも組み込みストアの制限です。 ClickHouse では当てはまらなくなります。ClickHouse は組み込み済みで、 下で扱います。
  • ペイロードは大きくなります。プロンプトと出力は バイトではなくキロバイトの単位で、そこが mcpd を動かす価値そのものです。 ストレージはスパン数よりもテキスト量ではるかに速く増えるので、 毎秒スパン数のような数字ではなく、自分のペイロードで測ってください。
  • 保持期間が覆うのは、書き留めたプロジェクトだけです。 1 時間ごとの処理は、projects ブロックのすべての プロジェクトから削除します。retention_days を持つ プロジェクトはその値で、持たないプロジェクトはグローバルの値で削除 され、--project が名指しするプロジェクトも対象です。 送信元が勝手に作ったプロジェクトがどちらにも現れなければ、期間を 持たず、一度も削除されません。保持期間をプライバシーの手段として 当てにする前に、知っておく価値があります。

流量がインスタンスの手に余ってきたときの手段を、代償の小さい順に 挙げます。ファイルの大きさを抑える --retention-days、 流入を抑える --sample(決定的なので、残すと決めたトレースは まるごと残ります)、そして書き手が 1 つという制限とレプリカが 1 台と いう制限を同時に外す ClickHouse への移行です。 サンプリング、および 秘匿化と保持期間を 参照してください。

すでに Postgres を運用している場合

mcpd に Postgres の接続文字列を渡すと、組み込みファイルではなくそちらに 保存します。スキーマは起動時に作られます。

Postgres に保存する

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

これで ClickHouse と同じ 2 つの制限が外れます。書き込みが 1 つの接続を 通らなくなるので、mcpd は 1 つのサービスの後ろで複数のレプリカとして 動きます。しかも、たいていのチームがすでに運用し、バックアップし、 監視しているデータベースの上でそうできます。

マイグレーションは Postgres のアドバイザリーロックを取り、スキーマが すでに最新なら丸ごと飛ばされます。だから複数のレプリカが同時に起動して も安全ですし、ローリングデプロイが、他のレプリカが書き込んでいる テーブルに排他ロックを取ることもありません。

全文検索は GIN インデックスを張った生成列の tsvector を 使い、構成は simple です。小文字化して語の境界で区切ります が、語幹処理はしません。プロンプトを、打ち込んだそのままのトークンで 検索したときに期待どおりに動きます。

保持期間が数十億スパンで測られるようになったら、Postgres ではなく ClickHouse を選んでください。それより下なら、すでに運用している データベースのほうが良い答えです。

ファイルでは形が合わなくなったときの ClickHouse

mcpd に ClickHouse の HTTP エンドポイントを渡すと、組み込みファイルでは なくそちらに保存します。スキーマは起動時に作られるので、手で適用する ものはありません。

ClickHouse に保存する

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

上に挙げた制約が両方とも一度に外れます。書き込みが 1 つの接続を通らなく なるので、mcpd は 1 つのサービスの後ろで複数のレプリカとして動き、 スパンは、このデータが実際に持つ形に合わせて作られた MergeTree に着地します。読み取りよりはるかに書き込みが多く、 時間範囲を走査するクエリが来る、という形です。

保持期間が、ノート PC のディスクではなく本番トラフィックの数か月で 測られるようになったら選んでください。それより小さいうちは、組み込み ファイルのほうが動かすのも速く、バックアップも速く、運用するものが 1 つ 少なくて済みます。つらくなるまではファイルのままで構いません。

トレースは ReplacingMergeTree です。トレースの集約は スパンが届くたびに計算し直され、最後の書き込みが正しいものだからです。 順序が入れ替わって届いたスパン、再送されたスパン、その場で更新された スパンは、重複を積み上げるのではなく正しい答えに収束します。

ここでは他の 2 つのバックエンドより弱くなる保証が 1 つあり、それは あなたが証明を求められるかもしれない保証です。ClickHouse での削除は 非同期のミューテーションなので、消去のリクエストは受理され、そのあとで 適用されます。SQLite と Postgres は、応答したトランザクションの中で 削除します。どちらが起きたかは deletes_pending で返答に 書かれます。 秘匿化と保持期間を 参照してください。

デプロイの点検

mcpd doctor は設定とストアを読み、何がおかしいかを、それを 直すフラグの名前とともに報告します。これがあるのは、実際に痛い目に あわせる失敗が静かなものだからです。何も落ちず、何週間もあとで気付く のです。

ストア以外には何も接続せずにデプロイを点検する

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

3 つのフラグはこのコマンド専用です。--offline はストア以外 には何も接続しないので、転送先は点検したことにはならず、飛ばしたと報告 されます。--read-only は何も書かないので、ストレージの点検は ストアが読めることを示すだけで、書けることは示さず、そのように報告します。 --window-days は、活動、時刻、価格表の点検が何日ぶんの履歴を 読むかで、指定しなければ 7 日、1 から 36525 の間です。それ以外に受け取る のはデーモン自身の設定なので、あなたが起動するはずの呼び出しをそのまま 採点します。

12 のグループがこの順に採点され、レポートの最後に各レベルの件数が 出ます。

グループ採点する内容
storage ストアが ping に答えること、そして試験用のスパンが書き込み、読み取り、削除を通り抜けること
runs 実行ストアが答えること、保持している定義のバージョン数と実行中の実行の数、そして他の人が届くアドレスで実行まわりにキーが必要かどうか。読み取りのみで、doctor がステップを取得したり期限を発火させたりすることはありません
keys すべてのスコープと、その継承関係を明示します。管理キーが設定されておらず、そのためどの読み取りキーでも削除できてしまう状態が、ドキュメントの中ではなく画面に出ます
identity 監査の主体ヘッダーと、それを設定してよい範囲が信じられるほど狭いかどうか
projects 設定されたプロジェクトと、実行を持つプロジェクトの突き合わせ。スパンを一度も受け取っていないプロジェクトは、ほぼ必ず名前の打ち間違いです
retention 各プロジェクトのデータがどこまで遡るか、そしてそれをいつか削除するものがあるかどうか
limits プロジェクトごとの取り込み上限と、サンプリングの割合
forwarding 転送先を、スパンの入っていない OTLP バッチを POST して確かめます
judge ジャッジのブロックを、設定としてのみ確認します
monitors モニターと目標、およびそれらが読むシグナル
clock 各プロジェクトの履歴の両端にある実行を、このマシンの時計と突き合わせます。時計が遅れている送信元の実行は、正直な実行すべての下に並んでしまい、最新のものを並べたページには決して現れないからです
pricing 最近のモデルのスパンを価格表と突き合わせ、どこからも価格を付けられないモデルを名指しします

identity と limits は最も新しく加わった 2 つで、 どちらも同じ理由で採点されていませんでした。どちらも落ちないのです。 主体の無い監査ログは、何が行われたかは記録しますが、誰が行ったかは 決して記録しません。0.0 から 1.0 の外にあるサンプリングの割合は --sample では拒否されますが、プロジェクトのブロックでは 一度も検査されていなかったので、プロジェクトがそれを持っていても、 デーモンはそのまま動いていました。プロジェクトごとに上限を設定し、その横に トップレベルの上限を置かない構成は、誰も書き留めていないすべての プロジェクトを上限なしのままにします。しかもプロジェクト名を選ぶのは 送信元です。

skipped は ok ではありません

実行されなかった点検は skipped と表示され、ok には なりません。この区別こそがこのコマンドの主眼です。指定した設定に 存在しないサブシステムは、健康なサブシステムではありません。両者を同じ ように報告すれば、doctor は間違ったファイルを渡されたときにこそ最も 自信満々になってしまいます。集計行は飛ばした点検を別に数え、言葉でも そう書くので、14 件の所見のうち 2 件しか確かめていない実行が、健康 診断書のようには読めません。

新しいデプロイに対する実行の終わり方
$ 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.

混んでいるストアは壊れたストアではありません

ストレージの試験だけが書き込みロックを必要とします。それより下はすべて 読み取りで、読み手が書き手の後ろで待つことはありません。試験の書き込みが そのロックの取り合いに負けた場合は skipped と報告 されます。試行は 3 回で、3 回ともロックを待って諦めたときは、書き込みを 確認できなかったことと、その理由がレポートに出ます。

これが重要なのは、組み込みストアの書き手が 1 つで、本番の取り込みを 受けているデプロイはそのロックをほぼ絶えず握っているからです。競合を 障害として報告していたころは、デプロイが健康で忙しいときにかぎって このコマンドがデプロイを止めていました。この試験が捕まえるべき失敗は それぞれ自分の名前を名乗り、そのどれも「locked」とは言いません。 読み取り専用のマウント、いっぱいのディスク、INSERT の無い ロールは今も障害ですし、壊れたロックも障害です。それは「locking protocol」と名乗り、実際の損傷です。

終了コード

障害があれば非ゼロで終了するので、デプロイの門番にできます。警告は障害 ではありません。ゼロで終了し、人が読むことを期待します。1 つだけ、警告 ではなく障害として扱う構成があり、これはパイプラインに組み込む前に 知っておく価値があります。ループバックでないアドレスで読み取りスコープ が開いている構成です。これはポートに届く誰にでも、保存されたプロンプトを すべて渡します。同じアドレスで取り込みスコープが開いているのは警告の ままです。ごみの行は保持期間が消してくれますが、漏れたものは取り返せ ないからです。インターネット全体を含む信頼アドレス範囲も、同じ理由で 同じ扱いです。どのアドレスからでもヘッダーを設定でき、名乗ったとおりの 人物として記録されてしまいます。

外から届くようにする前に確認する設定

ループバックでないアドレスにバインドしたとき、mcpd は、届くべきでない 相手に開いている面ごとに起動時の警告をログに出し、それぞれの警告は設定 すべきフラグの名前を示します。起動を拒否するのではなく警告するのは、拒否 すると何も無い状態から最初のトレースまでの道が断たれてしまうからです。 それでもこれは、実在する穴についての警告です。読み取り API が開いていれば、 ポートに届く誰にでも、保存されたプロンプトをすべて渡すことになります。

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

最後の 3 つのキーが無いと、ingest キーと read キーは実行の面にも届き、 read キーは削除もできます。各キーの詳細と、設定できるそれ以外のすべては 設定にあります。