オフラインイベント「Cookpad Tech Kitchen #26 数千万レコードをリアルタイムに捌く生鮮EC事業開発」を開催しました!

買物プロダクト開発部部長の勝間(@ryo_katsuma) です。3/24に「Cookpad Tech Kitchen #26 数千万レコードをリアルタイムに捌く生鮮EC事業開発」をWeWork 渋谷スクランブルスクエアで開催しました。

イベントにはクックパッドの新規事業「クックパッドマート」を開発するエンジニア、デザイナーが参加し、クックパッドマートの最新の開発状況や組織についていろいろお話させていただきました。今回は当日の様子を発表資料も交えて紹介させていただきます。

クックパッドマートの概要とチャレンジ by 勝間

まず、1番めのトークとして私、勝間からクックパッドマートの事業紹介、および2022年3月現在の事業や組織規模、開発体制などについてお話させていただきました。

外から見るとある意味ほぼ完成されている(?)とも見えかねないサービスですが、実際は既存機能の価値向上や新たな価値づくり、流通の柔軟性の向上など多くの挑戦を行っています。

この日は、その中でも料理の楽しみを広げるための新たな挑戦として、3月にリリースした「グループ」機能の紹介もさせていただきました。

生鮮ECのタイムセールを耐え抜いてきた話 by Leo

2番目のトークは、ECアプリケーション開発グループ長のLeo(@lchin)から、イベントのタイトルでも触れている「数千万の生きたレコード」を、タイムセールというイベントを切り口に、高いパフォーマンスを保ったまま捌くためのトライについてお話させていただきました。

「商品配送先エリアや配送曜日毎に購入可能商品が変わる」「タイムセールの特徴上、負荷が集中する期間が短期間」「データ量もアクセス数も増え続ける」という、事業構造やサービス固有の状況に対して、モニタリングやその分析など、地道に、かつ着実に問題を1つ1つ解決していく事例をいくつか紹介いたしました。

物理世界でモノを運ぶための仕組み by 長

最後のトークは長(@s_osa)より、クックパッドマートの流通の仕組みについてお話させていただきました。

クックパッドマートの流通は、実際の配送こそいくつかの運送会社様のサポートを受けていますが、どのドライバーさんがいつ、何を、どこからどこへ物を動かすかは、自分たちで設計し、仕組みを実現しています。この日は、その中でも流通に必要な商品に貼り付ける「ラベル」や、オペレーションをサポートする「Webアプリケーション」にフォーカスし、私たちなりの工夫についてお話させていただきました。

この日に話せなかったテーマについて、ちょうど開発者ブログにも先日流通の取り組みについて執筆させていただいたので、興味ある方はこちらもご覧ください。

パネルディスカッション

トークの後は、スピーカーの3人にデザイナー統括マネージャー兼、クックパッドマートリードデザイナーの米田(@tyoneda)も加わり、会場にいらっしゃった方からの質問への回答を中心にパネルディカッションを行いました。

実は、質問が全くこなかった場合に備えて、汎用的なトークテーマをいくつか裏で用意していたのですが、実際は「事業構造」「組織カルチャー」「パートナーのアカウント管理」「キャッシュ戦略」...etc など、想定をはるかに超えたいろんなジャンルの質問を多くいただき、用意していたテーマについて話す時間が無かったのは嬉しい誤算でした。同時に、いただいた質問についてもすべてお話しできなかったのだけは残念。。

なお、当日の質問の様子はSli.doのページにアーカイブが残っています。当日回答できなかった質問に対する回答も追記していますので、ぜひあわせてご覧ください。

まとめ

今回のイベントは、クックパッドとしては約2年ぶり(!)のオフラインイベント、またコロナ禍でのイベント開催ということもあり、感染対策に気を配りつつもどうすれば価値のあるイベントを運営できるか、運営側も当日ぎりぎりまでかなり試行錯誤が多かったのは本音です。また、参加される方たちにとっても相当久々のオフラインイベントで、参加すること自体がかなりハードルが高かったと思います。

一方で、イベント前に受付で軽く雑談をさせていただいたり、イベント後に各スピーカーや来場者同士での意見交換をさせていただくなど、オフラインイベントならではの熱量を持った場を久々に取り戻すことができたのは運営側にとっても嬉しい限りでした。

まだまだ段階的ではありますが、今後クックパッドは今回のようなオフラインイベントも少しづつ再開していこうと考えています。イベントの最新情報はTwitterで公開していますので、ご興味ある方はぜひ@cookpad_techをFollowお願いいたします!

また、クックパッドマートはエンジニアを絶賛募集しています。今回のイベントでも話したEC領域、流通領域において、どのような環境で開発をしているか、どのようなエンジニアを募集したいかについてnoteにまとめていますので、中の様子が気になる方は、こちらもあわせて参照いただければ幸いです。

クックパッドマート最難解ロジック!?「採番」

クックパッドマート流通基盤アプリケーション開発グループでバックエンドエンジニアをしている奥薗 ( @mokuzon )です。今日まで 4 日間連載でクックパッドマートの流通についてご紹介してきました。最後のこのエントリーではマート内で 1,2 を争う難解かつ重要な処理と言われている「採番」についてご紹介します。

先に クックパッド生鮮 EC お届けの裏側 2022 年版 を読むとよりイメージがつきやすいです。

採番とは

マートでは

  1. 商品はハブという大規模拠点に出荷され
  2. ハブ便でハブからハブへ移動し
  3. ステーション便でハブからステーションと呼ばれる拠点に移動して
  4. ユーザーはステーションに商品を受け取りに行く

というのが基本の流通になっています。

マート流通の概略図

採番とは、この商品がどのようなスケジュールで出荷されどういった経路でユーザーまで運ばれるかを計画する処理です。多くは注文時にオンラインで、一部特定時刻にバッチ処理で行っています。

出荷された商品やそれが入ったコンテナには以下のようなラベルが貼られています。

枠で囲った部分は実際のハブとステーションにある番地名です。商品の配送計画をするにあたってこのように番地などを決定していくため「採番」と呼ばれています。

この採番はドメイン知識の集合体かつ非常に手続き的な処理のため、物理的な流通の仕組みが大きく変わるとこの採番もほぼ作り直しになるなど、改善も含めてこれまでに 3 回リプレースしました。

なかなかに難解な処理でずっと職人と呼ばれる人たちの独断場でしたが、3 年強の運用で大分こなれて勝ちパターンが見えてきたのでこのタイミングでご紹介することにしました。

なにが難しいか

ひとえにマート流通のあらゆるドメイン知識が詰め込まれていることです。3 日かけてマート流通を紹介してきましたが、これだけでも複雑ですし、実際には紹介しきれないぐらい細かいより複雑な要件が沢山あります。この複雑な流通を計画するという採番の性質上、どうしてもこの難しさとは向き合っていく必要があります。

採番するにあたりどのようなことを考慮するかをざっと挙げると

  • キャパシティ管理
    • コンテナに収まるか
    • ハブ便の 2t トラックに収まるか
    • ステーション便の軽カーゴに収まるか
    • 冷蔵商品を入れるシッパーに収まるか
    • ハブの番地数は足りるか
    • ステーションの番地数は足りるか
  • どのコンテナに収めるか
    • 商品が出荷されるハブはどこか
    • ハブ便で移動先のハブはどこか
    • 最終的に到達するステーションはどこか
  • 温度管理
    • 常に冷蔵が必要な商品か
    • 冷蔵でも常温でもいい商品か
      • 夏は? 冬は?
    • 冷蔵 NG な商品か
  • 出荷日
    • 販売者の営業日か
    • 商品の消費期限内に流通させられるか

のような条件を一つ一つクリアしていきます。

設計の工夫

現実世界では密結合でもデータでは疎結合にする

配送のフェーズによって商品がいる場所は以下のように変化します。

  1. ハブのコンテナの中
  2. ハブ便のコンテナの中
  3. ハブのコンテナの中
  4. ステーション便のコンテナの中
  5. ステーションのコンテナの中

すべてのコンテナは同一のコンテナで、コンテナに収まるかのキャパシティ管理とそのコンテナの経路だけ決めればいいようにするのが理想です。具体的には以下のイメージです。

しかし、現実には

  • ステーションに直接出荷する特殊な出荷方法
  • ユーザーの受け取り期限を延長し、ステーションのコンテナが入れ替わっても特定商品はそのまま残したい
  • 配送拠点で商品を仕分けし、それぞれの配送都合に最適な形でコンテナに詰め直したい

このような要望があり、綺麗に 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 でトランザクションを張れていないため、マートが定期的に開催するタイムセールなど注文が殺到するとどうしても競合データが生まれてしまいます。

こうした不正なデータ対策として、データのチェックバッチを運用しています。

考えられ得る限りの不正な状態を実装してチェック、想定外なパターンが出てくればそれも実装してチェックバッチを育てています。簡単なものなら自動でデータ修正もしています。

育てやすくするためにチェックバッチはプラガブルな設計になっています。これはどこかで改めて詳細にご紹介できればと思っています。

Slack にチェックバッチの結果が通知されている様子

リニューアル方法

現在マートは 24 時間 365 日注文を受け付け続けていて、この採番処理もいつでも動きえます。

ロジックをリニューアルする際は本当はメンテナンスを挟んで移行したいところですが、事業上注文や流通を止めたくはないですし、この採番データは流通のあらゆるシステムで参照されていて変更箇所が膨大になるので、ビッグバンリリースを避ける観点でもオンラインで変更していくようにしています。

方法としてはこの手の移行だとセオリー通りかと思いますが、新旧ロジックがあるとして

  1. 旧ロジックの裏で、新ロジックも裏で動かす
    • テーブルも新設して新旧両方のテーブルに書き込む状態にする ( Dual Write )
    • この時点では各所では旧ロジックの結果のみ参照する
    • 先述のチェックバッチなどで、新ロジックの結果が意図通りになっていることを 1, 2 週間運用して確認する
  2. 新ロジックの結果を元に旧ロジックが書き込むテーブルにデータをコピーする
    • 旧ロジックは止める
  3. データの参照先を旧テーブルから新テーブルに徐々に切り替えていく

という手法で行っています。

おわりに

クックパッドマート随一難解な採番処理についてご紹介しました。

正直文章だけでお伝えしきるのは難しいものなので、少しでも興味を持っていただけましたら Twitter で @mokuzon に声をかけていただいてもよいですし、カジュアル面談も実施していますのでぜひお気軽にご応募ください。

cookpad.careers

2022年クックパッドマート連載の他のエントリ

クックパッドマートの配送ルートを自動生成している仕組み

こんにちは、クックパッドマート流通基盤アプリケーション開発グループのオサ(@s_osa_)です。

生鮮食品の EC サービスであるクックパッドマートでは、「1品から送料無料」をはじめとするサービスの特徴を実現するために、商品の流通網を自分たちでつくっています。

このエントリでは、商品をユーザーに届けるための配送ルートを自動生成している仕組みについて紹介します。

解決したい問題

配送ルートとは

クックパッドマートにはいくつかの流通方法がありますが、ここでは「ステーション便」と呼ばれるものについて解説します。他の流通方法などを含む全体像が気になる方は以下のエントリがオススメです。

クックパッド生鮮 EC お届けの裏側 2022 年版 - クックパッド開発者ブログ

ステーション便では、ハブと呼ばれる流通拠点からユーザーが商品を受け取りに行く場所であるステーションへと商品を運びます。東京都、神奈川県、埼玉県、千葉県の一部地域に700以上のステーションがあり、それらのステーションに対して複数のハブから配送をおこなっています。 ステーションはコンビニやマンションなどに設置されている冷蔵庫で、クックパッドマート配送の大きな特徴のひとつです。

クックパッドマートのステーション(冷蔵庫)

サービスの利用方法は次のような流れになります。

  1. アプリから商品を注文する
  2. 最寄りのステーションに商品が届く
  3. 都合の良いタイミングで受け取る

この仕組みにより、1品から送料無料を実現できたり、配達時間に在宅しておく必要がなくなったりといった特徴が実現されています。

一部地域を抜粋してハブとステーションを地図にプロットすると以下のようになります。白抜きされた大きな点がハブ、小さくて黒い点がステーションです。

ハブとステーションの位置

すべてのステーションに商品を届けるためには、ハブ(白抜きされた大きな点)を出発してステーション(黒い点)を網羅するルートを作る必要があります。

たとえば、以下のような感じです。

配送ルートのイメージ

配送ルートに求めるもの

配送ルートを組むにあたり、どんなルートでも良いかというと当然そんなことはなく、いくつかの制約や目標があります。具体的には以下のようなものです。

  • 所要時間(所定の時間内に配送完了できる)
  • 積載量(商品をあふれさせず積みきれる)
  • ステーションの営業時間
  • コスト効率
  • スケーラビリティ

それぞれの性質について簡単に解説します。

所要時間

現在のクックパッドマートでは12:00から商品を受け取れるようにしており、08:00から12:00までの4時間でステーションへの配送をしています。生成するルートはこの時間内に配送を終えられるように組む必要があります。

積載量

車に積める量には限度があります。

クックパッドマートでは商品をコンテナ(カゴ)に入れて運んでいます。常温商品はコンテナをそのまま積み込めば良いですが、冷蔵商品はコンテナをシッパーと呼ばれる保冷容器に入れた上で蓄冷材と一緒に運ぶ必要があるのでより多くの体積を必要とします。

シッパー(保冷容器)に収まっているコンテナ

こういった温度帯ごとの体積の違いといった事情も考慮した上で商品を車両に積みきれるようにルートを組む必要があります。

ステーションの営業時間

配送先のステーションには、納品できる時間帯に制限があるステーションも存在します。

たとえば、マンションで09:00以降にならないと管理人さんがいないため入れない、ドラッグストアの営業時間が10:00から、といったケースなどがあります。

こういった各ステーションの営業時間を考慮して、納品できる時間に着くようにする必要があります。

コスト効率

配送は車両と人間を動かす必要があるので、かなりコストがかかります。毎日ルートが1本減ればその1本分、月で延べ30本分のコストが削減できます。

したがって、できるだけ少ないルート数で配送できるようにしたいという要求があります。

スケーラビリティ

上記のような制約や目標を考慮しながらルートを組むのは人間にはあまりにも難しいです。

いにしえの時代、クックパッドマートの配送ルートは人間によって手組みされていましたが、ステーションの数が300くらいの頃で、熟練のルート職人が2日かけてやっとルートを組み終わるといった状況でした。

サービスは今後も拡大していくため、ステーションが増加してもルートを組み続けられる仕組みが必要です。

ブロック分割

さて、これからルートを組んでいくわけですが、複数のハブから700箇所のステーションに運ぶとなると「それぞれのステーションにはどのハブから運ぶのか」ということが問題になってきます。

そこで、ルート生成に先立って、それぞれのステーションをハブに対応付ける前処理を入れます。我々はこれをブロック分割と呼んでいます。

先にブロック分割の結果を図示しておくと次のようになります。同じ色の点が同じブロックに属するステーションです。

ブロック分割のイメージ

ブロック分割の方針

基本的な考え方はとてもシンプルで「それぞれのステーションに近いハブから運ぶ」というものです。この方針はわりと素直に受け入れてもらえるものだと思います。

ただ、この方針を実装していくにあたり、考慮・対応すべきことがいくつかあるので、それらの点について書いていきます。

「近い」とは

「近いハブから運ぶ」と言っても、本当に関心があるのは距離ではありません。実は「ルートに求めるもの」のところでも距離には一切触れていません。

では代わりに何に触れているかといえば、時間です。所定時間内に運びきるという制約を考えるためには距離よりも時間に着目する必要があります。

移動時間の求め方

最も素朴な移動時間の求め方はハブやステーションの緯度経度から直線距離を求めて、平均速度で割るというものです。この方法は正確性は低いですが実装が楽で早くリリースできるので最初期はこの方法で計算していました。

ただし、この直線距離を使う方法には大きな問題があります。最も顕著な例としては、東京や神奈川から千葉に商品を運ぼうとすると車が東京湾の海上を走れることになってしまいます。言うまでもなく車は海上を走れないので、実際には沿岸部の道路だったり東京アクアラインだったりを通って迂回する必要があり、計算結果にかなり大きな誤差が生じてしまいます。また、東京湾ほど大きくないものだと「川は橋がある箇所でしか渡れない」「行きたい方向に走っている太い道路がない」などの誤差要因もあります。

そこで、道路を考慮した移動時間を計算するために Open Source Routing Machine (OSRM) を使っています。

http://project-osrm.org/

OSRM は OpenStreetMap (OSM) の地図データを用いて経路計算をしてくれるルーティングエンジンです。Google Maps の経路検索をイメージしてもらうとわかりやすいと思います。

OSRM はフロントエンドまで含めたプロジェクトになっていますが、バックエンドの API だけでも利用することが可能です。クックパッドマートでは社内にデプロイした OSRM の API を使って移動時間を計算しています。また、この API は数十msと比較的高速にレスポンスを返してくれますが、ルート生成の過程では大量の移動時間の計算が発生するため、必要になる移動時間は事前に計算して DB に入れておいて、計算時には一括でメモリに載せてしまうなどのパフォーマンス上の工夫もいくつかおこなっています。

各ハブのキャパシティ考慮

近くのハブから運びたいというのは間違いなく真です。ただし、「運びたい」と「運べる」は別の話です。

現実の各ハブが無限の荷量をさばけるかというと決してそんなことはなく、実際にはさばける荷量(キャパシティ)には上限があります。そこで、各ハブがさばける荷量に収まるようにステーションの紐付け先を調整します。

この調整はわりと素朴なアルゴリズムでおこなっています。具体的には、事前に各ハブに移動時間に対する係数を持たせておいた上で、キャパシティを超えているハブがあった場合、そのハブの係数を増やします。そして、変更後の係数を使って移動コスト(係数を反映した重み付き移動時間)を再計算した上で、紐付け先を計算し直してキャパシティに収まっているかチェックして……というのを繰り返します。細かいところの動作は全然違いますが、k-means 法などをイメージしてもらうとわかりやすいかと思います。

ルート生成

ブロック分割によって、それぞれのステーションにどのハブから商品を運ぶかということが決まったので、各ブロック内でルートを組んでいきます。

初めにも貼りましたが、再掲しておくと、たとえばこんなルートになります。

配送ルートのイメージ(再掲)

配送ルートの組み方

配送ルートを組む問題を一般に配送計画問題(Vehicle Routing Problem, VRP)と呼びます。組み合わせ最適化問題の一種で、最適解を現実的な時間(多項式時間)で求めることはできない問題です。

一方で、VRP という名前がついているくらいには有名な問題なので、近似解を求めるライブラリなどは存在しています。クックパッドマートでは OR-Tools というライブラリの Ruby wrapper を使用しています。公式の C++, Python 実装ではなく非公式の Ruby 版を使っているのは既存実装との繋ぎ込みやチームのメンバー構成などを考慮した結果です。

VRP の最も難しい部分である解の探索はライブラリがやってくれるので、我々アプリケーション開発者は自分たちのサービスが必要としているルートの条件を整理・変形して、ライブラリが解ける一般的な問題に落とし込んでいきます。

一般的な問題への落とし込み

基本的な考慮事項は前述の所要時間・積載量・ステーションの営業時間などです。これらを OR-Tools が求めるカタチにして渡していきます。

たとえば、移動時間の計算に OSRM を使う話をしましたが、OSRM が返す移動時間は実際にかかる移動時間よりも少なめに出ることが多いので、その差を補正するための係数をかけています。

また、移動時間は OSRM で算出することができますが、実際には移動時間とは別に各ステーションでの納品にかかる作業時間があります。そこで、作業時間を考慮したトータルの所要時間を入力として渡すようにします。

さらに、一般的な VRP は配送完了後に出発地点に帰ってくるルート、つまり1周するルートを組むようにできています。しかし、クックパッドマートで必要な制約は「12:00までにステーションに配送完了」であって「12:00までにハブに戻る」ではないので、最終配送の後の戻りルートを時間計算に含めないようにする必要があります。

こうして、自分たちのサービスで必要なことをひとつずつ一般的な問題に落とし込んでいきます。

運送会社へのルートの割り当て

クックパッドマートでは実際の配送業務自体は運送会社に外注しています。また、外注先の運送会社は複数社あります。そこで、生成したそれぞれルートをどの会社に割り振るかということを考える必要があります。いわゆるマッチング問題の一種ですが、ここでもいくつかの考慮事項があります。

たとえば、それぞれの運送会社には「行きたいハブ」と「行きたくないハブ」があります。これは運送会社の立地や営業エリアなどの特性によって生じるものです。運送会社と継続的な関係を結んでいくためには各運送会社にとって無理がないルートを割り振る必要があります。

この問題を解決するために、確保しているそれぞれの車両とハブの組み合わせについて「行きたくない度」(選好)を事前に登録しておき、その点数ができるだけ小さくなるようにルートを割り当てています。

また、「あるステーションは入館証が必要で、入館証を持っているのはA社のみ」のような制約もあるので、そういった点も考慮して各ルートを運送会社に割り振っています。

やっていないこと

ここまでルート生成でやっていることについて書いてきましたが、一方で意図的にやっていないこともあります。

代表的なものが具体的な走行ルートの指示です。各ステーションの配送順序や住所とその住所の Google Maps URL などは提供していますが、具体的な走行ルートは指定・指示していません。これは交通状況は常に変化するため効果的なルート指定が難しい上、運送会社やドライバーの方は運転・配送についてはプロなので指定する必要性が低いというのが理由です。

また、それぞれのブロックは独立しているのでルート生成は並列化可能ですが、今のところ特に困っていないので直列で計算しています。

おわりに

クックパッドマートで配送ルートを自動生成している仕組みについて簡単に紹介してきました。

どの領域でもそうだと思いますが、流通というドメインにも特有の問題や難しさ、そして楽しさがあります。また、問題解決のためには事業ドメインと技術ドメインの両方を考慮して解決方法を探っていく必要があります。現実世界のモノを運ぶという課題に対して、ソフトウェアを軸足にして取り組んでいくのは非常に楽しいです。

このエントリで紹介できたのは流通という領域のごく一部です。少しでも興味が湧いた方がいたらぜひご連絡ください。採用サイトからの正規ルートでも良いですし、@s_osa_ まで雑に DM していただくなどでも大丈夫です。よろしくお願いします。

2022年クックパッドマート連載の他のエントリ