🦀

RustでAPIサーバーを書くのが思ったより良い

2024/06/11に公開

最近いろんなところで採用事例が増えてきたRustですが、Webサービス開発でのAPIサーバーを書くのにRustは向いてないと言われたりします。Rustの第一のターゲットはシステムプログラミングでありGCがないためAPIサーバー開発でシビアなメモリ管理はしたくないというのは一理あるのですが、RustでAPIサーバーの開発を実際にやってみるとむしろ開発体験が結構いいなと感じます。パフォーマンスのために難しい所有権を無理にがんばるみたいなマイナスのイメージはほとんど当てはまらなかったです。

Rustの難しいライフタイム、所有権があまり出てこない

Rustにおいて難しいとされるライフタイム、所有権といった概念があり私も書く前はかなり身構えていたのですが、これに苦しむことは思ったよりも少ないです。その要因はWebサーバーで扱う処理のほとんどがリクエスト・レスポンスモデルでデータの流れが一方向でシンプルかつ短命なライフタイムであることだと思っています。

この図のように関数の引数は user_id: &str のような参照、戻り値は user: UserModel のような実体(なんていうのが適切?)を返す書き方で大体のケースで問題なく、複雑なライフタイムを扱う機会はほとんどありません。関数や構造体にライフタイムパラメータをつけることもあまり無いです。とはいえやはり最初は苦戦するもので、そのフェーズでは .clone() を多用してやり過ごすというのも割り切りとしてアリです。わかってきたら消していけます。
これはRustの最大の特徴であるライフタイムと所有権を駆使する必要がない=Rustを使わなくてもいいという主張と表裏一体とも言えますが、私はRustの一番の難しさにふれることなくRustのメリットを享受できていると感じます。Rustに入門する機会としてまず例えば小さなAWS Lambdaを書くのに試してみるとかはアリだと思います。

Enumがモデリングにとても便利

RustのEnumは代数的データ型(ADT: Algebraic Data Type)や直和型と呼ばれるもので、私は関数型に詳しくないので詳細な説明はできませんがざっくりいうと複数のパターンを取りうる事柄を一つの型にまとめて表現できるものです。他言語でもEnumはありますが列挙子それぞれに値を持つことができます。

/* C言語の例 */
enum cardsuit {
    CLUBS,
    DIAMONDS,
    HEARTS,
    SPADES
};
// RustのEnumは列挙子に値を持てる
enum WebEvent {
    // An `enum` variant may either be `unit-like`,
    PageLoad,
    PageUnload,
    // like tuple structs,
    KeyPress(char),
    Paste(String),
    // or c-like structures.
    Click { x: i64, y: i64 },
}

Rust By Examplesからコード例を拝借しました。このように起こり得るイベント、コマンド、処理の結果やエラーでもEnumとして表しておくと読み手に優しいですしパターンマッチで処理がもれなく網羅的(exhaustive)であることがコンパイル時に保証されます。

Resultやトレイトなどで抽象性高く、高レベルな書き心地

前述のEnumに加えてResultとトレイトなどの言語機能が充実しているのもRustの特徴的なところです。トレイトによってDIのようなこともでき、Resultにthiserrorといったエラーを扱うライブラリを組み合わせるとEnumによる正確さと ? での短いエラーハンドリングを共存できます。
GCがなくメモリを開発者自ら管理するローレベルな所有権・ライフタイムと、Zero cost abstractionを掲げた抽象性の高い言語機能が共存しているのがRustの面白いところです。APIサーバー開発においては最初に挙げたように所有権・ライフタイムはそれほど出てこないために、結果としてはKotlinやSwiftくらいの抽象性の高い書き心地になります。それでいて後述するようにパフォーマンスはC、C++に次ぐくらいのトップレベルの高さというのはとても得をしている気持ちになります。

所有権、ライフタイムはある程度普遍性のある概念で学びがある

RustではGCがなくメモリを管理するための仕組みとして所有権とライフタイムがありますが、これを学んで理解してくるとRustに閉じた概念ではないことに気付きます。いろんなところから読み書きされるデータの整合性を保つにはそれらすべてが読み取りだけを行うか、データのアクセスを1箇所に限定してそこでは読み取りと書き込みをするというのが &Data &mut Data の基本的なアイディアで、これはRust以外の言語でも概ね共通した考え方かなと思います。これをマルチスレッドに拡張すると ArcMutex になってきます。APIサーバーの開発において難しい所有権は出てこないというのは、慣れてくるとデータの流れが意識できて変な所有権・ライフタイムが必要になる設計にしなくなるというのもあります。
所有権とライフタイムはRustの難しいところで学習コストを要求するところですが、その学習コストはRust以外では無駄になるかというとそうでもないのは良い点だと思います。

rustup/cargo, rust-analyzer/clippyが便利

Rustは言語まわりのエコシステムが充実しているのも良い点です。

  • rustup
    • Rustのインストールや複数のバージョン、ビルドターゲットを管理できる
  • cargo
    • パッケージ管理やビルド・テストなどを行う
  • rust-analyzer, clippy
    • LSPを実装したエディタ拡張、よくある間違いを指摘してくれるLinter

こういったツールの充実でRust自体をインストールしてプロジェクトを作ってビルド・テストするまでとてもスムーズです。Rust言語自体の学習コストがある程度高いのはそうだと思いますがツールのインストールなどでつまづくことは無いと思います。Cargoはワークスペース機能をサポートしていることなども便利で、他の言語を触るときCargoくれとなります。rust-analyzer, clippyは特にRust初心者にありがたい存在で指摘からRustを学びつつ書けます。

普通に作ってもパフォーマンスが良い

Rustは他のプログラミング言語と比べたときにC、C++の次くらいに速いのでRust開発したサーバーもあまり労力を費やさなくともそのくらいの速度になると思います。(C/C++ -> Rust -> Go, Javaのようなイメージでいますが違ったらすいません)
これはAWS Lambdaのような実行時間に対して課金されるタイプのサービスでは直接コストの削減になりますしk8sで台数を少なくできるなどほとんどのケースでコスト面でメリットを得られるはずです。実際に別の静的型付け言語からRustに書き換えた際はベンチマークの各指標で少なくとも数十%、高いところでは数倍の改善が見られました。(実際に近いワークロードでベンチマークを行ったので純粋な言語の差異以外もあると思います)

マクロで楽をしようとする文化(?)がある

Rustはマクロがあって、マクロを活用した有名なライブラリだとserdeclapがあります。これらのライブラリを使ってWebアプリケーション開発でよくある処理を短く読みやすく書くことができます。
APIサーバーを書いていて頻出するJSONのハンドリングはserdeがやってくれて、JavaScript(JSON)はcamelCaseでRustはsnake_caseなのも #[serde(rename_all = "camelCase")] と1行書けば良く便利です。

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct User {
    name: String,
    avatar_url: Option<String>,
    #[serde(rename = "type")]
    user_type: String,
    age: i32,
}

serdeは各データフォーマット向けに拡張があって serde_dynamo でdynamodbの読み書きがとても楽でした。
clapはコマンドラインパーサで、私は env アトリビュートをアプリケーションの設定を環境変数から読み込むのに使っていて便利です。

#[derive(Parser)]
struct AppConfig {
    #[arg(env = "PORT")]
    port: u32,
    #[arg(env = "USER_API_URL")]
    user_api_url: String,
}

let app_config = AppConfig::parse();

APIサーバーを開発するエコシステムがそれなりにある

Webサーバー自体はActix WebやAxum、DBとの接続はSeaORMやsqlxなどAPIサーバーを開発する一般的なユースケースに対しては定番だったり選択肢があるくらいの状況にあるように感じました。Railsほどの充実を求めると厳しいのでやりたいことに対して良いライブラリやエコシステムがあるのを確認できたらRustを使うことは十分選択肢に入るのではと思います。

悪いところももちろんある

Rustにしたから全てでハッピーというわけではなく、ビルドの遅さだったり学習コストだったり用途によってはエコシステムが他の言語よりも充実していないなどあるのでそのあたりは調査したうえで使うのがいいと思います。私はAWSでRustを使ったのですがAWS SDKでのRustサポートは十分で特に問題はなく、サーバー自体が小規模だったのもありそこまでビルド時間でも苦しみませんでした(それでも遅さは感じます)。

まとめ

RustでAPIサーバーを書くメリットをいくつか書きましたが、やはりRustの一番難しいライフタイムや所有権といったところがあまり出てこないというのは実際にRustでAPIサーバーを書いてみてわかった発見です。私個人としてはRustでAPIサーバーを書くこと自体にメリットを感じていますし、それに加えてRustの活用が盛んな分野であるWASMやWebフロントエンドのツーリングに入門する足がかりにもなってお得だなと思っています。RustでAPIサーバーおすすめです🦀

Discussion