Upgrade to Pro — share decks privately, control downloads, hide ads and more …

東京Ruby会議12 Ruby と Rust と私 / Tokyo RubyKaigi 12 ...

東京Ruby会議12 Ruby と Rust と私 / Tokyo RubyKaigi 12 Ruby, Rust and me

東京Ruby会議12でのキーノートの発表資料です。
https://regional.rubykaigi.org/tokyo12/

Kohei Suzuki

January 18, 2025
Tweet

More Decks by Kohei Suzuki

Other Decks in Technology

Transcript

  1. アウトライン Ruby と Rust を比較しながら個人の考えや感想を述べていく • CLI を作る上での比較 ◦ Ruby

    で実装した CLI の例: hako ◦ Rust で実装した CLI の例: ecs-logs-merger • ISUCON での言語移植 ◦ エラーハンドリング ◦ リソース開放 ◦ Web フレームワーク • まとめ
  2. Ruby で実装した CLI の例: hako • hako https://github.com/eagletmt/hako ◦ Amazon

    ECS で Web アプリケーションをデプロイしたりバッチジョブを起動し たりするためのツール ◦ Jsonnet という設定言語を採用し、Jsonnet 上で共通化しながら ECS の設定 が可能 ◦ script という機能を持ち、デプロイの前後などに処理を追加できる ▪ script はユーザ側で拡張可能にしたい ▪ 仕事で作ったものを GitHub で公開するためのテクニックという側面もあ る (社内固有の事情は拡張として社内に持つ)
  3. hako script の実装 • Jsonnet 上で type に指定された文字列を使って require する

    ◦ scripts: [{ type: 'foo_bar' }] であれば require 'hako/scripts/foo_bar' する • その require によって期待するクラスが定義されたと仮定して const_get して new する ◦ scripts: [{ type: 'foo_bar', hoge: 'fuga' }] であれば Hako::Scripts.const_get('FooBar').new({ hoge: 'fuga' }) する • この慣習に従うよう Hako::Scripts::FooBar を実装して gem にする ことで hako script を拡張できる
  4. hako script を Rust で? • Ruby では動的に require や

    const_get ができるので、プラグイン機構を 作るのが非常に簡単で、拡張性を確保しやすい • 静的型付けの性質が強い言語では同様のプラグイン機構を作るのが難 しい • I/O などを許可しなければ WebAssembly で拡張させるという手法が良さ そうに思う ◦ 例: Envoy proxy • しかし hako script のように I/O 含めて任意の処理を許可したいことも多 い ◦ 個人的には Terraform や Packer などで採用されている hashicorp/go-plugin の手法が答えかなと思っている
  5. hashicorp/go-plugin の機構 • gRPC サーバを起動する実行可能ファイルとしてプラグインを配布 する • メインプロセスはサブプロセスとしてプラグインを起動して stdout 経

    由で接続先 (ポート番号や UDS パス) を受け取り、gRPC の呼び 出しでプラグインに処理を移譲する • プラグインは環境変数経由でメインプロセスから証明書を受け取り、 mTLS で通信をセキュアにする • hashicorp/go-plugin は Go 専用だが、この機構は幅広い言語で採 用可能
  6. とはいえ…… • 個人的には拡張可能な CLI を作るなら Ruby を選びそう • hashicorp/go-plugin 方式だとプラグインの配布や配置をどうする

    かという課題がある ◦ Terraform の場合は https://registry.terraform.io/ でプラグイン (実行可能ファ イル) をホストし、そこから配布している ◦ Ruby なら通常の gem として配布できる
  7. Rust で実装した CLI の例: ecs-logs-merger • クックパッドでは ECS で動かしたアプリのシステムログを Fluentd

    経由で Amazon S3 に保存している ◦ システムログ: Rails.logger で出力するようなログを指し、ユーザの行動ログ のようなログではない • 速報性のため、短いインターバルで S3 にログを保存している • ログは2年間保持する • ある程度古いログは参照されることが稀なので、Glacier という容量 単価の安いストレージに移動してコストを抑えたい
  8. Rust で実装した CLI の例: ecs-logs-merger • Glacier に送るときにはオブジェクト数に応じた料金が発生する ◦ 容量に対する料金と比べて、このオブジェクト数に対する料金がかなり割高

    • 前述のように短いインターバルで S3 にログを置くと、オブジェクト数 が膨大になる • したがって、細かいオブジェクトをマージして1つのオブジェクトにまと めることができれば全体のコストを抑えられる • => ecs-logs-merger
  9. なぜ Rust? • ecs-logs-merger の役割は gzip 圧縮された JSON Lines ファイル

    を S3 からダウンロードしてマージするだけで、Ruby で実装するの も難しくはない • それでも Rust を選んだ理由: ◦ 大量のログを高速に処理する必要があった ◦ Serde が便利だった ◦ (仕事でも使ってみたかった)
  10. 並列処理 • ログはとにかくたくさんある • 試運転してみると gzip の inflate/deflate にかなり CPU

    を持ってい かれることが分かった ◦ 細かい大量のオブジェクトのダウンロード・アップロードの I/O もあるが、CPU もマルチコアを活かして効率的に処理したい • Ruby で CPU-intensive な処理を並列化するのは難しい ◦ たとえば Fluentd ではマルチプロセス化や gzip を外部コマンドとして実行す るオプションがある https://docs.fluentd.org/deployment/performance-tuning-single-process
  11. 並列処理 • Rust ではスレッドを起動すればマルチコアで動く ◦ マルチスレッドプログラミングでミスしやすい箇所の多くは型チェックで防ぐこと ができる • Ruby だと工夫が必要なところが、Rust

    では普通に書くだけで達成 できる ◦ 個人的に、「がんばれば速くなる」と「普通に書けば速い」の間の距離は結構大 きいと思っている ◦ Ractor に期待
  12. Serde • 何らかのフォーマットで表現されたデータと Rust のデータ型の間を 変換するためのフレームワーク (SERialization/DEserialization) • たとえば JSON

    <-> データ型の変換が簡単かつ柔軟にできる • Derive マクロという機能を活用しており、アノテーションをつけて コード生成する ◦ この手の Derive マクロは広く利用されており、コマンドライン引数をデータ型 にマッピングするライブラリ (clap) などもある
  13. Ruby では? • JSON.parse では Hash が返ってくる ◦ Hash のままだとどんなキーが存在するのかコード上から分からないので、

    Data や Struct に入れ直したい • Hash を Data や Struct に入れるのはやや面倒 ◦ 型チェックや存在チェックをしないといけない ◦ ネストを自分で扱わないといけない • Ruby でやや大きめの JSON を扱うときは、自分は Protocol Buffers (protobuf) を利用することが多い
  14. とはいえ…… • protobuf である程度カバーできるとはいえ、Rust の Serde の使い やすさには負ける ◦ たとえば柔軟なリネームができない

    ◦ protobuf の性質上、存在すべきフィールドを表現できない (Go のゼロ値のよ うな値にデシリアライズされる) ▪ 型が違う場合は decode_json 時に例外 ◦ protoc を拡張してこのへんをがんばることもできるが…… 「がんばればでき る」と「普通に書ける」の距離は大きい
  15. 使い分け • 1つのことを上手くやる CLI では、やることに応じて言語を選ぶこと が多い • Ruby を優先したいとき ◦

    プラグインで拡張させたい ◦ 実装が十分短い • Rust を優先したいとき ◦ 並列処理でパフォーマンスを優先したい ◦ 難しい JSON を読みたい
  16. ISUCON での言語移植 • 近年の ISUCON においては、お題となる Web アプリが出題チー ムにより最初に Go

    で実装される • 運営のお手伝いとして言語移植担当が呼ばれ、Go から各言語に 移植する • なるべく Go 実装に近い形での移植が望まれる ◦ できる限り言語間で有利・不利が出ないようにする
  17. エラーハンドリング: Go • Go: error + if ◦ output, err

    := F(input); if err != nil { return ... } の形のエラーハンドリングが 多い https://github.com/isucon/isucon14 より引用
  18. エラーハンドリング: Ruby • Ruby: 例外 ◦ エラーを例外で扱って output = f(input)

    の形にする ◦ エラーの中身を調べるときは begin/rescue
  19. エラーハンドリング: Rust • Rust: Result + ? ◦ Rust では

    Result 型でエラーを扱うことが多く、? という演算子で if err != nil { return ... } 部分を省略し output = f(input)? の形で書ける
  20. エラーハンドリング: Rust • Result 方式のエラーハンドリングだとエラーの型をどうするかが問 題になりがちだが、最近の Rust では anyhow または

    thiserror と いうライブラリを使うことが多いと思う ◦ anyhow: Go の error のように、すべてのエラーを1つの型の値にラップする。 アプリ向き ◦ thiserror: enum で細かくエラーの種類を分けて扱うのをやりやすくする。ライ ブラリ向き • ? 演算子を使ったときにエラー型の変換が自動で行われる (From trait)
  21. リソース開放: Go • Go: defer ◦ file, _ := os.Open(...);

    defer file.Close() とすると関数を抜けるときにファイル クローズのようなリソース開放を漏れなく実行できる https://github.com/isucon/isucon14 より引用
  22. リソース開放: Ruby • Ruby: ブロック (ensure) ◦ File.open(...) { ...

    } と書けるよう にして、ブロックを抜けるときに漏 れなくリソースを開放できるよう にしている API が多い ◦ File.open の中では ensure を 使って漏れなくリソースを開放す るよう実装する
  23. リソース開放: Rust • Rust: Drop (RAII) ◦ let file =

    std::fs::File::open(...)? と書くと、file 変数のスコープを抜けるときに リソース開放を漏れなく実行できる ◦ これは std::fs::File が Drop trait を実装していて、そこでファイルをクローズす るよう実装されているためである
  24. Web フレームワーク: Ruby • Ruby では Sinatra + mysql2 が基本

    • かなり記述量が抑えられる点が好き ◦ おおよそ Go 実装の半分くらいのコード量で済む ◦ struct を定義しない (しづらい) ことで減っている分もあるけど…… • GET リクエストのクエリ文字列や POST リクエストの JSON をデシ リアライズするときが若干扱いづらい ◦ とくに JSON の中でネストがあると存在チェックや型チェックが面倒
  25. Web フレームワーク: Rust • Rust ではここ2年間は Axum + SQLx にしている

    • GET リクエストのクエリ文字列や POST リクエストの JSON を Serde でデシリアライズできるのはかなり楽 • SQLx でも Serde と同じように MySQL の1行を Rust のデータ型 にデシリアライズする Derive マクロが用意されている
  26. ISUCON では • Ruby と Rust で書き方すごく大きな差があるようには感じていない ◦ JSON や

    MySQL の行に型がついて Rust のほうがちょっと嬉しい、くらい ◦ 言語移植の観点では、Rust のほうが凡ミスをしにくいので、最初に Rust に移 植してコード理解を深めた後に Ruby に移植するようにしている • 8時間の競技であることを考えると、Rust のコンパイルの遅さは結 構たいへんそう ◦ 本番サーバだと c5.large (2 vCPU、4 GiB memory) なのでリリースビルドは 激重
  27. まとめ • Ruby と Rust は型付けの面などで正反対に語られることも多いけ ど、結構近いものがあるように感じている ◦ 実行時かコンパイル時かの差はあるけど、どちらもコードを短く書くための機 能が豊富

    ▪ Rust 側はライフタイムが絡むとコードが長くなりがちではあるが…… ◦ if や case/when・match が文ではなく式な点も共通 • YJIT や rb-sys などで Rust が Ruby 界隈に受け入れられつつあ る? ので、用途に応じて使い分けていくのがいいと思う