Slide 1

Slide 1 text

0 総会員数1,500万人のレストラン Web予約サービスにおけるRustの活用 山本浩平 | 2024-11-30 Rust.Tokyo 2024

Slide 2

Slide 2 text

1 自己紹介 ● 山本浩平 @kymmt90 ● 2023年に一休に入社し、一休.comレストランの Rustでの開発に途中から参加

Slide 3

Slide 3 text

2 本発表で話すこと ● 一休.comレストランについて ● Rust活用の詳細 ○ バックエンド ○ エッジコンピューティング ● 今後のチャレンジ

Slide 4

Slide 4 text

3 一休.comレストラン | サービス概要 ● 上質なレストランの Web予約サービスを提供 ● 2006年ローンチの長い 歴史を持つサービス

Slide 5

Slide 5 text

4 一休.comレストラン | サービス概要 ● 一休.com全体の会員規模1,500万人 ● バックエンドへのトラフィックは最大1,000req/sほど (夕方〜夜が多い)

Slide 6

Slide 6 text

5 一休.comレストラン | Rustを利用中 ● システムのさまざまな箇所をRustに移行中 ○ Web UI バックエンドのGraphQL API ○ 社内の他システムとの連携用REST API ○ Fastly Computeでのエッジコンピューティング ○ [WIP] Solr (全文検索エンジン)のインデクサー

Slide 7

Slide 7 text

6 本発表で話すこと ● 一休.comレストランについて ● Rust活用の詳細 ○ バックエンド ■ 概要と設計 ■ 技術トピック ○ エッジコンピューティング ● 今後のチャレンジ

Slide 8

Slide 8 text

7 Rustバックエンド | 概要 GraphQL API REST API frontend app internal systems SQL Server フレームワークはAxum GraphQL実装にはasync-graphqlを利用 Python, C#などからの移行 Solr Fastly (後述) internal systems backend

Slide 9

Slide 9 text

8 バックエンド | Rustの選定理由 ● 「シンプル、かつすばやく、それでいて堅牢に作れる」 という社内の技術選定方針に基づく ○ 表現力の高い型システムでドメインモデルをコード化 ○ 高速/省リソース: 運用コストを中長期的に改善 ○ crateによるモジュール依存関係の制御 ○ 社内で利用する技術のバランス

Slide 10

Slide 10 text

9 バックエンド | 設計 ● クエリモデル ○ C向け予約サービスの 特性上、表示用の 非正規化されたデータが ほしい場面が多いので、 クエリモデルを導入 DB, フレームワーク, 環境の設定 dataloaderのI/Fやユースケースなど エンティティ (クエリモデル) データアクセス層など

Slide 11

Slide 11 text

10 バックエンド | 設計 [例:データアクセス層] async fn fetch_restaurants( &self, database: &crate::Database, keys: &[RestaurantId], ) -> Result>> { let query = format!( // クエリ ); // レコードフェッチ、 DTOからクエリモデルへ let restaurant_models = database .query_as::(&query, params) .await .context("failed to query restaurants")? .into_iter() .map(|d| (d.id, Restaurant::try_from(d))) .collect(); Ok(restaurant_models) } 具体的なDBや設定は外から注入 上位層→モデルの依存方向になり モデルは外界に依存しない

Slide 12

Slide 12 text

11 バックエンド | モジュールの設計 ● Cargo Workspaceでリポジトリ内をモジュラーに ○ 各層(実際はもう少し細粒度)をworkspace crateで表現 ○ 各crateのCargo.tomlでcrate間の依存の向きを強制 # ルートディレクトリのCargo.toml [workspace] resolver = "2" members = [ "backend/*", ] [workspace.dependencies] backend-query-model = { path = "./backend/query-model" } # ...

Slide 13

Slide 13 text

12 バックエンド | モジュールの設計 ● workspace.dependenciesで設定した特定のcrateへの 依存をCargo.tomlに明記 # データアクセス層のCargo.toml [package] name = "backend-data-access" version.workspace = true authors.workspace = true edition.workspace = true publish.workspace = true [dependencies] backend-query-model = { workspace = true }

Slide 14

Slide 14 text

13 本発表で話すこと ● 一休.comレストランについて ● Rust活用の詳細 ○ バックエンド ■ 設計 ■ 技術トピック ○ エッジコンピューティング ● 今後のチャレンジ

Slide 15

Slide 15 text

14 バックエンド | utoipaでOpenAPI記述 ● 社内用REST APIはutoipaでOpenAPIドキュメントを記述 ● Rust内からもドキュメントにアクセスできるので、リクエストボディ のバリデーションにOpenAPI上のJSON Schemaを使う ○ Axumのextractorを定義 ○ ハンドラでリクエストボディを使うときに自動バリデーション

Slide 16

Slide 16 text

15 バックエンド | utoipaでOpenAPI記述 pub struct ConformantJson(pub T); #[async_trait] impl FromRequest for ConformantJson where // ... { type Rejection = ApiError; // IntoResponseである必要あり async fn from_request(req: Request, state: &S) -> Result { // utoipa::openapi::OpenApiの参照から特定のレスポンスボディのJSONスキーマを取得できるようにしてある let schema = OPENAPI_DOC .get_request_body_schema(path, &req.method().clone().into()) .expect("..."); match Json::from_request(req, state).await { Ok(Json(body)) => match validate_json_schema_conformance(&schema, &body) { Ok(_) => { let deserialized = serde_json::from_value(body).expect("..."); Ok(ConformantJson(deserialized)) } // 400エラーを返すApiError Err(err) => Err(err), }, Err(rejection) => // ... } } } ConformantJsonという構造体にAxumの FromRequestを実装することで、 JSON Schemaでリクエストボディを バリデーションしてから取り出すextractorを 作る

Slide 17

Slide 17 text

16 バックエンド | utoipaでOpenAPI記述 #[utoipa::path( post, path = "/internal/coupon-settings", request_body( content = CouponSetting, content_type = "application/json", ), responses(/* ... */), )] pub async fn create_coupon_setting( State(state): State, ConformantJson(new_coupon_setting): ConformantJson, ) -> Result { // ... } extractorであるConformantJsonで CouponSettingを取り出すとき、 そのJSONスキーマでバリデーションされ、 エラーなら自動で400 Invalid Requestを返す utoipaのマクロ (utopia::path , utoipa::ToSchema な ど)でPOST /internal/coupon-settings の スキーマを定義する

Slide 18

Slide 18 text

17 バックエンド | CPUバウンドな処理の扱い ● 一休では検索時の席在庫計算処理などがCPUバウンド ○ Tokioなど非同期ランタイムはawaitで他タスクに切替 ○ CPUバウンドな処理でブロックしてawaitまでに時間がかかると… ■ 他タスクに切り替わらずスレッド数上限以上には並行できない ■ 結果、他のリクエストが詰まったりする ● Rayonのスレッドプールに処理を委譲し、それをawaitすることで 問題を解決 (cf. https://ryhl.io/blog/async-what-is-blocking/#the-rayon-crate)

Slide 19

Slide 19 text

18 バックエンド | CPUバウンドな処理の扱い pub async fn spawn_rayon(f: F) -> R where //... { let (tx, rx) = tokio::sync::oneshot::channel(); rayon::spawn(move || { let _ = tx.send(f()); }); rx.await.expect("...") } // CPUバウンドな処理を実行する let res = spawn_rayon(move || /* 席在庫計算などの処理 */).await?; CPUバウンドな処理はRayonに任せて 結果だけチャンネル経由で受け取る CPUバウンドな処理が動くタスクを awaitできるので非同期ランタイムが タスク切り替えできる

Slide 20

Slide 20 text

19 本発表で話すこと ● 一休.comレストランについて ● Rust活用の詳細 ○ バックエンド ○ エッジコンピューティング ● 今後のチャレンジ

Slide 21

Slide 21 text

20 エッジコンピューティング | CDNキャッシュ ● 店舗探し体験の改善は顧客価値につながる ○ 高速にレスポンスするためにCDNからキャッシュ配信 ○ 一休ではFastlyを利用 ● キャッシュロジックを少し工夫したい ○ パーソナライズされないページでキャッシュ ○ 会員のランクに応じたコンテンツをキャッシュ ○ etc.

Slide 22

Slide 22 text

21 エッジコンピューティング | Fastly Compute ● Fastly Computeを導入 ○ エッジ上でWASMを実行。ロジックをRustで記述可能 ○ 一休.comレストランではVCLでの設定から移行中 ■ コードが読みやすい、テストが書きやすい

Slide 23

Slide 23 text

22 エッジコンピューティング | Fastly Compute #[fastly::main] fn main(mut req: Request) -> Result { // ... let resp = handle(req.clone_with_body())?; // ... Ok(resp) } fn handle(mut req: Request) -> Result { let backend = select_backend(&req); let resp = match &backend { Backend::RustBackend(cache_type) => { if !req.get_method().is_safe() { return Ok(req.with_pass(true).send(backend.name())?); } match cache_type { CacheType::Cache(options) => { // キャッシュ制御ロジック }, CacheType::CacheWithCacheGroup(options) => { // 会員ランクごとのキャッシュ制御ロジック }, // ... #[fastly::main]を付与したmain でリクエストをハンドリングする RustのコードとしてCDN上での キャッシュ制御やリクエスト振り分け を実装できる

Slide 24

Slide 24 text

23 本発表で話すこと ● 一休.comレストランについて ● Rust活用の詳細 ○ バックエンド ○ エッジコンピューティング ● 今後のチャレンジ

Slide 25

Slide 25 text

24 今後のチャレンジ | 予約コアロジックの移行 ● VBScriptで書かれWindows Server上で動く、予約作成、 変更、キャンセルなどを担うシステム (サービス開始当初から存在) ● VBScriptは2027年ごろWindowsからデフォルト無効化 https://techcommunity.microsoft.com/t5/windows-it-pro-blog/vbscript-deprecation-timelines-and-next-steps/ba-p/4148301

Slide 26

Slide 26 text

25 今後のチャレンジ | 予約コアロジックの移行 ● 新規予約、変更、キャンセル、割引やポイント付与を含む 既存のVBScriptからRustへの移行 ● Rustのコードとして、どうデータモデルを表現するかから 取り組むことが求められる

Slide 27

Slide 27 text

26 エンジニアを募集しています https://www.ikyu.co.jp/recruit/engineer