クックパッドマート流通基盤アプリケーション開発グループでバックエンドエンジニアをしている奥薗 ( @mokuzon )です。今日まで 4 日間連載でクックパッドマートの流通についてご紹介してきました。最後のこのエントリーではマート内で 1,2 を争う難解かつ重要な処理と言われている「採番」についてご紹介します。
先に クックパッド生鮮 EC お届けの裏側 2022 年版 を読むとよりイメージがつきやすいです。
採番とは
マートでは
- 商品はハブという大規模拠点に出荷され
- ハブ便でハブからハブへ移動し
- ステーション便でハブからステーションと呼ばれる拠点に移動して
- ユーザーはステーションに商品を受け取りに行く
というのが基本の流通になっています。
採番とは、この商品がどのようなスケジュールで出荷されどういった経路でユーザーまで運ばれるかを計画する処理です。多くは注文時にオンラインで、一部特定時刻にバッチ処理で行っています。
出荷された商品やそれが入ったコンテナには以下のようなラベルが貼られています。
枠で囲った部分は実際のハブとステーションにある番地名です。商品の配送計画をするにあたってこのように番地などを決定していくため「採番」と呼ばれています。
この採番はドメイン知識の集合体かつ非常に手続き的な処理のため、物理的な流通の仕組みが大きく変わるとこの採番もほぼ作り直しになるなど、改善も含めてこれまでに 3 回リプレースしました。
なかなかに難解な処理でずっと職人と呼ばれる人たちの独断場でしたが、3 年強の運用で大分こなれて勝ちパターンが見えてきたのでこのタイミングでご紹介することにしました。
なにが難しいか
ひとえにマート流通のあらゆるドメイン知識が詰め込まれていることです。3 日かけてマート流通を紹介してきましたが、これだけでも複雑ですし、実際には紹介しきれないぐらい細かいより複雑な要件が沢山あります。この複雑な流通を計画するという採番の性質上、どうしてもこの難しさとは向き合っていく必要があります。
採番するにあたりどのようなことを考慮するかをざっと挙げると
- キャパシティ管理
- コンテナに収まるか
- ハブ便の 2t トラックに収まるか
- ステーション便の軽カーゴに収まるか
- 冷蔵商品を入れるシッパーに収まるか
- ハブの番地数は足りるか
- ステーションの番地数は足りるか
- どのコンテナに収めるか
- 商品が出荷されるハブはどこか
- ハブ便で移動先のハブはどこか
- 最終的に到達するステーションはどこか
- 温度管理
- 常に冷蔵が必要な商品か
- 冷蔵でも常温でもいい商品か
- 夏は? 冬は?
- 冷蔵 NG な商品か
- 出荷日
- 販売者の営業日か
- 商品の消費期限内に流通させられるか
のような条件を一つ一つクリアしていきます。
設計の工夫
現実世界では密結合でもデータでは疎結合にする
配送のフェーズによって商品がいる場所は以下のように変化します。
- ハブのコンテナの中
- ハブ便のコンテナの中
- ハブのコンテナの中
- ステーション便のコンテナの中
- ステーションのコンテナの中
すべてのコンテナは同一のコンテナで、コンテナに収まるかのキャパシティ管理とそのコンテナの経路だけ決めればいいようにするのが理想です。具体的には以下のイメージです。
しかし、現実には
- ステーションに直接出荷する特殊な出荷方法
- ユーザーの受け取り期限を延長し、ステーションのコンテナが入れ替わっても特定商品はそのまま残したい
- 配送拠点で商品を仕分けし、それぞれの配送都合に最適な形でコンテナに詰め直したい
このような要望があり、綺麗に 1 度コンテナに商品を入れただけでは完結しないシチュエーションが多々あります。そこで、以下のように各拠点や各便を疎に表現するようにしています。
こうすることで、ある 1 点での特殊な状態が少ない影響で表現しやすく、また将来的に流通の構成要素に変更があった際も、変更範囲が相対的に少なく抑えられると期待しています。
留まることの表現
採番では「この地点にこの体積のものがいつからいつまで留まる」という表現が多発します。
以前は「この便に乗っているものは何時から何時」というドメイン知識から同じ番地を使い得るもの同士で「これとこれは被る」「これとこれは被らない」というのをすべて網羅して判定していました。一例として挙げると
- 今日出荷された商品は昨日出荷された商品が最初に到達するハブでは時間帯が被らない
- 今日出荷された商品は昨日出荷された商品が最後に到達するハブでは時間帯が被る
- etc…
というような条件を愚直に十数件判定していました。
すべて網羅していたというのは嘘で、実際にはどんどん複雑化して考慮漏れがあり、現実世界で番地競合を起こし配送で大事故が起きたことがありました。
これ以外にも似たようなパターンのミスが起きていて、共通しているのは留まる期間が 1 日を超え、別の日に扱うもの同士が干渉しうる状態になっていたことです。複雑度がここまで来ると扱いきれなくなるという指標ができました。
そこで、留まることを具体的に表現するテーブルを作る方針に変更しました。
以下の ER 図の assignments
テーブルです。
- 始点をキリの良い時刻で表現したほうが運用上便利
- データを
<, >
より<=, >=
で扱えた方が直感的
という理由で 13:00-14:00 のような期間は実際には start_at: 13:00:00, end_at: 13:59:59 のように保存しています。
このテーブルがあると、例えば以下の図で X start_at と X end_at の間に被るレコード B, C, D は
assignments.start_at <= X end_at and X start_at <= assignments.end_at
という簡単なクエリで導出出来ます。
注意点としては我々が DB で採用している B-tree index だと範囲指定を複数のカラムで使ってる場合、片方の index しか効きません*1。
assignments.end_at <= X start_at
は時間が経つほど過去のレコードが積み重なり参照する行数が増えていきます。なので未来のレコードを参照し相対的に桁数が少ない assignments.start_at <= X end_at
の index が使用されるようにしています。
チェックバッチ
採番はナマモノです。採番後に前提としていたデータが再入稿されたり突然ステーションが臨時休業になったりして、一時は正しかった採番データが不正なデータになったりします。
また、この採番処理は注文時にオンラインで実行していてパフォーマンス上の事情で DB でトランザクションを張れていないため、マートが定期的に開催するタイムセールなど注文が殺到するとどうしても競合データが生まれてしまいます。
こうした不正なデータ対策として、データのチェックバッチを運用しています。
考えられ得る限りの不正な状態を実装してチェック、想定外なパターンが出てくればそれも実装してチェックバッチを育てています。簡単なものなら自動でデータ修正もしています。
育てやすくするためにチェックバッチはプラガブルな設計になっています。これはどこかで改めて詳細にご紹介できればと思っています。
リニューアル方法
現在マートは 24 時間 365 日注文を受け付け続けていて、この採番処理もいつでも動きえます。
ロジックをリニューアルする際は本当はメンテナンスを挟んで移行したいところですが、事業上注文や流通を止めたくはないですし、この採番データは流通のあらゆるシステムで参照されていて変更箇所が膨大になるので、ビッグバンリリースを避ける観点でもオンラインで変更していくようにしています。
方法としてはこの手の移行だとセオリー通りかと思いますが、新旧ロジックがあるとして
- 旧ロジックの裏で、新ロジックも裏で動かす
- テーブルも新設して新旧両方のテーブルに書き込む状態にする ( Dual Write )
- この時点では各所では旧ロジックの結果のみ参照する
- 先述のチェックバッチなどで、新ロジックの結果が意図通りになっていることを 1, 2 週間運用して確認する
- 新ロジックの結果を元に旧ロジックが書き込むテーブルにデータをコピーする
- 旧ロジックは止める
- データの参照先を旧テーブルから新テーブルに徐々に切り替えていく
という手法で行っています。
おわりに
クックパッドマート随一難解な採番処理についてご紹介しました。
正直文章だけでお伝えしきるのは難しいものなので、少しでも興味を持っていただけましたら Twitter で @mokuzon に声をかけていただいてもよいですし、カジュアル面談も実施していますのでぜひお気軽にご応募ください。