Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
東京Ruby会議12 Ruby と Rust と私 / Tokyo RubyKaigi 12 ...
Search
Kohei Suzuki
January 18, 2025
Technology
3
2.2k
東京Ruby会議12 Ruby と Rust と私 / Tokyo RubyKaigi 12 Ruby, Rust and me
東京Ruby会議12でのキーノートの発表資料です。
https://regional.rubykaigi.org/tokyo12/
Kohei Suzuki
January 18, 2025
Tweet
Share
More Decks by Kohei Suzuki
See All by Kohei Suzuki
少人数でも運用できるインフラ作り / Operating infrastructure with less effort
eagletmt
1
3k
Cookpad Lounge #4 SRE 座談会 コンテナ中心の構成からサーバーレスへの展望 / From containers to serverless
eagletmt
0
660
Cookpad Tech Kitchen #20 Amazon ECS の安定運用 / Building a steady ECS infrastructure
eagletmt
1
3k
クックパッドでの Webアプリケーション開発 2017 / Web application development in Cookpad 2017
eagletmt
20
10k
ECS を利用したデプロイ環境
eagletmt
12
6.7k
ActiveRecord 3.2 -> 4.1
eagletmt
3
1.8k
クックパッドにおける Rubyの活用
eagletmt
0
490
複数DBとRails
eagletmt
14
7k
R/W Splitting in Rails
eagletmt
2
1.5k
Other Decks in Technology
See All in Technology
OpenID Connect for Identity Assurance の概要と翻訳版のご紹介 / 20250219-BizDay17-OIDC4IDA-Intro
oidfj
0
160
ビジネスモデリング道場 目的と背景
masuda220
PRO
9
410
技術負債の「予兆検知」と「状況異変」のススメ / Technology Dept
i35_267
1
1k
10分で紹介するAmazon Bedrock利用時のセキュリティ対策 / 10-minutes introduction to security measures when using Amazon Bedrock
hideakiaoyagi
0
180
Developer Summit 2025 [14-D-1] Yuki Hattori
yuhattor
19
5.8k
Oracle Cloud Infrastructure:2025年2月度サービス・アップデート
oracle4engineer
PRO
1
140
なぜ私は自分が使わないサービスを作るのか? / Why would I create a service that I would not use?
aiandrox
0
510
組織貢献をするフリーランスエンジニアという生き方
n_takehata
1
1.2k
利用終了したドメイン名の最強終活〜観測環境を育てて、分析・供養している件〜 / The Ultimate End-of-Life Preparation for Discontinued Domain Names
nttcom
1
120
「海外登壇」という 選択肢を与えるために 〜Gophers EX
logica0419
0
640
7日間でハッキングをはじめる本をはじめてみませんか?_ITエンジニア本大賞2025
nomizone
2
1.7k
2.5Dモデルのすべて
yu4u
2
790
Featured
See All Featured
The Invisible Side of Design
smashingmag
299
50k
Mobile First: as difficult as doing things right
swwweet
223
9.3k
GitHub's CSS Performance
jonrohan
1030
460k
Thoughts on Productivity
jonyablonski
69
4.5k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
7
630
Imperfection Machines: The Place of Print at Facebook
scottboms
267
13k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
44
9.4k
Building a Scalable Design System with Sketch
lauravandoore
460
33k
Side Projects
sachag
452
42k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
366
25k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
231
53k
Bootstrapping a Software Product
garrettdimon
PRO
306
110k
Transcript
Ruby と Rust と私 Kohei Suzuki (@eagletmt)
自己紹介 • Kohei Suzuki (@eagletmt) • クックパッド レシピ事業部 バックエンド基盤グループ (SRE)
• 好きなプログラミング言語は Ruby と Rust
アウトライン Ruby と Rust を比較しながら個人の考えや感想を述べていく • CLI を作る上での比較 ◦ Ruby
で実装した CLI の例: hako ◦ Rust で実装した CLI の例: ecs-logs-merger • ISUCON での言語移植 ◦ エラーハンドリング ◦ リソース開放 ◦ Web フレームワーク • まとめ
Ruby: hako
Ruby で実装した CLI の例: hako • hako https://github.com/eagletmt/hako ◦ Amazon
ECS で Web アプリケーションをデプロイしたりバッチジョブを起動し たりするためのツール ◦ Jsonnet という設定言語を採用し、Jsonnet 上で共通化しながら ECS の設定 が可能 ◦ script という機能を持ち、デプロイの前後などに処理を追加できる ▪ script はユーザ側で拡張可能にしたい ▪ 仕事で作ったものを GitHub で公開するためのテクニックという側面もあ る (社内固有の事情は拡張として社内に持つ)
hako script
hako script デプロイ開始時に log driver が awslogs だったら CloudWatch ロググループを作成
hako script
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 を拡張できる
hako script を Rust で? • Ruby では動的に require や
const_get ができるので、プラグイン機構を 作るのが非常に簡単で、拡張性を確保しやすい • 静的型付けの性質が強い言語では同様のプラグイン機構を作るのが難 しい • I/O などを許可しなければ WebAssembly で拡張させるという手法が良さ そうに思う ◦ 例: Envoy proxy • しかし hako script のように I/O 含めて任意の処理を許可したいことも多 い ◦ 個人的には Terraform や Packer などで採用されている hashicorp/go-plugin の手法が答えかなと思っている
hashicorp/go-plugin の機構 • gRPC サーバを起動する実行可能ファイルとしてプラグインを配布 する • メインプロセスはサブプロセスとしてプラグインを起動して stdout 経
由で接続先 (ポート番号や UDS パス) を受け取り、gRPC の呼び 出しでプラグインに処理を移譲する • プラグインは環境変数経由でメインプロセスから証明書を受け取り、 mTLS で通信をセキュアにする • hashicorp/go-plugin は Go 専用だが、この機構は幅広い言語で採 用可能
とはいえ…… • 個人的には拡張可能な CLI を作るなら Ruby を選びそう • hashicorp/go-plugin 方式だとプラグインの配布や配置をどうする
かという課題がある ◦ Terraform の場合は https://registry.terraform.io/ でプラグイン (実行可能ファ イル) をホストし、そこから配布している ◦ Ruby なら通常の gem として配布できる
Rust: ecs-logs-merger
Rust で実装した CLI の例: ecs-logs-merger • クックパッドでは ECS で動かしたアプリのシステムログを Fluentd
経由で Amazon S3 に保存している ◦ システムログ: Rails.logger で出力するようなログを指し、ユーザの行動ログ のようなログではない • 速報性のため、短いインターバルで S3 にログを保存している • ログは2年間保持する • ある程度古いログは参照されることが稀なので、Glacier という容量 単価の安いストレージに移動してコストを抑えたい
Rust で実装した CLI の例: ecs-logs-merger • Glacier に送るときにはオブジェクト数に応じた料金が発生する ◦ 容量に対する料金と比べて、このオブジェクト数に対する料金がかなり割高
• 前述のように短いインターバルで S3 にログを置くと、オブジェクト数 が膨大になる • したがって、細かいオブジェクトをマージして1つのオブジェクトにまと めることができれば全体のコストを抑えられる • => ecs-logs-merger
ecs-logs-merger のイメージ S3 S3 ecs-logs-merger Glacier へ
なぜ Rust? • ecs-logs-merger の役割は gzip 圧縮された JSON Lines ファイル
を S3 からダウンロードしてマージするだけで、Ruby で実装するの も難しくはない • それでも Rust を選んだ理由: ◦ 大量のログを高速に処理する必要があった ◦ Serde が便利だった ◦ (仕事でも使ってみたかった)
並列処理 • ログはとにかくたくさんある • 試運転してみると gzip の inflate/deflate にかなり CPU
を持ってい かれることが分かった ◦ 細かい大量のオブジェクトのダウンロード・アップロードの I/O もあるが、CPU もマルチコアを活かして効率的に処理したい • Ruby で CPU-intensive な処理を並列化するのは難しい ◦ たとえば Fluentd ではマルチプロセス化や gzip を外部コマンドとして実行す るオプションがある https://docs.fluentd.org/deployment/performance-tuning-single-process
並列処理 • Rust ではスレッドを起動すればマルチコアで動く ◦ マルチスレッドプログラミングでミスしやすい箇所の多くは型チェックで防ぐこと ができる • Ruby だと工夫が必要なところが、Rust
では普通に書くだけで達成 できる ◦ 個人的に、「がんばれば速くなる」と「普通に書けば速い」の間の距離は結構大 きいと思っている ◦ Ractor に期待
Serde • 何らかのフォーマットで表現されたデータと Rust のデータ型の間を 変換するためのフレームワーク (SERialization/DEserialization) • たとえば JSON
<-> データ型の変換が簡単かつ柔軟にできる • Derive マクロという機能を活用しており、アノテーションをつけて コード生成する ◦ この手の Derive マクロは広く利用されており、コマンドライン引数をデータ型 にマッピングするライブラリ (clap) などもある
Serde の例
Serde の例 Serde でデシリアライズす るためのマクロ time フィールドを時刻型にデシ リアライズ JSON のフィールド名をリ
ネームしてマッピング
Ruby では? • JSON.parse では Hash が返ってくる ◦ Hash のままだとどんなキーが存在するのかコード上から分からないので、
Data や Struct に入れ直したい • Hash を Data や Struct に入れるのはやや面倒 ◦ 型チェックや存在チェックをしないといけない ◦ ネストを自分で扱わないといけない • Ruby でやや大きめの JSON を扱うときは、自分は Protocol Buffers (protobuf) を利用することが多い
例: Terraform の tfstate を読む
とはいえ…… • protobuf である程度カバーできるとはいえ、Rust の Serde の使い やすさには負ける ◦ たとえば柔軟なリネームができない
◦ protobuf の性質上、存在すべきフィールドを表現できない (Go のゼロ値のよ うな値にデシリアライズされる) ▪ 型が違う場合は decode_json 時に例外 ◦ protoc を拡張してこのへんをがんばることもできるが…… 「がんばればでき る」と「普通に書ける」の距離は大きい
使い分け • 1つのことを上手くやる CLI では、やることに応じて言語を選ぶこと が多い • Ruby を優先したいとき ◦
プラグインで拡張させたい ◦ 実装が十分短い • Rust を優先したいとき ◦ 並列処理でパフォーマンスを優先したい ◦ 難しい JSON を読みたい
ISUCON での言語移植
ISUCON ISUCONとはLINEヤフー株式会社が運営窓口となって開催している、お題となるWeb サービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバト ルです https://isucon.net/ • 複数の言語でお題となる Web アプリが提供され、それを高速化するコンテスト (競
技) • 複数の言語で提供するために、言語移植が必要になる ◦ ここ4回ほど、Ruby や Rust への言語移植を自分が担当している
ISUCON での言語移植 • 近年の ISUCON においては、お題となる Web アプリが出題チー ムにより最初に Go
で実装される • 運営のお手伝いとして言語移植担当が呼ばれ、Go から各言語に 移植する • なるべく Go 実装に近い形での移植が望まれる ◦ できる限り言語間で有利・不利が出ないようにする
エラーハンドリング: Go • Go: error + if ◦ output, err
:= F(input); if err != nil { return ... } の形のエラーハンドリングが 多い https://github.com/isucon/isucon14 より引用
エラーハンドリング: Ruby • Ruby: 例外 ◦ エラーを例外で扱って output = f(input)
の形にする ◦ エラーの中身を調べるときは begin/rescue
エラーハンドリング: Rust • Rust: Result + ? ◦ Rust では
Result 型でエラーを扱うことが多く、? という演算子で if err != nil { return ... } 部分を省略し output = f(input)? の形で書ける
エラーハンドリング: Rust • Result 方式のエラーハンドリングだとエラーの型をどうするかが問 題になりがちだが、最近の Rust では anyhow または
thiserror と いうライブラリを使うことが多いと思う ◦ anyhow: Go の error のように、すべてのエラーを1つの型の値にラップする。 アプリ向き ◦ thiserror: enum で細かくエラーの種類を分けて扱うのをやりやすくする。ライ ブラリ向き • ? 演算子を使ったときにエラー型の変換が自動で行われる (From trait)
エラーハンドリング • 個人的には、Ruby 方式と Rust 方式の間に大きな好みの差は無い • 一般には Result 型だと面倒になりがちだが、?
演算子と anyhow/thiserror でかなり記述を省略できる
リソース開放: Go • Go: defer ◦ file, _ := os.Open(...);
defer file.Close() とすると関数を抜けるときにファイル クローズのようなリソース開放を漏れなく実行できる https://github.com/isucon/isucon14 より引用
リソース開放: Ruby • Ruby: ブロック (ensure) ◦ File.open(...) { ...
} と書けるよう にして、ブロックを抜けるときに漏 れなくリソースを開放できるよう にしている API が多い ◦ File.open の中では ensure を 使って漏れなくリソースを開放す るよう実装する
リソース開放: Rust • Rust: Drop (RAII) ◦ let file =
std::fs::File::open(...)? と書くと、file 変数のスコープを抜けるときに リソース開放を漏れなく実行できる ◦ これは std::fs::File が Drop trait を実装していて、そこでファイルをクローズす るよう実装されているためである
リソース開放: Rust スコープを抜けるときに rollback https://github.com/launchbadge/sqlx より引用
リソース開放 • 個人的には、Ruby 方式と Rust 方式の間に大きな好みの差は無い • どのリソース開放が行われそうか分かりやすくなる点では Ruby 方
式のほうが好き • ネストが深くならない点では Rust 方式のほうが好き
Web フレームワーク: Ruby • Ruby では Sinatra + mysql2 が基本
• かなり記述量が抑えられる点が好き ◦ おおよそ Go 実装の半分くらいのコード量で済む ◦ struct を定義しない (しづらい) ことで減っている分もあるけど…… • GET リクエストのクエリ文字列や POST リクエストの JSON をデシ リアライズするときが若干扱いづらい ◦ とくに JSON の中でネストがあると存在チェックや型チェックが面倒
リクエストボディを Data に入れる
リクエストボディを Data に入れる
Web フレームワーク: Rust • Rust ではここ2年間は Axum + SQLx にしている
• GET リクエストのクエリ文字列や POST リクエストの JSON を Serde でデシリアライズできるのはかなり楽 • SQLx でも Serde と同じように MySQL の1行を Rust のデータ型 にデシリアライズする Derive マクロが用意されている
リクエストボディを struct に入れる
リクエストボディを struct に入れる
SQLx で MySQL の1行をデータ型にマッピング
ISUCON では • Ruby と Rust で書き方すごく大きな差があるようには感じていない ◦ JSON や
MySQL の行に型がついて Rust のほうがちょっと嬉しい、くらい ◦ 言語移植の観点では、Rust のほうが凡ミスをしにくいので、最初に Rust に移 植してコード理解を深めた後に Ruby に移植するようにしている • 8時間の競技であることを考えると、Rust のコンパイルの遅さは結 構たいへんそう ◦ 本番サーバだと c5.large (2 vCPU、4 GiB memory) なのでリリースビルドは 激重
まとめ
まとめ • Ruby と Rust は型付けの面などで正反対に語られることも多いけ ど、結構近いものがあるように感じている ◦ 実行時かコンパイル時かの差はあるけど、どちらもコードを短く書くための機 能が豊富
▪ Rust 側はライフタイムが絡むとコードが長くなりがちではあるが…… ◦ if や case/when・match が文ではなく式な点も共通 • YJIT や rb-sys などで Rust が Ruby 界隈に受け入れられつつあ る? ので、用途に応じて使い分けていくのがいいと思う