電話が鳴ったのは午前2時。RiversideにあるEコマースのDCのシフトスーパーバイザーが、ほとんど叫ぶような声で言った。「Lao Mi、ソーターがおかしくなった!同じカートンが3回もスキャンされた、シュートは溢れかえって、コンベアが詰まりそうだ!」

私は彼に、設備には手を触れるな、代わりにログファイルを2つ取ってこいと言った。30分後、彼はバツが悪そうな声で折り返してきた。「おっしゃる通りでした……WESが同じウェーブを2回リリースしていました」

この映画はもう何度も見てきた。現場はいつも「システムがグリッチを起こしている」と言う。私の経験では、10回中9回、グリッチに見えるものはWESとWCSが互いに争っている状態で――根本原因はほぼ例外なく両者の間のインターフェースにある。これが私が実際に使っている、症状から根本原因までのトラブルシューティングの流れだ。

ラインに触れるな。まず2つのログを並べて話を聞け。

コマンド競合はたいてい、次の2つのどちらかで起きる。タスクの重複ディスパッチ、あるいはステータス返信(コールバック)のロストだ。WESがリリースコマンドを送る、ネットワークが途切れる、受信確認が返ってこない、そこでWESは設定されたタイムアウトの後に同じコマンドをもう一度送る。WCSは両方とも喜んで受け入れる――インターフェースにタスク番号の重複排除を組み込んだ人間が誰もいなかったからだ。1つのタスクがソーテーションキューに2回入り、同じパーセルがシュートから2回も3回も要求される。

ステップ1はいつも同じだ。WESのディスパッチログとWCSのコマンド受信ログを並べて、タスク番号を1つずつ突き合わせる。だがここに、10の倉庫のうち8つが引っかかる落とし穴がある。2台のサーバーの時計が同期していないのだ。WESのログにはコマンドが02:14:03に送信されたとあり、WCSのログには02:13:47に受信されたとある。タイムスタンプが一致しなければ、因果関係は決して確定できない。どのログにも触る前に、両側がNTPに載っていること――そしてタイムゾーンを確認しろ。片方がUTC、もう片方が現地時間でログを取っていた現場を、私は1つならず見てきた。

私自身も2021年にこれでつまずいた。OntarioのDCで、10万平方フィート超、クロスベルトソーターを回していた。ピークシーズンの真夜中、同じパーセルがシステムに3回現れた。両方のログを並べれば一目瞭然だった――WESがウェーブを2回リリースしており、間隔はちょうどタイムアウト1回分、WCSの実行ログには同一のタスク番号が2つ、両方とも実行されていた。一文で言えば、根本原因は「冪等性のないインターフェースにタイムアウト再送が当たった」ことだ。

このタイムアウト値には、本物のトレードオフが潜んでいる。短すぎれば、ネットワークのわずかな揺らぎのたびに誤った再送が飛ぶ。長すぎれば、ウェーブの切り替えも緊急オーダーも待ちぼうけだ。この数字は勘では決められない――自社の実ネットワークで測ったレイテンシから出さなければならず、リトライ回数とバックオフポリシーはインターフェース仕様書に書面で残す。口約束のままにしておくインテグレーターが多すぎる。そして約束は、エンジニアと一緒にドアの外へ消えていく。

実行フロアの仕分けライン

緊急オーダーが実行中タスクを追い出したら、古いタスクはどこへ行くのか

緊急オーダーによるプリエンプションは、最も厄介な競合ゾーンだ。ロジックは単なる重複よりずっとトリッキーだ。WESが緊急オーダーを受ける、現在のウェーブのタスクの一部をキャンセル済みにして、新しいタスクを滑り込ませる。だがWCSはすでにそれらのコマンドをPLCに落としている――すでに動いているダイバートを止めるには、キャンセルは遅すぎる。今や同じパーセルが、古いタスクからシュートAへ、新しいタスクからシュートBへと呼ばれている。設備はどちらに従うのか。

トラブルシューティングとは2本の糸を追うことだ。WESのタスク状態遷移(キャンセルは本当に出たのか、正確にはいつか)と、WCSのコマンドキュー(キャンセルが着いたとき、そのコマンドは実行のどこにいたか)だ。決定的な証拠はやはりタイムスタンプの差――キャンセルが発行された時刻と、コマンドの実行が実際に完了した時刻だ。引き算すれば、キャンセルに追いつく見込みがあったかどうかが分かる。

その下にもっと陰険な落とし穴がある。そもそも両者は同じタスク番号を話していない。WESは独自のタスクIDを使い、WCSはそれを設備コマンド番号に変換する。間にマッピングテーブルがある。それを失くせば、2つのシステムはすれ違いの会話を始める。2023年にChicago郊外の倉庫で見た――マッピングテーブルはメモリ上にあり、夜間にWCSサービスが再起動して、テーブルは消えた。夜勤のエンジニアは2つのログをにらんでも、一致するタスクを1つも見つけられなかった。何もログに残っていなかったからではなく、番号が合わなかったからだ。地味だが効く対策がある。マッピングテーブルをディスクに永続化し、WCSのログに元のWESタスク番号を最後まで載せることだ。

緊急オーダーについての私の結論はこうだ。物量が求めない限り、ハードなプリエンプションはするな。緊急オーダーには専用のシュートか専用のウェーブを与えろ――すでに実行中のタスクには触れるな。プリエンプションで稼げる数分では、1回のライン詰まりのコストは賄えない。どうしてもプリエンプトするなら、キャンセルは2フェーズにしなければならない。WCS側で未実行コマンドをロックするプレキャンセル、そして正式なキャンセル。すでに動いているものは、完了後補償フローに回す。1つのキャンセルコマンドが物理法則に勝てると期待するのは、光の速さへの敬意を欠いているだけだ。

「なぜ」を問うな。冪等にしろ。

上で見た2つの故障モードは、根本原因が1つだ。インターフェースが冪等でないことだ。WES側に必要なのはシンプルだ――すべてのタスクがグローバルに一意なタスク番号を持ち、再送時も同じ番号を使うこと。WCS側は、受信時に「このタスク番号は見たことがあるか」を確認すること。見たことがあれば、成功を返して二度実行しない。ステータス返信も同じ――単調増加のシーケンス番号を付けて、WES側が順不同や重複した返信を自力で整理できるようにする。

受け入れの場では、私はインテグレーターに3つの文章を渡す。そして契約書の付属書に入る。第1に、ネットワークを数秒切断して再接続する――タスクが2回実行されてはならない。第2に、WCSを再起動する――過去のタスクマッピングは生き残らなければならない。第3に、実行中タスクに対する緊急オーダーのキャンセルは、定義された補償アクションを発動させなければならない。「キャンセルは送信された」だけではダメだ。この3つを通れば、実行層の怪談の9割は起きない。

私の結論は変わらない。「システムのグリッチ」の10回中9回は、冪等のないインターフェースだ。残りの1回のうち半分は時計の非同期、半分は午前2時のボタンに置かれた誰かの震える手だ。

次の稼働開始の前に、ネットワークケーブルを数秒抜いて、自分で差し直してみろ。その数秒を生き延びたシステムだけが、ピークシーズンを任せるに値する。

英語版からの翻訳。内容に相違がある場合は英語版を優先します。