Slide 1

Slide 1 text

0 Rust、何もわからない… #12 一休.comレストランの Rustバックエンド開発の様子 2024-02-28 山本浩平

Slide 2

Slide 2 text

1 自己紹介 ● 山本浩平 (kymmt, @kymmt90) ● 一休.comレストランのエンジニア ○ 一休には2023年9月末に入社し、 一休.comレストランのRustでの開発に参加 ● Rust歴は半年強

Slide 3

Slide 3 text

2 一休とRust | 一休.comレストランとは ● 上質なレストランをスムーズに 予約できるサービス https://restaurant.ikyu.com

Slide 4

Slide 4 text

3 一休とRust | Rustを利用している箇所 ● スマートフォンビューにおけるGraphQLバックエンド ○ 従来のPythonバックエンドを一部置き換え ○ 2023-10から本番で運用 ○ レストランの詳細や空席の閲覧から予約完了まで ● 社内のマイクロサービスと通信するためのREST API ○ まだ少ない

Slide 5

Slide 5 text

4 一休とRust | Rustを選定した狙い ● CPU利用効率の向上に伴うパフォーマンスの改善 ○ CVRに直結するレスポンス高速化 ○ クラウドの利用コストを削減 ● 開発生産性の向上 ○ 型や便利な機能でドメインロジックを実装 ● 組織の技術ポートフォリオの多様性を維持 ○ 古くからある.NET系、近年増えたGoが大半だった ところにRustが!

Slide 6

Slide 6 text

5 一休.comレストランにおける Rust活用の様子を紹介します 🦀

Slide 7

Slide 7 text

6 1. Cargo Workspaceでcrate依存関係制御 ● 現在のモジュール構造 ○ CQRSに基づいて 細かくcrateを分けている

Slide 8

Slide 8 text

7 1. Cargo Workspaceでcrate依存関係制御 ● Cargo Workspace ○ プロジェクトで複数のcrateを管理できる ○ 各crateのCargo.tomlで依存する他のcrateを指定する ● 依存関係を変更したいとき ○ Cargo.tomlが変更されるのでレビューで確認できる ○ うっかり変な方向の依存が入ったりしにくい ドメインモデル層 データアクセス層 OK! ?

Slide 9

Slide 9 text

8 1. Cargo Workspaceでcrate依存関係制御 # ルートディレクトリのCargo.toml [workspace] resolver = "2" members = [ "backend/*", ] [workspace.dependencies] backend-query-model = { path = "./backend/query-model" } # ... 各crateをworkspaceのmemberにする (この例ではbackendディレクトリ配下) workspaceのmemberが backend-query-modelとして ./backend/query-model に依存できるようルートで設定する

Slide 10

Slide 10 text

9 1. Cargo Workspaceでcrate依存関係制御 # データアクセス層の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 } workspace.dependencies で設定した特定のcrateへの 依存をCargo.tomlに明記する

Slide 11

Slide 11 text

10 2. 便利なツールでコードの質をチェック ● rustfmt (cargo fmt)でフォーマットし忘れをチェック ○ cargo fmt --all -- --check ● Clippy (cargo clippy)で望ましくない書きかたをチェック ○ cargo clippy --all-targets --all-features -- -D warnings ● cargo-nextestで自動テストを実行 ○ よりCIに向いたテストランナー ○ cargo nextest run

Slide 12

Slide 12 text

11 3. 型安全なデータ変換 ● Webバックエンドではデータ変換が頻出 ○ データストアの生データ ↔ DTO ○ DTO ↔ クエリモデルのインスタンス ○ クエリモデルのインスタンス ↔ HTTPのレスポンス ● 一休.comレストランの場合 ○ DBや検索サーバからのデータが最終的にGraphQLの レスポンスに

Slide 13

Slide 13 text

12 3. 型安全なデータ変換 | Serde ● Serde/serde_asを生データからDTOへのデシリアライズで 利用 ○ 属性マクロ(アノテーションのようなもの)を書いて 生データとDTOの属性をマッピング

Slide 14

Slide 14 text

13 3. 型安全なデータ変換 | Serde mod dto { // ... #[serde_with::serde_as] #[derive(Debug, serde::Deserialize)] pub struct Restaurant { #[serde(rename = "restaurant_id")] #[serde_as(as = "serde_with::TryFromInto")] id: RestaurantId, #[serde(rename = "restaurant_name")] name: String, // ... } } • カラム名と属性名 • 整数値と値オブジェクト のマッピングを定義して 型安全にレストランの データをデシリアライズ

Slide 15

Slide 15 text

14 3. 型安全なデータ変換 | From/TryFrom ● std::convertのトレイト FromやTryFromをRust上の データの変換に利用 ● Fromを実装してDTOからモデルへの変換方法を定義 ○ 例: impl From for query_model::Restaurant ○ Intoも自動的に実装される ■ ブランケット実装 impl Into for T where U: From ○ from/intoでDTO ↔ モデルを型安全に変換できる

Slide 16

Slide 16 text

15 3. 型安全なデータ変換 | From/TryFrom // Fromを実装するとブランケット実装によりIntoも自動的に実装 impl From for query_model::Restaurant { fn from(d: dto::Restaurant) -> Self { Self { id: d.id, name: d.name, // ... } } } // 相互に変換可能 let model: query_model::Restaurant = dto.into(); let dto = dto::Restaurant::from(model); 型推論が効くので 個別の型注釈は不要な ことも多い

Slide 17

Slide 17 text

16 4. Webアプリ開発に便利なcrateを活用 ● GraphQLサーバの実装にはasync-graphql ● OpenAPIドキュメントの生成にはutoipa ● マクロの表現力を活用してDSLを提供するcrateが多い

Slide 18

Slide 18 text

17 4. Webアプリ開発に便利なcrateを活用 // type Restaurant { // name: String! // } // のようなスキーマを定義 pub struct Restaurant(pub query_model::Restaurant); #[async_graphql::Object] impl Restaurant { // リゾルバ async fn name(&self) -> &str { &self.0.name } // ... } async-graphqlによる GraphQLスキーマ定義 #[async_graphql::Object] がcrate提供のマクロ

Slide 19

Slide 19 text

18 4. Webアプリ開発に便利なcrateを活用 #[utoipa::path( get, path = "/internal/api/{id}/inventories", responses( (status = OK, description = "成功", body = Vec), (status = NOT_FOUND, description = "存在しない") ), params( ("id" = String, Path, description = "ID", example = "1234") ) )] pub async fn get_inventories(...) utoipaによるOpenAPI ドキュメントのパス定義 #[utopia::path(...)] がcrate提供のマクロ

Slide 20

Slide 20 text

19 5. 本番で効率よく運用 ● 本番ではCloud Runを利用 ○ 予約が多い年末もコンテナインスタンス2〜4台で 問題なく運用できた ● インフラ全体の運用コストを改善 ○ 従来のバックエンドのEKSのReplicaSet数減 ○ Rustバックエンドから効率よくSolrを利用して負荷減 cf. Solr クエリを速度改善したら Solr 全体のパフォーマンスが向上した https://user-first.ikyu.co.jp/entry/2023/12/06/173215

Slide 21

Slide 21 text

20 最後に | Rustでの開発の感想 ● 型に基づく開発体験(rust-analyzerなど)とてもよい ● 便利で安定したパッケージ管理やビルド、ツール実行の 仕組みを提供してくれるCargoもとてもよい ● Web開発の細かい情報をもっと増やしたい ○ 例: 私(kymmt)の細かいアウトプット https://blog.kymmt.com/archive/category/Rust

Slide 22

Slide 22 text

21 PR | エンジニア採用中です https://www.ikyu.co.jp/recruit/engineer