個人開発でデータパイプラインを運用していたら、いつの間にかジョブが111個に膨れ上がっていました。新機能を追加するたびに新しいジョブを作り、不要になっても消さず、似たようなジョブが乱立します。
「増やすのは簡単、減らすのは怖い」——このバイアスに正面から向き合い、111ジョブを約50に削減する設計を行った記録です。
問題:ジョブ数の指数関数的な増加
なぜジョブは増え続けるのか
1Month 1: 10ジョブ — 管理は楽勝2Month 3: 30ジョブ — まだ全体を把握できる3Month 6: 60ジョブ — 「あのジョブ何だっけ?」が増える4Month 9: 90ジョブ — 依存関係が把握不能5Month 12: 111ジョブ — 管理基盤の見直しが必要に増加の原因パターンを整理します。
| パターン | 例 | 発生頻度 |
|---|---|---|
| 機能追加のたびに新ジョブ | 「通知を追加しよう」→ 通知専用ジョブ新設 | 高 |
| 試行錯誤の残骸 | 「実験用に作ったジョブ」が本番に残る | 中 |
| 粒度の不統一 | 1つのジョブでやるべき処理を3つに分割 | 中 |
| 恐怖による温存 | 「消して壊れたら怖い」で放置 | 高 |
111ジョブの運用コスト
- 監視: 111個のログを毎朝チェックするのは不可能です
- 障害対応: 「どのジョブが失敗した?」の切り分けに時間がかかります
- リソース: macOS launchdで111プロセスの同時起動は現実的ではありません
- 認知負荷: 全体像を把握できる人間がいません(自分すら含めて)
削減の判断基準:4つの問い
ジョブを1つずつ以下の4つの問いにかけます。
問い1: このジョブの出力を、誰が(何が)消費しているか?
1ジョブA → output_a.parquet → ジョブBが読む → OK(消費者がいる)2ジョブC → output_c.csv → 誰も読んでいない → 削除候補出力の消費者がいないジョブは、どんなに精巧でも無価値です。これが最も強力なフィルターになります。
問い2: 別のジョブと入出力が重複していないか?
1ジョブD: APIからデータ取得 → 加工 → 保存2ジョブE: 同じAPIからデータ取得 → 別の加工 → 保存入力源が同じジョブは統合候補です。取得部分を共通化して、加工だけを分岐させます。
問い3: 実行頻度は適切か?
1ジョブF: 毎時実行 — でも入力データは1日1回しか更新されない2→ 日次に変更(23回/日の無駄な実行を削減)過剰な頻度のジョブは、統合ではなく頻度調整で負荷を削減できます。
問い4: 独立したジョブである必要があるか?
1ジョブG: データ取得(5秒で完了)2ジョブH: データ加工(ジョブGの出力を使う、3秒で完了)3ジョブI: レポート生成(ジョブHの出力を使う、2秒で完了)3つで合計10秒です。依存関係が線形で、間に他のジョブが割り込む必要がないなら、1つのジョブにまとめて問題ありません。
統合の4パターン
パターン1: 直列統合
1Before: ジョブA → ジョブB → ジョブC(線形依存)2After: ジョブABC(1つのジョブ内で順次実行)適用条件: 依存が線形、間に他ジョブが割り込まない、合計実行時間が短い
パターン2: 入力源の共通化
1Before: ジョブD(API取得→加工X), ジョブE(API取得→加工Y)2After: ジョブDE(API取得→加工X+加工Y)適用条件: 同じ入力源を使うジョブ同士
パターン3: スケジュール統合
1Before: ジョブF(毎時), ジョブG(毎時), ジョブH(毎時) — 全部同じ時刻に実行2After: 毎時バッチ(F→G→Hを順次実行)適用条件: 同じスケジュールで動くジョブ群
パターン4: 削除(統合ではなく除去)
1Before: ジョブI — 出力を誰も消費していない2After: (削除)適用条件: 問い1で消費者がいないジョブ
実際の削減プロセス
Step 1: 全ジョブの棚卸し
まず111ジョブの一覧を作成し、以下を記録します。
1| ジョブ名 | 実行頻度 | 入力 | 出力 | 消費者 | 最終実行 | 判定 |2|---------|---------|------|------|--------|---------|------|3| fetch_A | 毎時 | API_A | a.parquet | agg_A | 今日 | 維持 |4| fetch_B | 毎時 | API_A | b.parquet | なし | 30日前 | 削除 |5| agg_A | 日次 | a.parquet | summary.csv | report | 今日 | 統合候補 |Step 2: 依存グラフの可視化
1fetch_A ──→ process_A ──→ agg_A ──→ report_A2fetch_B ──→ (誰も使っていない)3fetch_C ──→ process_C ──→ agg_C ──→ report_Aグラフにすると「孤島」(どこにもつながっていないジョブ)と「直列チェーン」(統合候補)が一目でわかります。
Step 3: 段階的な実施
一気に削減すると事故のリスクが高いです。以下の順序で進めます。
- 孤島の削除(リスク: 最低)— 消費者がいないジョブを無効化
- 30日間の無効化期間 — 削除ではなく無効化。問題が出たら即時復旧
- 直列チェーンの統合(リスク: 低)— 出力形式は変えず、実行を1ジョブにまとめる
- 入力源の共通化(リスク: 中)— API呼び出しの削減、エラーハンドリングの統合
- 頻度の調整(リスク: 低)— 過剰な実行頻度の是正
Step 4: 削減前後の比較
1Before: 111ジョブ2 - 孤島(消費者なし): 18ジョブ → 削除3 - 直列チェーン統合: 30ジョブ → 12ジョブに4 - 入力源共通化: 16ジョブ → 8ジョブに5 - 頻度過剰: 7ジョブ → 頻度調整のみ(数は変わらず)6
7After: 約55ジョブ(50%削減)「減らす勇気」をどう持つか
心理的バリア
「このジョブを消して、何か壊れたらどうしよう」——これが最大の障壁です。
対策: 削除ではなく「無効化 → 待機 → 削除」
1Day 0: ジョブを無効化(設定でOFF、コードは残す)2Day 30: 30日間問題なし → コードを削除この「30日ルール」があるだけで、心理的ハードルは大幅に下がります。万一問題が出ても、無効化を解除するだけで即時復旧できます。
もう一つの対策: 消費者の明示
各ジョブの設定に「このジョブの出力を使っているジョブ」を明記します。
1daily_fetch:2 consumers:3 - daily_aggregation4 - weekly_report消費者が明示されていれば、「消して大丈夫か?」の判断が格段に楽になります。
まとめ
| 判断基準 | 問い | アクション |
|---|---|---|
| 消費者の有無 | 出力を誰が使っているか? | なし → 削除 |
| 入力の重複 | 同じデータを取得していないか? | 重複 → 統合 |
| 頻度の妥当性 | 入力更新より高頻度でないか? | 過剰 → 調整 |
| 独立性の必要性 | 1ジョブにまとめられないか? | 線形依存 → 統合 |
ジョブの数は、システムの「健康」を示す指標ではありません。必要最小限のジョブで、必要な出力がすべて生成されている状態が健全です。
「このジョブ、本当に必要?」と問いかけ続ける仕組みを作ること。それが、ジョブ数の再膨張を防ぐ唯一の方法です。