Skip to content

オペレーションランブック

このランブックは Recotem の本番デプロイメントにおける day-two オペレーションを扱います: 鍵ローテーション、アーティファクトリカバリ、CLI フラグリファレンス、学習パイプラインの可観測性、メモリサイジング、SIGTERM 処理、ウォッチャーのセマンティクス、バックアップ、モニタリング、アップグレード、トラブルシューティング。

完全な環境変数リファレンスについては 環境変数 (またはすべての変数とデフォルト値・スコープを一覧する Docker デプロイメント ページのテーブル) を参照してください。


署名鍵のローテーション

署名鍵は RECOTEM_SIGNING_KEYS にカンマ区切りの <kid>:<hex64> エントリのリストとして設定します (64 hex 文字 = 32 生バイト)。サーバーはいずれかのエントリに対して検証します。recotem train は常に最初のエントリ (アクティブキー) で署名します。

このマルチ kid パターンにより、ゼロダウンタイムのローテーションが可能です。

ステップバイステップのローテーション

1. 新しい鍵を生成する。

bash
recotem keygen --type signing --kid prod-2026-q3
# kid=prod-2026-q3
# plaintext=<64 hex chars>       <-- 32 生バイト; これが署名鍵
# fingerprint=ddeeff00           <-- sha256(key_bytes)[:8]; /security.posture ログと一致
# env_entry=RECOTEM_SIGNING_KEYS=prod-2026-q3:<64 hex chars>

署名鍵の場合、plaintext 行が実際の鍵です — それ (または既成の env_entry= 行) を RECOTEM_SIGNING_KEYS にコピーしてください。fingerprint= 行は sha256(key_bytes)[:8] であり、起動時の security.posture ログの fingerprint フィールドと一致します。これは情報提供のみで、RECOTEM_SIGNING_KEYS で使用してはなりません。(sha256: ワイヤープレフィックスは RECOTEM_API_KEYS エントリ専用です。)

2. 新しい kid を最初のエントリとして追加し、古いものを残す。

bash
# 変更前:
RECOTEM_SIGNING_KEYS="prod-2026-q2:aabbcc..."

# 変更後 (新しい鍵を最初に):
RECOTEM_SIGNING_KEYS="prod-2026-q3:ddeeff...,prod-2026-q2:aabbcc..."

更新した環境変数で recotem serve を再起動 (またはリロード) してください。サーバーはどちらの kid で署名されたアーティファクトも受け入れるようになります。

3. すべてのモデルを再学習する。

各レシピに対して recotem train を実行します。各新しいアーティファクトは prod-2026-q3 (最初のエントリ) で署名されます。サーバーは新しいアーティファクトが現れるたびに各モデルをホットスワップします。prod-2026-q2 で署名された古いアーティファクトは各レシピが再学習されるまで引き続きサービスされます。

4. 古い kid を削除して検証する。

すべてのレシピが再学習されてホットスワップされたら、古いエントリを削除してください。

bash
RECOTEM_SIGNING_KEYS="prod-2026-q3:ddeeff..."

recotem serve を再起動します。古い kid で署名されたアーティファクトはロードに失敗し、/v1/health/detailsloaded: false と表示されます。それらのレシピを再学習してください。

すべてのレシピが正常にロードされたことを確認してください。レシピごとの状態は認証が必要な /v1/health/details エンドポイントにあります — パブリックな /v1/health{status, total, loaded} の集計値と、レシピファイルがまったくパースできなかった場合の skipped (パースできないレシピファイル を参照) のみを返します。

bash
# -f / --fail は 4xx/5xx で終了コード 22 を返し、503 を隠す場合がある。
# 代わりに -w でステータスコードを取得する。
HTTP_STATUS=$(curl -s -o /tmp/health.json -w "%{http_code}" \
  -H "X-API-Key: $RECOTEM_API_PLAINTEXT" \
  http://localhost:8080/v1/health/details)
echo "HTTP $HTTP_STATUS"
jq '.recipes | to_entries[] | select(.value.loaded == false)' /tmp/health.json

jq コマンドの出力が空であれば、すべてのレシピが新しい鍵で正常にロードされています。

鍵フィンガープリント

起動時に recotem serve は kid ごとに sha256(key)[:8] を含む security.posture イベントをログ出力します。鍵自体を公開することなく正しい鍵がアクティブであることを確認できます。

json
{"event": "security.posture", "signing_keys": [{"kid": "prod-2026-q3", "fingerprint": "ddeeff00"}], ...}

API キーのローテーション

API キーは RECOTEM_API_KEYS<kid>:sha256:<hex64> エントリとして格納されます。ローテーションは追加式です: 新しいエントリを追加し、クライアントを更新し、古いエントリを削除します。

1. 新しい鍵を生成する。

bash
recotem keygen --type api --kid client-a-v2
# kid=client-a-v2
# plaintext=<43-char base64url — クライアントに共有する>
# hash=sha256:<64-hex — RECOTEM_API_KEYS に入れる>
# env_entry=RECOTEM_API_KEYS=client-a-v2:sha256:<64-hex>

--type api が必要です — 指定しない場合 recotem keygen はデフォルトで --type signing となり、誤ったキーフォーマットを出力します。

2. 古いエントリの隣に新しいエントリを追加する。

bash
# 変更前:
RECOTEM_API_KEYS="client-a:sha256:oldhhh..."

# 変更後:
RECOTEM_API_KEYS="client-a:sha256:oldhhh...,client-a-v2:sha256:newhhh..."

recotem serve を再起動します。両方の鍵が同時に有効になります。新しいプレーンテキストをクライアントに共有してください。

3. クライアントが新しい鍵に切り替える。

4. 古いエントリを削除する。

bash
RECOTEM_API_KEYS="client-a-v2:sha256:newhhh..."

recotem serve を再起動します。

プレーンテキストは生成時に一度だけ表示されます。紛失した場合は新しい鍵を生成してください — リカバリの手段はありません。


破損したアーティファクトからのリカバリ

アーティファクトが破損している場合 (不完全な書き込み、ディスクエラー、ストレージ側の破損)、recotem serve はエラーをログ出力し、レシピを loaded: false としてマークします。起動時のイベント名は initial_artifact_parse_failed (または initial_artifact_read_failed) で、ウォッチャーのホットスワップ中は artifact_load_failed です。

json
{"event": "artifact_load_failed", "name": "my_recipe", "error": "magic bytes mismatch", "kid": "<unknown>"}

kid フィールドが "<unknown>" になるのは、アーティファクトが完全な kid を保持するには短すぎる場合 (不完全な書き込み、ゼロバイトファイル) のみです。期待される長さの改ざんまたは誤ったマジックファイルの場合、解析された kid 文字列がそのまま表示されます。

サーバーは継続して動作し、そのレシピの推薦エンドポイントに対して 503 を返します。

リカバリ手順:

1. アーティファクトを検査する (破損したファイルでも安全 — HMAC とサイズチェックがデシリアライズ前に拒否します)。recotem inspect はローカルパスと fsspec URI の両方を受け付けます。

bash
recotem inspect ./artifacts/my_recipe.recotem
# ローカルパス — 終了コード 5: ArtifactError: magic bytes mismatch

recotem inspect s3://my-bucket/artifacts/my_recipe.recotem
# オブジェクトストア URI — 同じ終了コードが適用される

2. 再学習する。

bash
recotem train ./recipes/my_recipe.yaml

新鮮な署名済みアーティファクトが書き込まれます。サーバーは次のポーリングで新しいファイルを検知してホットスワップします。

3. 確認する。

bash
curl -H "X-API-Key: $RECOTEM_API_PLAINTEXT" \
  http://localhost:8080/v1/health/details | jq '.recipes.my_recipe'
# {"loaded": true, "best_class": "IALSRecommender", ...}

versioning: append_sha でアーティファクトが書き込まれた場合、古い破損ファイルは sha サフィックス付きの名前でまだ存在します。新しいアーティファクトのロードを確認してから削除できます。

bash
ls ./artifacts/
# my_recipe.recotem           <- ポインターファイル (現在のものを指す)
# my_recipe.abc12345.recotem  <- 古い破損ファイル (削除可能)
# my_recipe.def67890.recotem  <- 新しい正常ファイル (現在)
rm ./artifacts/my_recipe.abc12345.recotem

CLI フラグリファレンス

recotem train フラグ

フラグデフォルト説明
--no-lockfalseレシピごとの POSIX ファイルロック取得をスキップする。別のメカニズム (例: スケジューラーレベルのミューテックス) で同時書き込みがないことを保証できる場合のみ安全。
--fail-on-busyfalseレシピのロックが保持されている場合、デフォルトの動作 (終了コード 0、recipe_lock_contended_skipping をログ) の代わりに即座に終了コード 6 (LockContestedError) で終了する。非ゼロを「他で再試行」と扱うオーケストレーターで使用する。
--lock-timeout <seconds>0.0失敗前にレシピのロックを待機する秒数。0.0 = ノンブロッキング即時失敗 (デフォルト)。-1 = 無期限待機。--no-lock が設定されている場合は無効。
-q / --quietfalseOptuna のトライアルごとの出力を抑制する。大きな探索予算でのログ量を削減する。
-v / --verbosefalseトライアルごとのハイパーパラメータ値をログに出力する。探索動作のデバッグに有用。本番環境では使用しないこと (大量のログを生成する場合がある)。
--run-id <id>ランダム 12-hex安定した実行識別子。同じ値を繰り返し使用することで永続的な Optuna スタディを再開できる (レシピに training.storage_path が設定されている必要がある)。パターン: [A-Za-z0-9_.-]{1,64}。省略すると毎回新しいランダム ID が生成される。
--env-var KEY=VALUEシェル環境にエクスポートせずにレシピの環境変数展開用の追加 RECOTEM_RECIPE_* 値を注入する。KEYRECOTEM_RECIPE_ で始まる必要がある。繰り返し可能: --env-var A=x --env-var B=y
--dev-allow-unsignedfalseHMAC 署名をスキップし、決定論的なインメモリ開発鍵を使用する。RECOTEM_ENV=development--i-understand-this-loads-arbitrary-code の両方が必要。管理されたローカルテスト環境以外では絶対に使用しないこと。

recotem inspect フラグ

recotem inspect はアーティファクト引数としてローカルパスと fsspec URI の両方を受け付けます。

bash
recotem inspect ./artifacts/my_recipe.recotem           # ローカルパス
recotem inspect s3://my-bucket/artifacts/my.recotem     # S3 URI
recotem inspect gs://my-bucket/artifacts/my.recotem     # GCS URI
recotem inspect az://my-container/artifacts/my.recotem  # Azure Blob URI
recotem inspect https://host/artifacts/my.recotem        # HTTPS URI

RECOTEM_SIGNING_KEYS が設定されている必要があります (または RECOTEM_ENV=development--dev-allow-unsigned)。署名鍵が存在せず --dev-allow-unsigned が渡されない場合、inspect は終了コード 8 (_EXIT_CONFIG) で終了します — 5 ではありません。

フラグデフォルト説明
--dev-allow-unsignedfalseRECOTEM_SIGNING_KEYS が未設定の場合、決定論的なインメモリ開発鍵 (dev:0000…) に対して検証する。recotem train --dev-allow-unsigned で生成されたアーティファクトの検査に有用。

完全な終了コード表については 終了コードとエラー を参照してください。


学習パイプラインイベント

成功した学習実行はこれらの構造化イベントを順番に出力します。SLO とアラートルールの基礎として使用してください。

イベントフェーズ主要フィールド
training_started開始recipe, run_id
fetching_dataデータソース
data_fetchedデータソースn_rows
data_cleansedクレンジングn_rows, drop_count
splitting_data / split_done分割val_offset
search_startedチューニングalgorithms, n_trials
search_doneチューニングbest_class, best_score, n_completed
training_final_model / final_model_trained再フィットrecommender
artifact_written永続化versioning, artifact, pointer (append_sha), kid
train_done終了name, run_id, exit_code, artifact, best_class, best_score, trials, n_orphaned, trained_at, kid, recipe_hash, n_rows, n_users, n_items
train_error失敗error, code (非ドメイン例外は internal_error)、recipe, run_id, exit_code, trained_at; code=min_data_violation の場合はさらに n_rows, n_users, n_items, min_rows, min_users, min_items
recipe_lock_contended_skipping開始recipe, run_id (デフォルト --fail-on-busy=False は終了コード 0)
csv_source_redirectデータソースfrom_, to, status
csv_source_size_exceededデータソースpath, bytes_read, cap
metadata_source_redirectデータソースfrom_, to, status
metadata_source_size_exceededデータソースpath, bytes_read, cap

csv_source_redirect / csv_source_size_exceeded にアラートを設定するオペレーターは、metadata_source_redirect / metadata_source_size_exceeded にも同等のアラートを追加してください。どちらのイベントファミリーも、HTTP/HTTPS フェッチがリダイレクト上限またはバイト上限に達したときに発生します。

train_error イベントはレシピ名フィールドに (recipe= ではなく) name= を使用し、署名 kid が判明している場合は kid= を含みます。これは train_done イベントのフィールド名と一致します。

ウォッチャーとローダーの構造化ログイベント

アラートに有用な、ウォッチャー、レシピローダー、サイズ上限ヘルパーが出力する追加イベント:

イベントレベル出力元重要性
recipe_security_violation_skippedERRORrecipe/loader.py 寛容なローダーレシピファイルにセキュリティカテゴリのエラー (パストラバーサル、許可されていないスキーム、埋め込まれた認証情報) が含まれる。レシピはスキップされるがサーバーは継続して動作する。アラート対象 — 設定ミスまたは潜在的に悪意のあるレシピファイルを示す。
recipe_load_error_skippedWARNrecipe/loader.py 寛容なローダー非セキュリティ上の理由 (スキーマエラー、YAML パースエラー) でレシピのロードに失敗した。レシピはスキップされる。
size_cap_probe_failedWARN_size_cap.pyオブジェクトストアパスへの fsspec info() 呼び出しが予期せず失敗した。サイズ上限チェックがスキップされた。後続の読み取りは続行されるが、事前読み取り上限の制限を受けない。
auth_anonymous_bypassDEBUGserving/auth.pyAPI キーなしで通過したすべてのリクエスト (RECOTEM_API_KEYS が空の場合)。アクセスログ相関のためすべてのリクエストで出力される。
auth_anonymous_bypass_first_seenINFOserving/auth.py特定の client_host からの最初の匿名リクエスト (プロセスごと)。最初に見た IP を追跡する LRU キャッシュは 1024 エントリに制限される。
kid_extraction_failedWARNserving/watcher.pyアーティファクトの kid バイトを生バイトから解析できなかった。
artifact_stat_timeoutWARNserving/watcher.pystat() フューチャーがフューチャーごとのタイムアウト内に完了しなかった。ハングしたオブジェクトストアの stat はティックの進行や SIGTERM 処理をブロックしなくなった。
sidecar_disappearedWARNserving/watcher.py前回のポーリングでは存在していた .sha256 サイドカーファイルが、今回の読み取りで ENOENT になった。ポーリングごとではなく、消失の遷移ごとに 1 回だけ出力される。
artifact_load_error_clearedINFOserving/watcher.py未解消の last_load_error が、もはや成立しないことをウォッチャーが示せたため撤回された。reason (marker_restored または bytes_unchanged) と、撤回された本文を 200 文字で切り詰めた previous_error を伴う。artifact_load_failed と対にすることで、レシピが degraded だった期間を特定できる。

同時学習と永続的な探索ストレージ

recotem train は作業の開始前に <recipe.output.path>.lock でレシピごとの POSIX flock を取得します。ロックはホストローカルです: flock は同一ホスト上のプロセスのみを調整します。output.path がリモート URI (s3://, gs://, http(s)://, ...) の場合、ロックファイルは URI から派生したホストローカルパスに作成され、別の Pod やノードによる同じアーティファクトへの同時書き込みを防ぎません。ホスト間のシングルライター保証にはスケジューラーを使用してください (Kubernetes の concurrencyPolicy: Forbid、Argo の synchronization.mutex、Airflow の max_active_runs=1 など)。Recotem はリモートスキームの実行ごとに recipe_lock_local_only をログ出力します。

ロックのデフォルト動作:

  • ノンブロッキング: ロック競合が発生した場合は即座に終了コード 0 と recipe_lock_contended_skipping で終了します (cron フレンドリー: 遅い実行によって重複したジョブが積み重なりません)。
  • --fail-on-busy: これを終了コード 6 (LockContestedError) に変更し、オーケストレーターが作業を他の場所に委任できるようにします。LockContestedError は意図的に TrainingError 階層の外にあります — これはオーケストレーションの状態であり、学習の失敗ではありません。
  • --no-lock: ロック取得を完全にスキップします。他のメカニズムで同時書き込みがないことを保証できる場合のみ安全です。

単一ホストまたは分散クラスター上での複数プロセスの Optuna 探索 (並列化) には、レシピに training.storage_path を設定してください。受け入れられる形式: 裸のパス (SQLite)、または sqlite://postgresql+psycopg://mysql+pymysql://mariadb+pymysql:// で始まる URL。+driver サフィックスは必須です: この URL は Optuna の RDBStorage にそのまま渡され、RDBStorage はドライバの事前チェックを行いません。そのため裸の postgresql:// (インストールされていない psycopg2 にルーティングされます) は Optuna 内部で ImportError: Failed to import DB access module for the specified storage URL として失敗し、postgres:// (SQLAlchemy 2.x で削除されたダイアレクト) は NoSuchModuleError として失敗します。2.1.0 以降は、どちらもその手前で検出されます。recotem validate は — そして recotem train もデータを取得する前に — URL を事前チェックし、ダイアレクト名・使うべき綴り・インストールすべきエクストラを示したうえで code: storage_path_unusable とともに終了コード 8 で終了します。

サーバーバックエンドの形式には、加えてドライバのエクストラのインストールが必要です。素の pip install recotem にはどちらも含まれていません:

bash
pip install "recotem[postgres]"   # postgresql+psycopg:// 用
pip install "recotem[mysql]"      # mysql+pymysql:// 用

これは見落としやすい問題です。URL 自体はパースが通ってしまうためです: sqlalchemy は推移的にすべてのインストールへ入ります (Optuna が依存しており、Optuna はコア依存です) が、psycopgpymysql は上記のエクストラにしか含まれません。そのため素のインストールでは、推奨形式である postgresql+psycopg:// であっても、綴りを間違えた場合とまったく同じ ImportError: Failed to import DB access module for the specified storage URL で失敗し、メッセージからは両者を区別できません。+driver サフィックスを既に書いているのにこのエラーが出る場合は、エクストラをインストールしてください。

同じレシピに対する複数の recotem train 呼び出しは、作業を重複させるのではなく共有トライアルプールに収束します。スタディ名は recotem_<recipe.name>_<run_id> です。


アトミック書き込みの保証

recotem train は同じディレクトリの一時ファイルにアーティファクトを書き込み、fsync() でデータをフラッシュし、その後 os.replace() します — ローカル FS 上では POSIX アトミックなため、リーダーは不完全なファイルを見ることはありません。オブジェクトストア (S3 / GCS / Azure) では put_object セマンティクス (最後の書き込みが勝つ) でアーティファクトが書き込まれます。versioning: append_sha モードでは、不変の sha サフィックス付きオブジェクトが最初に書き込まれ、次に小さなポインターオブジェクトが上書きされます。ローテーション中にポインターを開いたリーダーは、古いまたは新しいターゲット名のどちらかを見ます — 不完全なポインターは見ません。


SIGTERM / ドレインシーケンス

uvicorn が SIGTERM (または SIGINT) を受け取ったとき:

  1. uvicorn は新しい接続の受け入れを停止する。
  2. FastAPI のライフスパンが終了する: ArtifactWatcher.stop() が呼び出され、ポーリングスレッドは次のティック (≤ RECOTEM_WATCH_INTERVAL 秒) で終了する。繰り返しの警告タスクはキャンセルされる。
  3. 進行中のリクエストには RECOTEM_DRAIN_SECONDS (デフォルト 30) まで完了する時間が与えられ、uvicorn はその後残りの接続を閉じる。
  4. drain_seconds とともに最終的な serve_shutdown イベントがログに記録される。

Kubernetes では、SIGKILL の前にウォッチャーのティックとドレインウィンドウを確保するため、terminationGracePeriodSecondsRECOTEM_DRAIN_SECONDS + 5 以上に設定してください。


recotem serve のメモリサイジング

各モデルレプリカはロードされたすべてのモデルを RAM に保持します。適切に計画してください。

要因影響
RECOTEM_MAX_ARTIFACT_BYTESアーティファクトファイルごとのハード上限 (デフォルト 2 GiB、[1 MiB, 16 GiB] にクランプ)。小さなモデルが多い場合は削減する。
RECOTEM_MAX_PAYLOAD_BYTESアーティファクトごとのデシリアライズ済みペイロードの上限 (デフォルト 512 MiB、HMAC 検証後)。RECOTEM_MAX_ARTIFACT_BYTES 以下でなければならない。そうでない場合、recotem serve は起動時に ConfigError (終了コード 8) で失敗する。
RECOTEM_MAX_BODY_BYTESHTTP リクエストボディごとのハード上限 (デフォルト 128 MiB、[1 MiB, 2 GiB] にクランプ)。Starlette がボディをバッファリング/パースする前に 413 PAYLOAD_TOO_LARGE が返るため、単一のリクエストが上限以上を確保させることはない。制限するのは 1 リクエストであってプロセスではない — 後述の同時リクエストボディには上限がありませんを参照。
レシピ数各レシピは 1 つのモデルをロードする。アーティファクト 500 MiB × 10 レシピは 5 GiB ではなく常駐約 24 GiB — 後述の倍率を参照。
レプリカ数各レプリカは独立している。2 レプリカ = 2 倍のメモリ。
アイテムメタデータレシピごとのインメモリ DataFrame。サイズ ≈ 行数 × 列数 × 8 バイト。

おおよその計算式:

Pod あたりの RAM ≈ (4.8 × avg_artifact_size_GiB × n_recipes)
                 + (avg_metadata_size_GiB × n_recipes)
                 + 0.25 GiB プロセスのベースライン

危険 — ロード済みアーティファクトはディスク上のサイズの数倍のコストがかかります

このページの以前の版はこれを 1 倍と数え、固定で 1 GiB を足していました。その計算式が安全側になるのは、モデルが小さくて定数項が支配的なあいだだけです。おおよそ 213 MiB のアーティファクトを境に過大評価から過小評価に転じ、644 MiB では真の値の半分を予測します。これは本番では応答の遅さではなく、起動時の OOMKill として現れる方向です。

実測 (レシピ 1 つ、アイテムメタデータなし):

インタラクション数ユーザー数 × アイテム数ディスク上のアーティファクト準備完了後の serve の RSS旧計算式
10 万5,000 × 1,0001.4 MiB228 MiB1,025 MiB
100 万50,000 × 5,00055.9 MiB488 MiB1,080 MiB
1,000 万500,000 × 50,000644.5 MiB3,292 MiB1,668 MiB

この 3 点は RSS ≈ 4.8 × アーティファクト + 0.22 GiB によく当てはまります。

倍率の出どころ。 read_artifact はファイル全体そのペイロードスライスを同時に保持します (スライスはコピーです)。したがって生バイトが二重に常駐し、そのうえにデシリアライズされたモデルが 3 つ目のコピー相当として乗ります。あとからペイロードのバイト列を解放しても、同一プロセス内で OS にメモリが返るわけではありません。落ち着いてほしい定常状態ではなく、この数値でコンテナをサイジングしてください。

RECOTEM_MAX_PAYLOAD_BYTES はこれを制限します (そのための変数です)。ただし制限がかかるのは上限値そのものではなく、上限値の約 4.8 倍です。デフォルトの 512 MiB では、実際に上限まで達するレシピ 1 つにつき常駐メモリ約 2.5 GiB を見込んでください。

大きなモデル (多くのコンポーネントを持つ IALS、大規模なアイテムセット) の場合、ホストサイズを決定する前に recotem inspect を使って data_statsbest_params をヘッダーから読み取ってください。ペイロードサイズを決める項は 2 つあり、どちらが支配的かはデータセットによります。

payload ≈ (n_users + n_items) × n_components × 4 B     <- 因子行列
        + n_rows × 約 12 B                              <- pickle されたインタラクション行列

irspack のレコメンダーは学習済みオブジェクトにユーザー×アイテムの CSR を保持するため、重複除去後のインタラクション行がすべて約 12 バイトでアーティファクトに pickle されます。この項は探索が小さい n_components を選んでも縮みません。8 回の実行で測定したところ、この 2 項の見積もりは誤差 2.7% 以内に収まり、しかも常に低めでした。正確な値としてではなく下限として扱い、余裕を持たせてください。n_components だけを読むのは通用しません。同じ 8 回で因子エントリあたりのバイト数は 4.75〜18.32 と幅があり、これはヘッダーの best_params ではなく 1 エントリの背後にあるインタラクション数で決まります。

recotem train は毎回 artifact_writtenartifact_bytespayload_bytes をログ出力し、書き出したファイルがこのホストの recotem serve なら拒否するサイズだった場合は artifact_payload_exceeds_serve_cap (RECOTEM_MAX_PAYLOAD_BYTES を名指し) で警告します。上の計算は計画のためのものであり、実行そのものが答えを教えてくれます。

recotem serve はプロセスあたり最大 100 レシピ向けに設計されています。それを超える場合は複数の serve プロセスにレシピをシャーディングしてください (別々の --recipes ディレクトリ、別々のポート、プロキシレイヤーでロードバランシング)。

同時リクエストボディには上限がありません

RECOTEM_MAX_BODY_BYTES が制限するのは 1 つのリクエストです。進行中のバイト数の予算も同時実行数の制限もないため、大きなボディが同時に到着した数に比例して常駐メモリが増え、しかもそのすべてが受理されます。

クリーンなサーバーからの実測では、63.5 MiB のボディ 1 本で 213 MB — ボディの 3.35 倍でした。実行のたびにサーバーを再起動して測り直したところ、同時 4 本の最大ボディはそれぞれ 3.1〜3.3 倍でした。レプリカのピークの実用的な見積もりは次のとおりです。

ピーク RSS ≈ アイドル + (同時に走る大きなボディの数) × (ボディサイズ) × 3.3

危険 — 倍率は 1.1 ではなく 3.3 です

このページの以前の版は 1.1 と記載しており、ピークを約 2.2 倍過小評価していました。本番では応答の遅さではなく OOMKill として現れる方向です。

Helm チャートのデフォルト limits.memory: 4Gi とデフォルトの 128 MiB ボディ上限に対しては、最大サイズのリクエストが同時に約 8 本で上限に達します。この数値が前提としている小さなアイドルフットプリントより実際のモデルが大きければ、さらに少なくなります。クライアントが大きなバッチボディを送りうる場合は、RECOTEM_MAX_BODY_BYTES を正当なバッチが必要とする値まで下げるか、Pod の手前で同時実行数を制限するか (セキュリティ — レート制限と DoS を参照)、メモリ上限を引き上げてください。

この確保はリークではなくアリーナの再利用です。63.5 MiB のボディを繰り返すと高水位で落ち着き、その後の通常のトラフィックでメモリは OS に返ります。ただしアイドル時の値までは戻りません。したがってコンテナの上限は定常状態ではなく高水位でサイジングしてください


フィーチャーアウェア iALS のサイジング

レシピの features: ブロックは、Recotem のレシピの他の部分とは異なるスケールのコストを追加します。以下はすべて features: が存在する場合にのみ当てはまります。

ボキャブラリはインタラクション件数ではなくカタログサイズに比例する

この機能の運用上もっとも意外な性質です。エンコード後の次元は取得したフィーチャーテーブル全体から構築され、インタラクションデータに実際に現れるアイテム/ユーザーの部分集合からは構築されません。これこそが、学習に一度も現れないコールドスタートのアイテムやユーザーをサービング時にスコアリングできる理由です。その帰結として、インタラクションがそのうち 1,000 アイテムしかカバーしていない 100 万アイテムのカタログでも、残り 999,000 アイテム分のエンコード次元 — および後述するトライアルあたりの学習コスト — をそのまま支払います。それらのカラムが役に立つのは、来ないかもしれないコールドスタートリクエストに対してだけであってもです。

RECOTEM_MAX_FEATURE_DIM (デフォルト 5000、[16, 100000] にクランプ) はサイドごとのエンコード次元に上限を設けます (アイテムとユーザーは独立して検査されます)。超過するとエンコーダ状態が構築される時点で TrainingError (終了コード 4) が発生します。オペレーターがこの上限に対して使える唯一のレバーが min_frequency (レシピレベル、カラムごと) です。高カーディナリティの categorical / multi_label カラムでこれを上げてボキャブラリを縮小してください。レシピからボキャブラリをインタラクションでカバーされた行に限定する方法はありません。

注意 — min_frequency が制限するのは次元であって、それを発見するために使うメモリではありません

ボキャブラリ構築は取得したカラムのすべてのトークンを辞書に数え上げ、その後で初めて刈り込みます。multi_label の分岐は、まず全行のトークンを 1 つのリストに平坦化します。したがって高カーディナリティのカラムは、min_frequency をどれだけ積極的に設定しても一時的なカウントコストを丸ごと支払います — 数十万の異なる値を持つカラムは、刈り込み後のボキャブラリが空になる場合でもカウントに数十 MB を要します。RECOTEM_MAX_FEATURE_DIM のチェックはすべてのカラムのボキャブラリが構築されたに実行されるため、上限が拒否する実行でもこの一時的なコストは全額支払われます。min_frequency はトライアルを保護しますが、エンコーダ状態の構築は保護しません。

feature_vocabulary_pruned (WARN) はノイズではなく本物の警告です

上限をクリアするために min_frequency を上げると、次元の数値には現れないコストが発生します。刈り込まれた値は、その値を持つ行のシグナルごと消えるのです。それらの行は当該カラムについて全ゼロのブロックにエンコードされ、その軸上では互いに区別できなくなります。刈り込みによってカラムの行の 20% 以上がシグナルを失う場合、学習は次のログを出します。

feature_vocabulary_pruned  column=category min_frequency=5 distinct_values=41
  kept_values=1 rows_without_signal=40 n_rows=50

先頭の値だけが残ったカラムも行ごとに変化はします。したがって「死んで」はおらず、feature_empty_vocabulary_column のチェックは沈黙したままです。それでいて、いま切り落としたロングテールについては何も寄与しません。min_frequency を下げて次元を別の場所で稼いでください (カラムを 1 つ削る、あるいは上限を引き上げて、すでにある上限とともに指数が上昇するコストを支払う — 後述の倍加ごとの表を参照)。テールに本当にシグナルがないのであれば、そのカラムごと削るのも手です。行の 20% 未満で済む刈り込みはこのレバーの想定どおりの使い方であり、報告されません。

トライアルあたりの時間は次元より速く、メモリは 2 乗で増加し、どちらも training.parallelism と乗算される

irspack はサイドごとに密な Fᵀ F グラム行列を構成し、Cholesky 分解で解きます。2 つのコストは異なるスケールをするため、ホストをサイジングする際は分けて考える価値があります。時間はエンコード後の次元に対して超線形に増加します。そしてサイジングを誤らせるのはここです — 指数そのものが次元とともに上昇するため、範囲全体に当てはまる単一のべき乗は存在しません。既定の 5,000 を下回る範囲ではフィーチャー処理はまだトライアル時間の主要因ではなく、次元を倍にしてもコストは 2 倍未満です。5,000 以上ではグラム行列とその Cholesky が支配的になり、倍化のコストは純粋な 3 乗が示す 8 倍に近づきます。1 つのフィクスチャ、parallelism: 1、3 回の交互実行の中央値で、倍化ごとに実測:

倍化コスト含意される指数
1,251 → 2,5011.74 倍0.80
2,501 → 5,0011.85 倍0.89
5,001 → 10,0015.07 倍2.34
10,001 → 20,0017.46 倍2.90

このページの以前の版は、範囲全体を平坦な dim^2.4 と要約し、倍化のコストを 5.1〜5.8 倍として 3 乗の場合を明示的に否定していました。これは 5,000 → 10,000 の段については正しく、両端では誤りです。小さい上限を引き上げるコストを過大に、既定の上限を引き上げるコストを過小に見せます — そして 10,000 → 20,000 の段こそ、既定の上限がカタログを拒否したときにオペレーターが取る段です。5,000 から 20,000 への2 回の倍化は約 30 倍ではなく約 38 倍で見積もってください。

メモリにこの複雑さはなく、2 乗で増加し、グラム行列は float64 で dim² × 8 バイトです。ただしこれは見積もりではなく下限として扱ってください。式は 200 MB / 800 MB / 3.2 GB を与えますが、特徴量なしの同一実行に対するピーク RSS の増分の実測は 287 MB / 960 MB / 3.5 GB で、式は 10〜43% 過小評価となり、既定の上限である 5,000 で最もずれます。グラム行列が支配的ですが、エンコーダ状態・特徴量行列そのもの・ソルバの作業領域も同時に生存しています。irspack はどちらでもエラーを出さず、劣化するだけです。トライアルあたりの実測値:

エンコード次元時間メモリ
5,0000.6〜2.4 秒約 200 MB
10,0004.2〜12 秒約 800 MB
20,00043〜70 秒約 3.2 GB

時間の列が範囲なのは、トライアルが同時に適合させるインタラクションデータにも依存し、次元だけで決まらないためです。低い方の値は小さいフィクスチャ、高い方の値は 10 万行のフィクスチャによるものです。メモリはグラムの式が予測するとおり、どちらでも安定しています。サイジングは高い方の値で行ってください。 上昇する指数はこの表からも見えます — 小さいフィクスチャでは 4.2/0.6 と 43/4.2 が倍化あたり 7.0 倍と 10.2 倍です。単一の平坦なべき乗が誤った要約だった理由がこれです。

training.parallelism は Optuna の n_jobs であり、プロセスではなくプロセス内スレッドです。したがって同時実行される各トライアルが独自の密なグラム行列を構築して解きます。parallelism=4, dim=10k ではグラム行列だけでおよそ 4 × 771 MB ≈ 3 GB になり、探索がメモリに保持する他のすべてがこれに加わります。学習ホストのサイジング (あるいは parallelismRECOTEM_MAX_FEATURE_DIM の設定) はこの乗算を念頭に行ってください。

ペイロードとサービング側の RSS は次元だけでなくカタログサイズにも比例する

irspack は学習済みレコメンダー上に self.item_features (および self.user_features) を保持し、__getstate__ を定義していないため、エンコード済みのフィーチャー行列はそのままアーティファクトのペイロードにシリアライズされます。サイズはエンコード次元だけでなく n_items × nnz_per_row に比例します。試算では、100 万アイテム × エンコード 500 次元 × 1 行あたり非ゼロ 5 件 ≈ 42 MiB、100 万アイテム × 5,000 次元 × 1 行あたり非ゼロ 10 件 ≈ 80 MiB です。512 MiB という RECOTEM_MAX_PAYLOAD_BYTES のデフォルトに対して無視できませんが、それ自体が致命的というほどではありません。

RECOTEM_MAX_FEATURE_DIM が制限するのはカラムです。n_items × nnz_per_row を制限するものは何もないため、1 行あたりのエンコードが密な (多数の multi_label タグ、低い min_frequency) 非常に大きなカタログは、エンコード次元が控えめでも大きなペイロードを生成しえます。アーティファクトがロードされると、同じバイト数がサービング側の常駐メモリにも計上されます (上記の recotem serve のメモリサイジング を参照)。

コールドスタートのレイテンシと n_threads

コールドスタートのスコアリングは行列の参照ではなく反復的な共役勾配法のソルブです。実測レイテンシ (1,000 アイテム、64 コンポーネント): 単一のコールドスタートリクエストは中央値 300〜500 µs、バッチ処理はこれをユーザーあたり 8〜12 µs まで償却します — ユーザーあたり 30〜40 倍の改善であり、バルクなコールドスタートのワークロードでバッチ動詞 (:batch-recommend / :batch-recommend-related) が推奨経路である理由です。

注意 — n_threads が大きいと単一リクエストのレイテンシが悪化します

n_threads=16 では中央値 734〜857 µs、p95 は 2.0〜2.2 ms であり、n_threads が 1〜4 のほうが高速です。irspack にはここに固定のデフォルトがありません — IALSRecommender(n_threads=None) は irspack のスレッディングヘルパーを通じて $IRSPACK_NUM_THREADS_DEFAULT に解決され、なければ os.cpu_count() にフォールバックするため、実効的なデフォルトは学習ホストのコア数になります。Recotem は n_threads を設定せず、解決された値は学習時にシリアライズされたモデルに焼き込まれます — サービング時のオーバーライドはありません。単一リクエストのコールドスタートレイテンシが重要なワークロードでは、学習環境で IRSPACK_NUM_THREADS_DEFAULT を設定してください。これはサービング時ではなく学習時の判断です。


SLO

Recotem は内部的に SLO を強制しません。本番環境の推奨ベースラインターゲット:

メトリクスターゲット
推薦エンドポイント p99 レイテンシ< 50 ms (純粋なレコメンダー、メタデータ結合なし)
/v1/health p99 レイテンシ< 5 ms
可用性 (レシピごと)recotem_model_loaded{recipe} Prometheus ゲージで測定
アーティファクトホットスワップ時間ローカルまたはブロックストレージでは ≤ RECOTEM_WATCH_INTERVAL + モデルロード時間。ネットワークファイルシステムではクライアントの属性キャッシュ分が上乗せされるが、これは定数ではなく分布である。Linux の NFS クライアントは通常ファイルの属性を clamp(file_age/10, acregmin=3 秒, acregmax=60 秒) の間保持し、書き込みはその窓の任意の位置に落ちる。デフォルトマウントの NFS ReadWriteMany PVC でインターバル 10 秒のときの実測は 7 試行で 1.2 秒~54.5 秒で、代表値と呼べるものはない — 平均ではなく acregmax + RECOTEM_WATCH_INTERVAL(既定値で約 70 秒)で見積もること。同じボリュームを noac でマウントするとこの項は消える—— 4 試行で 1.9 秒~4.2 秒、ウォッチャのティックのみで押さえられる
学習から提供までのラグ学習をスケジュール; serve は ≤ RECOTEM_WATCH_INTERVAL 秒で検知。アーティファクトがネットワークファイルシステム上にある場合は上記の属性キャッシュ分が加算される
スワップ中のレプリカ間の一致保証されない。 レプリカは独立してスワップするため、最後のレプリカがスワップし終えるまで同じ user_id が 2 つの異なるモデルから応答を受け取りうる。デフォルトマウントの NFS RWX PVC 上の 3 レプリカで実測 21.8 秒。どのモデルが応答したかはレスポンスの model_version で識別できる

Prometheus メトリクスを有効化:

bash
pip install "recotem[metrics]"

RECOTEM_METRICS_ENABLED=1 を設定して /v1/metrics エンドポイントを有効化してください。

このサーバーが提供していない名前では recipe ラベルは <unknown> になります

recotem_v1_requests_totalrecotem_v1_request_latency_seconds では、レシピ名はリクエストパスに由来し、ルートのパターンはその形式 (^[A-Za-z0-9_-]{1,64}$) しか制約しません。そのままラベルにすると、ひとりの呼び出し元が際限なく時系列を作り出せてしまい、prometheus_client はそれを破棄しません。そのため、登録されていない名前はすべて <unknown> として記録されます。<> は上記のパターンの範囲外なので、実在のレシピがこの値を取ることはありません。

何が起きているかはカウンターから分かります。これらのリクエストには status="recipe_not_found" が付きます。名前そのものは recipe_not_found ログイベントに残り、こちらはカーディナリティの制限を受けません。登録済みのレシピはロードされていない場合でも自分のラベルを保持するため、recotem_v1_requests_total{recipe="my_recipe",status="unavailable"} は影響を受けません。

recipe_not_found のレートを recipe でグルーピングしている場合、そのクエリは試行された名前ごとの系列ではなく <unknown> の 1 系列を返すようになります。


ウォッチャーとレジストリのセマンティクス

ArtifactWatcher は serve プロセス内のデーモンスレッドとして実行されます:

  • RECOTEM_WATCH_INTERVAL 秒ごと (1〜30 にクランプ、±10% のジッター) にポーリングします。最大 16 の stat() 呼び出しがスレッドプール経由で並列に発行されます。各並列 stat() フューチャーはフューチャーごとのタイムアウト min(RECOTEM_WATCH_INTERVAL, 30) 秒が適用されるため、ハングしたオブジェクトストアの stat (例: S3 の TCP ブラックホール) はティック全体をブロックしません。
  • recotem serve のシャットダウン (SIGTERM) 時に、ArtifactWatcher.stop()executor.shutdown(wait=False, cancel_futures=True) を呼び出し、キューに入っているが未開始のフューチャーが即座に破棄されます。
  • 変更はアーティファクトポインターの mtime/size (ローカル FS) または ETag/VersionId (オブジェクトストア) から検知されます。マーカーが変化すると、ウォッチャーは完全なバイトを一度読み取り、sha256 を計算し、sha256 も変化した場合のみリロードします — 同じ内容のファイルに置き換えると mtime は変化しますが不要なスワップはトリガーされません。
  • レシピディレクトリは各ティックで再スキャンされます: 新しい *.yaml ファイルは recipe_discovered と即時の強制ロードをトリガーし、削除されたファイルは recipe_removed をトリガーしてエントリがレジストリから削除されます。
  • リロード中に何らかの失敗 (artifact_load_failedartifact_load_unexpected_error) が発生した場合、既存のエントリは引き続きサービスされ、last_load_error フィールドが設定されるため /v1/health は陳腐化を示しつつ推薦エンドポイントは前の正常なモデルを返し続けます。
  • last_load_error は、もはや成立しないことをウォッチャーが示せた時点で撤回され、artifact_load_error_cleared (INFO、reasonprevious_error 付き) が記録されます。撤回のきっかけは 3 つです。新しいアーティファクトのロード成功、stat 側の失敗のあとの stat 成功 (reason=marker_restored — スロットリング、IAM の一時的な不調、一時的に到達できなかったパス)、そして再読み込みしたバイト列のハッシュがすでにメモリ上にあるアーティファクトと一致した場合 (reason=bytes_unchanged) — これは以前のアーティファクトへのロールバックの見え方です。
  • 一方、意図的に撤回されないエラーが 2 つあります。パースできなくなったレシピ YAML は、再びパースできるようになるまで報告され続けます (アーティファクトは YAML について何も語りません)。また、ディスク上にまだ存在するアーティファクトに対して記録されたロード失敗は、そのアーティファクトが変わるまで残ります。同じバイト列がまだそこにある以上、何も反証されていないからです。

注意 — アラートの変更点: degraded は「現在も障害が続いている」ことを意味します

通常の 2 つの回復経路 (再学習ロールバック) は、どちらも再起動なしで /v1/health/detailsok に戻します。ランブックから「Pod を再起動して解消する」手順を削除し、シグナルが自然に解消しないことを前提に設定した長い for: も見直してください。継続する degraded は、いま現在も成立している障害です。

初期ロードの失敗

起動時にアーティファクトのロードが失敗した場合、レシピはスタブとして登録されます (loaded=falseerror=<理由>)。サーバーは起動し、/v1/healthdegraded を報告し、レシピの推薦エンドポイントは 503 を返します。部分的な障害はプロセスを再起動せずに再学習によって回復できます。

起動専用のイベントバリアント:

イベントトリガー
initial_artifact_read_failed / initial_artifact_read_errorI/O エラーまたは上限超過
initial_artifact_parse_failedマジック / バージョン / ヘッダー構造エラー
initial_artifact_hmac_failedHMAC 不一致または不明な kid
initial_artifact_deserialize_failedFQCN 許可リスト拒否またはペイロードデコードエラー
initial_artifact_hmac_skipped_dev--dev-allow-unsigned

アーティファクトの学習後にレシピが変更された場合

recotem serve は、アーティファクトのヘッダーにある recipe_hash と、それを ロードしようとしているレシピのハッシュを比較します。一致しない場合は artifact_recipe_hash_mismatch を WARNING で記録し、配信は継続します — 起動時とホットスワップの両方のロード経路で行われます。

json
{"event": "artifact_recipe_hash_mismatch", "name": "news_articles",
 "artifact_recipe_hash": "9f1c2a4b7e05", "current_recipe_hash": "3d80ba61cc17"}

いずれのダイジェストも完全な sha256 の先頭 12 文字です。

拒否ではなく警告である理由は、再学習を必要としない編集 — 名前の変更や、データが すでに満たしている水準まで cleansing のしきい値を緩めるなど — の後には、ハッシュ の相違が想定される状態だからです。拒否してしまうと、誤字の修正のために稼働中の サーバーを停止させることになります。

一方で、この警告を発生させ得ない編集もあります。ハッシュはレシピのパース後の 表現に対して取られるため、コメント、空白の変更、マッピングキーの並べ替えは いずれも同じダイジェストに正規化され、警告は出ません。警告が出るのはが 変わったときだけです。

この警告が伝えているのは、配信中のモデルは古いレシピを反映しているという ことであり、サーバーがそのモデルについて報告する内容もすべて同様です。 /v1/recipes/{name} が返すのはアーティファクト側algorithmsmetriccutoff であって、いま編集したファイルの値ではありません。稼働中のサーバーは、 そのモデルを構築した際のレシピ本体から応答するためです。この差異が見えるのは この警告だけです。一致させるには再学習してください。学習に影響しない編集であれば 無視して構いません。

ヘッダーに recipe_hash をまったく持たないアーティファクトはフェイルオープンし、 何も記録しません。このフィールドは 2.0 より前には存在しませんでした。

パースできないレシピファイル

まったく パースできないファイル (YAML 構文エラー、スキーマ違反) は、アーティファクトのロードに失敗したレシピとは異なる扱いになります。そのファイルはレシピを宣言していません — 名前もアーティファクトも、配信する対象もありません。このようなファイルは スキップ されます:

  • /v1/healthtotalloaded のカウントから除外され、代わりに独立した skipped カウントとして報告されます。このフィールドはカウントが 0 でない場合にのみ現れます。/v1/healthロード可能な レシピがすべてロードされていれば ok (HTTP 200) を返すため、1 つのファイルのタイプミスが Pod 内の他のすべてのレシピの Kubernetes readiness プローブを失敗させることはありません。
  • /v1/health/details には引き続き表示され、ファイル名の語幹をキーとして "skipped": true と、問題の ファイル名 とパースエラーを示す error 文字列を伴います。レシピ名が読み取れないため語幹は便宜的なものです — 原因にたどり着く識別子はファイル名です。
  • skipped のエントリは /v1/health/detailsdegradedしません。配信が止まったものは何もないからです。
json
{"status": "ok", "total": 3, "loaded": 3, "skipped": 1}

Pod がトラフィックを受け続けるかどうかを決めるのがこの区別です。パースできないレシピファイルskipped カウント付きの 200 になり、Pod は Service に残ります。アーティファクトをロードできない正常なレシピ503 degraded (初期ロードの失敗 を参照) になり、Pod は外されます。ログ上はどちらも失敗したレシピに見えますが、可用性の事象は後者だけです。

アラート

skipped のカウントでページング (呼び出し) しないでください — これは可用性ではなく設定品質のシグナルです。壊れたファイルが気づかれて修正されるよう警告レベルで通知し (skipped > 0 がデプロイサイクルを超えて継続した場合)、readiness は status に紐づけたままにしてください。


バックアップと障害復旧

アーティファクトは自己完結型の署名済みバイナリです — 他のバイナリアセットと同様にバックアップしてください:

  • ローカル FS: アーティファクトルート (または各レシピの output.path を含むディレクトリ) をスナップショット。versioning: append_sha は自動的に以前のバージョンを保持します。ポインターファイルが唯一の変更可能な部分です。
  • オブジェクトストア: バケットのバージョニングを有効化してください。append_sha と組み合わせることで、学習実行ごとの不変な履歴が得られます。
  • レシピ: レシピディレクトリをバージョン管理にコミットしてください。RECOTEM_SIGNING_KEYS (シークレットマネージャーに別途保管) と合わせて、レシピ + 鍵で recotem train を通じてあらゆるアーティファクトを再現できます。

ホスト障害後に recotem serve を復旧するには、レシピディレクトリと署名鍵のみが必要です。不足しているアーティファクトを再生成するために学習を再実行してください。ウォッチャーは再起動なしにそれらを検知します。


モニタリング SLI

本番アラートの高シグナルメトリクス:

シグナルソースアラート閾値 (推奨)
レシピが未ロードrecotem_model_loaded{recipe=...} == 0RECOTEM_WATCH_INTERVAL × 3 を超えて継続オンコールに page
ホットスワップ失敗rate(recotem_swap_total{result="error"}[5m]) > 0warn
再起動からのアーティファクトロード失敗recotem_artifact_load_failures_total{recipe=...} の増加warn
irspack バージョンスキューrate(recotem_artifact_load_failures_total{reason="version_skew"}[5m])warn — 学習側とサービング側が乖離している。ホットスワップ時のスキューは旧モデルの配信を継続するが、同じアーティファクトは次回の再起動でレシピを失敗させる。irspack バージョンスキュー を参照
コールドスタートのクライアントエラーrate(recotem_v1_requests_total{status=~"features_not_supported|feature_value_unusable"}[5m])warn のみ。決してページングしないこと — 継続的なレートは、クライアントが features: ブロックを持たないレシピに user_features/item_features を送っているか、標準化できない値を送っていることを意味する。対処は呼び出し側にあり、モデルは正常
アーティファクト stat 失敗 (ウォッチャーポーリング)recotem_artifact_stat_failures_total{recipe=...} の増加warn
ウォッチャーの未処理エラーrecotem_watcher_unhandled_errors_total の増加warn
predict エラー率rate(recotem_v1_requests_total{status="error"}[5m]) / rate(recotem_v1_requests_total[5m])1% で warn、10% で page
predict レイテンシhistogram_quantile(0.99, recotem_v1_request_latency_seconds_bucket)レシピごとの SLO
アクティブレシピ前回のスクレイプから recotem_active_recipes が 0 より減少warn
BigQuery Storage API フォールバックbigquery_storage_fallback ログイベント — メトリクスではありません。後述の注記を参照warn
レシピディレクトリスキャン失敗rate(recotem_recipes_dir_scan_failures_total[5m]) > 0warn

根本原因のコンテキストのために、構造化ログイベント artifact_load_failedartifact_disappearedrecipe_not_loaded_at_startupauth_invalid_key と組み合わせてください。

注意 — recotem_bigquery_storage_fallback_total はスクレイプできません。アラートルールを組まないでください

このカウンター自体はコード中に存在しますが、インクリメントするのはデータソースだけであり、それが動くのは recotem train — HTTP サーバーを持たないバッチプロセスです。/v1/metrics を提供するのは recotem serve であり、そちらはデータを取得しません。したがってこの系列はスクレイプ可能なプロセスでは決して値を持たず、これに対して書いたルールは永久にゼロのままです。「フォールバックが起きていない」状態と区別がつきません。

代わりにログ基盤で bigquery_storage_fallback ログイベントにアラートしてください。BigQuery Storage Read API フォールバック を参照してください。


アップグレード

Recotem は semver に従います。メジャーバージョン内 (2.x):

  • レシピは有効のまま残ります。レシピローダーは後方互換性があります。
  • アーティファクトフォーマットバージョンは 1 です。古いリーダーは新しいフォーマットを unsupported format version で拒否します。フォーマットが変更された場合、ライターをアップグレードした後に再学習してください。リーダーは先にアップグレードできます。
  • FQCN 許可リストはリリースごとに凍結されます。変更は各リリースの GitHub Release ノート に記載されます。アーティファクトが削除されたクラスをエンコードしている場合は再学習してください。
  • 各リリースのオペレーター向けアップグレード手順は、製品リポジトリの docs/upgrading.md にあります。 マイナーバージョンをまたぐ前に必ず参照してください。2.0.0 → 2.1.0 の節では、IALS の再学習、Azure URI の変更、署名鍵の終了コード変更を扱っています。
  • irspack のシリアライゼーション形式は上記のいずれにもカバーされません。 irspack は自身のマイナーバージョン間で形式の安定性を保証しないため、irspack のマイナーをまたぐ Recotem のアップグレードは既存のアーティファクトを拒否することがあります — アルゴリズムごと、遷移ごとに判定されます。この軸は双方向です。サービングを先行させる段階的な移行はできず、ロールバックもできません。許可リストのルール、拒否されるアルゴリズム、アップグレード手順については irspack バージョンスキュー を参照してください。
  • scikit-learn はさらに別の、ガードのない軸です。 TruncatedSVD のアーティファクトは sklearn の推定器を埋め込んでおり、sklearn は自身のマイナーバージョンをまたぐデシリアライズの正しさを保証しません。Recotem は scikit-learn>=1.8,<1.10 を範囲でピン留めして窓を狭めていますが、閉じてはいません (範囲内の 2 つのインストールが異なる可能性があります)。ランタイムのチェックもありません。

serve フリートのゼロダウンタイムアップグレードには、新旧両方の署名 kid を設定した新しい Pod をデプロイし (ローテーションスタイル)、新しい Pod が正常になったら古い Pod をドレインしてください (RECOTEM_DRAIN_SECONDS に依存)。

この手順は新しい Pod が既存のアーティファクトをロードできることを前提としています

署名鍵のローテーションでは成り立ちますが、irspack のマイナーをまたぐ場合は成り立ちません。Recotem 2.1.0 は irspack を 0.4.x から 0.5.x へ移行します。irspack 0.5.x を実行する新しい Pod は、0.4.x で学習された IALS アーティファクトに対して決して正常になりません — デシリアライズ前に拒否され、レシピは loaded: false のままとなり、/v1/health は 503 を返すため、どの新規 Pod も readiness プローブを通過しません。

該当するレシピを先に新しい irspack バージョンで再学習するか、学習側とサービング側を同時にアップグレードして再学習の期間を受け入れてください。irspack を移行するアップグレードの前には必ず irspack バージョンスキュー を確認してください。


best_score の読み方とモデルの選び方

best_score が意味するもの、意味しないもの

best_scorerecotem inspecttrain_done ログ行の目玉となる数値であり、ヘッダーの中でもっとも読み違えられるフィールドです。

意味するものは、Recotem 自身の内部バリデーション分割における勝者トライアルのスコアです。レシピの metriccutoff で、training.split によって同じインタラクションテーブルから取り分けられた data_stats.n_heldout_interactions 件のインタラクションに対して、同じ学習済みアイテム集合をランキングして計算したものです。

意味しないものは、あなたの評価においてモデルがどれだけのスコアを出すかの推定値です。あなたのタスクは、どのインタラクションをホールドアウトするか、どのユーザーをスコアリングするか、どのアイテムを候補にするか、どの指標をどのカットオフで使うか、のうち少なくとも 1 つでほぼ確実に異なります。これらは別々の測定であり、best_score はあなたの測定の近似ではありません。

注意 — ずれは小さくなく、符号も一定ではありません

自前のホールドアウトに対して測定したところ、best_score は同じアーティファクトについて手作りの ndcg@10 を過大にも過小にも表しました。両方向に、しかも go/no-go の判断を反転させるだけの幅で、です。ここで補正係数を示さないのは、ずれの大きさが Recotem の性質ではなく、あなたのタスクが内部のランダム分割からどれだけ離れているかの性質だからです。

心に留めておく価値のある帰結が 2 つあります。

  • best_score は、どのアルゴリズムが出荷されるかを決める基準でもあります。 探索はこれを最大化するため、あなたのタスクの順位づけと食い違ったとき、その食い違いは報告されるだけでなく実際に採用されます。algorithms を広げても解決しません。あなたのタスクなら選ばなかった勝者を選ぶ機会が増えるだけです。選択が重要な場面では、候補を個別に学習し、自分のホールドアウトで採点してください。
  • best_score はコールドスタートをまったく測っていません。 目的関数は学習済み行列のホールドアウト行から作られるため、どのトライアルもコールドアイテム/コールドユーザーのリクエストを発行しません。features: ブロックがある場合、コールドスタートの動詞が依存するフィーチャー→埋め込み写像を支配するパラメータ lambda_item_feature / lambda_user_feature は、それにほぼ無関心な証拠でチューニングされることになります。1 つのレシピと 1 つのデータセットで 4 回測定したところ、コールドアイテムの precision@10 は桁で変動する一方、best_score の変動は 2% 未満でした。コールドスタート経路に依存するなら、別途検証してください。

同じレシピの実行間best_score を比較することも、見た目ほど強い比較ではありません。レシピリファレンス — 再現性 を参照してください。

小規模データセットでのモデル選択

これはエラーではなく、明らかに退化したモデルより見つけにくい問題です。出荷されるモデルはパーソナライズされており、多様なアイテムを返し、best_score も普通の数字に見えます。

探索は、ホールドアウトされたバリデーションのインタラクションで各トライアルを採点して勝者を選びます。それが何件だったかは、アーティファクトヘッダーの data_stats.n_heldout_interactions に書かれています。 この数が小さいと、そこから生まれる順位づけはほぼノイズであり、探索で勝ったアルゴリズムが実際のユーザーにとって最良である保証はなくなります。

25 ユーザー・118 アイテムのテナント (heldout_ratio: 0.2、4 アルゴリズム、n_trials: 20) で、探索が一度も見ていないホールドアウトに対して採点した結果:

モデル探索スコア (ndcg@10)実際の recall@10
探索が出荷したもの0.26180.0600
IALS 単独0.27780.2867
CosineKNN 単独0.24590.3600
RP3beta 単独 — 探索では最下位0.22780.3600
人気度ベースライン0.0867
決定的ランダム0.0933

出荷されたモデルは実際のホールドアウトデータで人気度ベースラインを下回り、探索が最下位にしたアルゴリズムが最良でした。実行は終了コード 0、/v1/healthok:recommend は 200 を返しました。

2 つの要因が重なっています。

  • バリデーションセットは 50 インタラクションでした。4 つのアルゴリズムを区別するには足りません。
  • n_trialsアルゴリズム間で均等に分割されるグローバル予算なので、n_trials: 20 を 4 アルゴリズムで割ると各 5 回です。同じレシピを algorithms: [IALS] で 20 回すべて使ったところ、0.0600 ではなく 0.2867 に達しました。

取るべき対応:

  1. best_score を信じる前に n_heldout_interactions を読む。 目安として、同梱のサンプルはそれぞれ 12 件、60 件、803 件をホールドアウトします。最初の 2 つはツールの学習用としては十分ですが、アルゴリズムの選択根拠にはなりません。

  2. 人気度 30 行程度のアイテム間コサイン kNN の両方と比較する。ただしその前に、この実行でどのベースラインが生きているかを測る。 何かを比較する前に 3 つの数値を出してください。

    • 学習データで人気上位 10 件のうち、ホールドアウトに 1 度でも現れるものが何件あるか — 0/10 なら人気度との比較は死んでいます。回転の速いカタログ (ニュース、フィード、セール、求人) では通常この状態です。
    • kNN の ndcg を決定的ランダムランキングと比べた値 — ランダムに勝てない kNN も基準にはなりません。カタログが利用者数より桁違いに大きい場合は通常この状態です。
    • 同じランダムランキングに対するモデル自身のマージンを、比ではなく判断として読む — ユーザーごとの差をブートストラップして 95% 区間を読むか、両者が異なるユーザーについて符号検定を行ってください。

    そのうえで、自身のチェックを生き延びたすべてのベースラインに勝つこと、かつ区間がゼロを含まないマージンでランダムランキングに勝つことを要求してください。両方のベースラインが同時に死ぬこともあり、そのときは内容を持つのはランダムとの比較だけです。死んだベースラインに対する大差は合格ではなく、定義できない比です。

    kNN の全体像: ユーザー×アイテム行列を二値化し、列を正規化し、対角をゼロにした Sᵢⱼ = cos(i, j) を取り、各アイテムの上位約 200 近傍を残し、ユーザーを X[u] @ S でスコアリングし、そのユーザーがすでに接触したアイテムを除外します。

  3. 予算が小さいときは algorithms を絞る。 あるいは、各アルゴリズムが意味のある回数を得られるよう n_trials を増やしてください。

  4. 小さなモデルを多数持つより、1 つのモデルにまとめる。 小規模顧客ごとにレシピを作るパターンがこの失敗を生みます。許容できる範囲で小さなテナントを 1 つのモデルにプールすれば、探索に扱えるだけの材料が渡ります。

Recotem はこれについて自動的に警告しません。上のテナントを検知できるしきい値は同梱チュートリアルも検知してしまうため、この数値は判定されずに報告されるだけです。判断はあなたのものです。


トラブルシューティング

recotem serve が起動するがレシピが loaded: false

bash
curl -H "X-API-Key: $RECOTEM_API_PLAINTEXT" \
  http://localhost:8080/v1/health/details | jq '.recipes'
json
{"my_recipe": {"loaded": false, "last_load_error": "signature mismatch"}}

原因と修正:

エラー原因修正
signature mismatchアーティファクトが RECOTEM_SIGNING_KEYS にない鍵で署名されている学習時に使用した署名 kid を追加する
unknown kid: prod-oldアーティファクト内の kid がサーバーの鍵リストにないその kid を追加するか、既知の kid で再学習する
magic bytes mismatch破損または不完全なアーティファクト再学習する
payload exceeds max bytesペイロードが RECOTEM_MAX_PAYLOAD_BYTES (デフォルト 512 MiB) またはアーティファクトが RECOTEM_MAX_ARTIFACT_BYTES (デフォルト 2 GiB) を超えている該当する上限を増やすかモデルサイズを削減する
header JSON too large不正なアーティファクト再学習する
irspack version skew: ...学習側とサービング側の irspack の major.minor 遷移について、そのアーティファクトのアルゴリズムが互換性検証済みでない (例: 0.4 ↔ 0.5 をまたぐ IALS アーティファクト)サービングホストの irspack バージョンでレシピを再学習する。irspack バージョンスキュー を参照
feature version check failed: ...アーティファクトの features.version が欠落、整数でない、またはこのビルドが実装するエンコーダ状態のバージョンでない (reason は feature_version)サービング側のビルドの Recotem バージョンで再学習する。セキュリティ — フィーチャーエンコーダのバージョンゲート を参照
feature state check failed: ...features ヘッダーがペイロード内のエンコーダ状態と食い違っている (reason は feature_state) — 不正に構築された、または部分的に改竄されたアーティファクト再学習する。セキュリティ — フィーチャーのヘッダーとペイロードの突き合わせ を参照

irspack バージョンスキュー

irspack はマイナーリリース間でシリアライゼーション形式の安定性を保証しません。Recotem はすべてのアーティファクトヘッダーに学習時の irspack_version を記録し、ペイロードをデシリアライズするに実行中の irspack と照合します。

このルールは拒否リストではなく許可リストです。

  • major.minor が同一 → 常にロードされます。パッチの差 (0.5.00.5.3) は許容され、検証済みテーブルは参照されません。
  • major.minor が異なる → アーティファクトの best_class その正確な遷移の両方が Recotem の互換性検証済みテーブルに存在する場合にのみロードされます。存在しないものはすべて拒否されます。

0.4 ↔ 0.5 で双方向に互換性が検証済み: CosineKNNRecommenderTopPopRecommenderRP3betaRecommenderDenseSLIMRecommenderTruncatedSVDRecommender。ある行がテーブルに載るのは、一方のバージョンで学習したアーティファクトを他方でロードし — irspack だけを変数として — 推薦スコアがビット単位で一致することを確認した場合のみです。

0.4 ↔ 0.5 で拒否されるもの:

best_class理由
IALSRecommender既知の破壊的変更、双方向。0.5.0 でフィーチャーアウェア iALS が追加され、IALSModelConfig のシリアライズされた状態が 7 要素タプルから 10 要素タプルに拡大した。__setstate__ はアリティが厳密なバインディング。
BPRFMRecommender未検証bprfm エクストラの提供開始により学習可能になったため相互運用の実験は可能になったが、まだ実施されていない (irspack 0.4.x 環境が必要で、さらに BPRFM のペイロードは LightFM オブジェクトを内包するため、このテーブルが扱っていない 2 つ目のバージョン軸が加わる)。テーブルにないことは未証明を意味し、既知の破損を意味しない。
best_class が欠落または文字列でないフェイルクローズ: 自身のアルゴリズムを名乗れないヘッダーはテーブルに一致しえない。

拒否された場合、レシピは reason version_skewloaded: false としてマークされ、次のエラーが表示されます (レシピ news、0.4.2 で学習した IALS アーティファクトを 0.5.0 で配信):

irspack version skew: retrain recipe 'news' with irspack 0.5.0 — IALSRecommender
0.4.2→0.5.0 is not verified compatible. Recotem allows only (algorithm, irspack
transition) pairs it has empirically verified load correctly; unverified is not
proof of breakage — the one known break is IALSRecommender at irspack 0.5.0,
whose serialized model state changed shape. Retrain and redeploy, or if you know
this artifact is unaffected set RECOTEM_ALLOW_IRSPACK_VERSION_SKEW=1 to
downgrade this to a warning.

対処法が意図的に前置きされています。サービングは保存する last_load_error を 200 文字に切り詰めてから /v1/health/detailserror として公開するため、修正方法、レシピ名、アルゴリズム、両方のバージョンをすべてその予算内に収める必要があります。全文はログには残ります。

将来の irspack のマイナーはすべて拒否から始まります。 このガードは検証済みのペアのテーブルを参照するため、後の 0.5 → 0.6 のアップグレードでは、上記の 5 つを含むすべてのアルゴリズムのアーティファクトが、誰かがその遷移を検証して行を追加するまで拒否されます。これは意図的であり、テストされていないものを拒否するという安全側のデフォルトを保ちます。

フェイルオープンのケース。 irspack_version を持たないヘッダー (2.0 以前のアーティファクト)、またはどちらか一方のバージョンがパースできない場合は、警告をログに記録してロードします。検証できないバージョンは非互換性の証拠ではなく、デシリアライザが最後の砦として残るからです。非対称性に注意してください — 使用できないバージョンはフェイルオープン、実際のスキューにおける使用できない best_class はフェイルクローズです。

このチェックが存在する理由。 これがなければ、障害は irspack の C++ 層から素の TypeError: __setstate__(): incompatible function arguments として現れ、レシピ名も対処法も示しません。

アップグレード手順。 学習側とサービング側を同時にアップグレードし、その後すべての IALS および BPRFM のレシピを再学習してください。破壊的変更は双方向であるため、サービングを先に移す段階的なアップグレードはできず、アーティファクトを 0.5.x で再学習した後にサービングを 0.4.x に戻すこともできません。アーティファクトのインプレース移行はありません。欠けているフィールドは内部の C++ の状態であり、再学習だけが正しく生成できます。

影響範囲 — 今は劣化、後で停止

サービングはクラッシュしません。影響を受けたレシピが失敗としてマークされ、他のすべてのレシピは配信を続けます。ホットスワップの間は以前ロードされたモデルがメモリに残るため (ロードエラーは loaded フラグを消さずにエントリに注記されます)、稼働中のフリートに投入されたスキューのあるアーティファクトは、障害ではなく「引き続き旧モデルを配信」に劣化し、件数ベースの /v1/health200 のままです。エラー文字列も走査する /v1/health/details だけが degraded を報告します。

この耐性はプロセス単位であり、再起動を生き延びません。 起動時にはフォールバックできる既ロードのモデルが存在しません。レシピは loaded: false のスタブとして登録され、/v1/health503 を返し、/v1/health を指すすべての readiness / liveness プローブが失敗します。つまりスキューのあるアーティファクトは稼働中のフリートでは無害に居座り、次の再起動、ノードのドレイン、スケールアップで — それを持ち込んだデプロイからかなり後になって — Pod を落とします。

同梱の Helm チャート (replicaCount: 2strategy: ブロックなし) では、Kubernetes のローリングアップデートのデフォルトにより maxUnavailable = floor(0.25 × 2) = 0 となるため、ローリングアップデートは即時の障害ではなく、旧 Pod が配信を続けたまま停滞します — 新しい Pod は決して ready にならず、場所を空けるために旧 Pod を落とすこともできません。危険なのは停滞したロールアウトではなく、劣化状態が次の不本意な再起動で終わることです。チャートは pdb.enabled: false も同梱しているため、ノードのドレインが両方のレプリカを同時に落とすことがあります。

エスケープハッチ。 RECOTEM_ALLOW_IRSPACK_VERSION_SKEW=1 は拒否を irspack_version_skew_allowed 警告に格下げし、ペイロードをデシリアライザに到達させます。アーティファクトが影響を受けないと分かっている場合にのみ使用してください — 既知の破損ではなく単に未検証であるアルゴリズムに対してがもっとも正当化しやすい使い方です。互換性のないペイロードがロード可能になるわけではありません。本当に壊れているアーティファクトは、このガードが置き換えようとしている素の TypeError で失敗します。

学習側とサービング側が乖離したフリートを検知するには recotem_artifact_load_failures_total{reason="version_skew"} を監視してください。

このガードがカバーしない別の軸: scikit-learn。 TruncatedSVDRecommender は sklearn の推定器をペイロードに埋め込みます。sklearn は自身のマイナーバージョンをまたぐデシリアライズが「コードの破損や無効な結果につながる可能性がある」と警告します (InconsistentVersionWarning)。Recotem は scikit-learn>=1.8,<1.10 を範囲でピン留めしてこれを抑えていますが、範囲の指定は軸を狭めるだけで閉じません — 範囲内の 2 つのインストールが異なる可能性があり、irspack のガードは sklearn のバージョンを一切検査しません。TruncatedSVD のアーティファクトをビット単位で再現する必要がある場合は、sklearn を厳密にピン留めするか、学習側とサービング側を同じロックファイルからビルドしてください。

recotem train が終了コード 3 (DataSourceError) で終了する

BigQuery の場合: gcloud auth application-default print-access-token を実行して ADC が機能していることを確認してください。JSON の標準エラー行で正確なエラーを確認してください。

bash
recotem train recipe.yaml 2>&1 | grep '"event":"train_error"' | jq .

BigQuery Storage Read API フォールバック

サービスアカウントが bigquery.readSessions.create を持っていない場合、BigQuery ソースは bigquery_storage_fallback 警告をログ出力し、より遅い REST API にフォールバックします。google-cloud-bigquery-storage がそもそもインストールされていない場合も、reason を変えて同じイベントが出力されます。ログ基盤でこのログイベントを監視してください。フォールバックが継続する場合は IAM 権限またはエクストラの欠落を意味します。これはログのみのシグナルです。recotem_bigquery_storage_fallback_total はスクレイプできません (モニタリング SLI を参照)。権限を付与するには:

bash
gcloud projects add-iam-policy-binding <PROJECT_ID> \
  --member="serviceAccount:<SA>@<PROJECT_ID>.iam.gserviceaccount.com" \
  --role="roles/bigquery.readSessionUser"

フォールバックを無効化してエラーを表面化させるには、RECOTEM_BQ_REQUIRE_STORAGE_API=1 を設定してください。

recotem train が min_data_violation で終了コード 4 で終了する

クレンジング後のデータセットが閾値を下回りました。JSON エラー行に観測されたカウントが含まれます。

json
{"event": "train_error", "code": "min_data_violation", "n_rows": 842, "min_rows": 1000, ...}

レシピの cleansing.min_rows を下げるか、ソースからの行数が減った原因を調査してください。

recotem train が zero_score で終了コード 4 で終了する

すべての Optuna トライアルのスコアが 0.0 でした。一般的な原因:

  • 分割によってホールドアウトのテストセットが空になった。randomtime_user ではホールドアウトが ユーザーごとに切り捨てられ、1 / heldout_ratio 個未満の異なりアイテムしか持たないユーザーは 何も寄与しません。したがってスキームの変更やユーザー数の増加では解決しません。split.heldout_ratio を上げてください (エラーメッセージに有効な最小値が示されます)。あるいは 1 ユーザーあたりの履歴を 深くするか、深いユーザーが検証ユーザーとして抽出されていない場合は split.test_user_ratio を上げてください。
  • クレンジング後のデータのアイテム数がカットオフに対して少なすぎる。training.cutoff を下げてください。

recotem train が feature_axis_error で終了コード 4 で終了する

features: のいずれかのサイドのフィーチャーテーブルが、インタラクションデータと ID の重なりをまったく持ちません — 1 件も一致しませんでした。ソース側で ID カラムの型が変わると、それまで成功していた実行がこれで中断されるため、一目で認識できるようにしておく価値があります。メッセージは両側から ID をサンプリングするため、通常はそれだけで原因が分かります。

features.item: none of the 1200 item ids in the interaction data were found in
the feature table's 'item_id' column, so every item would encode to the bias
column alone ... feature-table ids look like ['1.0', '2.0', '3.0']; interaction
ids look like ['1', '2', '3'].

これが警告ではなく致命的である理由は、そうしなければ障害がサイレントになるからです。すべてのエンティティがバイアスカラムのみにエンコードされるため、学習は最後まで実行され、実体は通常の iALS でありながらヘッダーが features を宣言するアーティファクトに署名してしまいます。モデルは配信され、スコアは悪化し、その理由はログのどこにも残りません。

発生原因はほぼ次の 2 つに集約されます。

  • ID の dtype 不一致 — 上記のサンプルが示すケースです。整数の ID カラムに空セルが 1 つあるだけで pandas は float64 と推論するため、インタラクション側が "1" を持つ一方で 11.0 として読み込まれます。データを整形するのではなくソース側で型を固定してください。csv のフィーチャーテーブルなら dtype: {item_id: str} を追加します。dtype は csv 専用です — bigquery / sql ではクエリ側でキャストし (CAST(item_id AS STRING))、parquet ではファイルのスキーマで型を修正してください。
  • 存在はするが誤った id_column — 存在はするがエンティティ ID を保持しないカラムは、フェッチ時の存在チェックを通過し、ここで初めて失敗します。features.<side>.id_columnschema.item_column / schema.user_column と同じ ID 空間を指しているか確認してください。

Recotem は意図的に ID カラムを自動変換しません。フレームが取得された時点で pandas はすでに float64 と推論しており、元のテキストは復元できません — 1.0 と読めるカラムは、ID が文字どおり "1.0" であるカラムと区別できません。したがって整数値の float を int に戻す整形は、その形式を正当に使っているカタログの ID を黙って書き換えることになり、検出可能な失敗を静かな破損と引き換えにしてしまいます。また誤った id_column のケースはまったく捕捉できません。

ヒント — 中断するのは一致がゼロの場合のみです

部分的なカバレッジは正当かつ想定内です。フィーチャーテーブルに存在しない ID はバイアスのみにエンコードされ、そのエンティティに限り通常の iALS に劣化します。これはコールドスタートのスコアリングを可能にしているのと同じ仕組みです。低カバレッジの警告しきい値は意図的に存在しません — dtype や id_column の誤りはカラム全体の性質であり、必ずちょうど 0% になるため、ゼロより上のしきい値は正しい設定でも発火してしまいます。カバレッジを追跡したい場合は feature_axis_coverage イベント (sidematchedtotal) に対して自分でアラートを設定してください。

recotem train が feature_table_error で終了コード 4 で終了する

片側の宣言済みフィーチャーカラムがすべて何もエンコードしなかったため、その側が暗黙のバイアスカラムだけに退化しました。実体は通常の iALS でありながらフィーチャーを謳うアーティファクトに署名するのを避けるため、学習は拒否します。

到達経路は 3 つあり、メッセージがどれかを名指しします。

  • min_frequency がすべてのトークンを刈り込んだ。 RECOTEM_MAX_FEATURE_DIM に収めるために引き上げた後の典型的な原因です。値を下げるか、2 つ目のカラムを宣言してそちらでその側を生かしてください (min_frequency を参照)。
  • 単独の numerical カラムの分散がゼロ。 全行が同じ値を持つため、標準化してもエンコードするものが残りません。
  • 宣言したカラムについてフィーチャーテーブルの値がすべて null。

注意 — recotem validate はこれを予測できません

ボキャブラリはテーブルを読んで初めて分かるため、3 つのケースすべてでバリデーションは終了コード 0 で通ります。兄弟の feature_axis_error (ID 一致ゼロ) は別の失敗です。上の項目を参照してください。

レシピファイルはあるのにエンドポイントが 404 になり /v1/health も数えない

ファイルの拡張子を確認してください。--recipes <dir> が列挙するのは、そのディレクトリ直下の *.yaml ファイルだけです。*.yml ファイルは Recotem にとってレシピファイルではありません。ロードされず、skipped にも計上されず (この件数はパースに失敗した *.yaml のためのものです)、ログにも何も出ません。ローダーがそもそも認識しないからです。したがって .yml しか置かれていないディレクトリは、空のディレクトリとまったく同じに見えます。

json
{"status": "ok", "total": 0, "loaded": 0}

そしてそのレシピのすべての動詞が {"code": "RECIPE_NOT_FOUND"} を伴う 404 を返します。ファイル名を .yaml に変更してください。

サブディレクトリ内のレシピ (列挙は再帰的ではありません) と、<dir> の外に解決されるシンボリックリンクにも同じことが当てはまります。ただし拒否されたシンボリックリンクは報告されますレシピリファレンス — レシピディレクトリのロード を参照してください。

推薦エンドポイントで 401

  • X-API-Key ヘッダーの先頭または末尾の空白は鍵の一部として扱われ、一致しません。クライアント側でトリムしてください。
  • RECOTEM_API_KEYS のハッシュが、送信しているプレーンテキストに対して recotem keygen --type api で生成されたことを確認してください。ワイヤープレフィックスは sha256: ですが、ダイジェストは scrypt です — 単純な sha256(plaintext) では一致しません。

/v1/recipes/{name}:recommend で 503 (および関連動詞)

レシピが不健全です (loaded: false)。エラーは /v1/health/details を確認してください。通常は署名の不一致または破損したアーティファクトです。

/v1/recipes/{name}:recommend で 404 UNKNOWN_USER

リクエストの user_id が学習データに存在しませんでした。これは新規ユーザーの場合に期待される動作です。アプリケーションレイヤーで処理してください (例: 人気度ベースのレコメンデーションにフォールバックする)。features: ブロックで学習したモデルであれば、user_features を渡すことで代わりにそのユーザーへの実際の推薦が返ります — サービング API — フィーチャーアウェアなコールドスタート を参照してください。

このステータスは 2 つの異なるコードで共有されます。

  • UNKNOWN_SEED_ITEMS — 渡された seed_items のいずれも学習済みモデルに知られていません。通常はクライアント側のデータの問題です。
  • NO_CANDIDATES — 少なくとも 1 件のシードは既知でしたが、ランカーが内部のフィルタリング後に候補を 1 件も残しませんでした。通常はクライアントの誤りではなくデータ分布の問題です。この動詞のすべての分岐 (2 つのフィーチャーアウェアなコールドスタート分岐を含む) が同じ形で送出するため、どの経路が処理したかに関わらず空の結果は同一に報告されます。

/v1/recipes/{name} のいずれかの動詞で 422

ハンドラーが実行される前にリクエストのバリデーションが失敗しました。ボディは {"detail": "Request validation failed", "code": "VALIDATION_ERROR", "errors": [...]} で、リクエストは recotem_v1_requests_totalstatus="validation_error" としてカウントされます。

コールドスタートのフィールドでは、キー数・キー長・値の型・値の長さの違反もこの形で現れます — サービング API — コールドスタートフィールドの長さとサイズの上限 を参照してください。

バッチエンドポイントは 1 回の呼び出しで最大 256 件のリクエストを受け付け、要素ごとの status を返すため、1 件の不正な入力がバッチ全体を失敗させることはありません。HTTP レスポンスはいずれかの要素が成功すれば 200 です (失敗した要素は code フィールド付きの status: "error" を持ちます)。HTTP 503 は、レシピ自体が利用不可で 1 件も処理できない場合のために予約されています。

クライアント側のデータの問題とモデルの問題を切り分けるには、code ごとに recotem_v1_batch_element_errors_total を監視してください。

ウォッチャーが新しいアーティファクトを検知しない

  • RECOTEM_WATCH_INTERVAL を確認してください。デフォルトは 5 秒です。
  • オブジェクトストアの場合、serve プロセスの IAM ロールがアーティファクトバケットに対する GetObject (S3) または storage.objects.get (GCS) を持っていることを確認してください。
  • アーティファクトパスに対して recotem inspect を実行し、それが有効でサーバーが知っている kid で署名されていることを確認してください。recotem inspect はローカルパスと fsspec URI の両方を受け付けます (例: s3://bucket/key.recotem)。

ログのリダクション

すべてのログイベントは出力前にリダクションプロセッサーによって処理されます。期待していた値があるべきログ行で [REDACTED] が見える場合、フィールド名がリダクションパターンに一致しています。これは意図的です — 詳細はセキュリティドキュメントを参照してください。