Rust本番投入におけるハードルの超え方

小林秀和氏(以下、小林):おはようございます。走ってきて心拍数がバグバグです。KOBA789と言います。

一番最初に質問なんですが、ふだんスクリプト言語を書いてるという方はどれぐらいいらっしゃいます?

(会場挙手)

よかった。まぁまぁいた。逆にCないしC++書いてるという方? あるいはアセンブラとかでもぜんぜんいいんですけど、メモリが見える言語を触ってる方?

(会場挙手)

けっこういらっしゃる。よかった。じゃあ、その中で、Rustを本番投入しているという方?

(会場挙手)

前のほう(登壇者席)がそうなのは、そうですよね。これで本番投入してない人がしゃべりに来ていたら、信憑性もくそもないですからね。

(会場笑)

じゃあ、人間を説得するのに苦労しているという方?

(会場笑)

案外いない。よかった。よし、今日は人間を説得する話はしないので、よろしくお願いします。

ということで、自己紹介です。

クックパッド株式会社のインフラストラクチャー部・データ基盤グループというところから来ました。サーバサイドエンジニアと言ってるんですが、とりあえずなんでもやることになっています。

2017年2月に大学を中退して入社しまして、もともとはJavaScriptというかNode.jsの人でした。もうJSで13年ぐらい書いています。プッシュ通知の配信基盤を作るということで、去年、Rustを本番に投入しました、という流れです。

今日はモヒカンでいくので、「なんかすげえ高圧的だな」とか「こいつ煽ってんな」「若いくせになに言ってんだ」とか思っても、そこは腹の中でぐっと堪えていただいて。

適当なタイミングで写真撮られてネットに上げられてもエクスキューズができるように、「偉そうに喋っています」って常に左上に出しています。

(会場笑)

今日話さないのは、さっき言ったとおり、人間の説得。人間の説得って時間を使うんですよ。説得専門の説得屋さんになりたいならそれでいいんですが、そうじゃないんだったら別に棟に引っ越したほうがお得ですよと。つまり、ビズリーチさんに相談でもして引っ越したほうがいいと、そんな感じになるわけですね。

ただし、今日の話を聞いて多少の説得力がつくかもしれないというのはありますが、保証はできかねます。あと言語仕様に関しては、もっと詳しい方が並んでいるので、その方に聞いていただくとして。細かいツールは、もう半年経つと全部変わっちゃうし、ケースバイケースだし、そこはもう今日は話しません。

今日話すことは、「Rustの本番投入のハードルはなにか」という話と、そのハードルをどのように超えるかという話をします。ぜひ、ハードルを明らかにしていくので、みなさん導入を諦めてください。

なぜRustを採用したのか?

ではまず、Rustをなぜ使いたいのか? これはけっこう大事ですよね。なんで使いたいのか? 「なんでGoを使わないでRust使うの?」っていうのがあるわけですよ。

いろいろあって。性能とか安全性とか生産性とか、趣味とか採用広報とか、いろいろあります。

参加者:趣味が一番でかいかな。

小林:趣味が一番でかいのは当然で、俺が採用したのも趣味だからですね。実際、会社で導入するときにも、1人プロジェクトだったんですけど、上司に黙ってRustで書き始めて、「すいません。もう300行ぐらいあるんですよね……」みたいな。

参加者:けっこう少ない(笑)。

小林:「300行なら巻き戻せるね」みたいなことを言って、「じゃあ気が向いたらJavaで書き直して」みたいなことを言われて、最後までRustでいって。最後、上司に「レビューお願いします」って、RustのdocのURLを貼り付けてレビューを投げると。

そうすると、なぜかうちの上司は頭が狂っているので、そのままレビューをしてくださるという。たいへんすばらしい人の下で働けているなという感じですね。

CookpadでRustが使えた理由

じゃあ、なぜRustを使えたのかという話をします。まず1つ、これは無視できない部分として、弊社のコンテナ基盤の話があります。うちの会社だと、エンジニアなら誰でも、1枚定義ファイルを書いて、リポジトリでPRを投げるとデプロイできる、Hakoというコンテナ基盤があるんですね。

これ、クックパッドのカンファレンスでも散々しゃべられているので、気になる人は調べてください。私の尊敬している超すごい人がサクっと作って本番に乗っけてしまった、やばいやつがあります。Dockerで即ドーンって話ですね。

原理的には任意の物体が全部動けばいいんです。Linuxの上でね。Linuxじゃなくても、Dockerが動くところだったら、動くという感じになってるんですけど。

でも、これだけで運用可能になるわけじゃないじゃないですか。みなさんわかってると思いますが、そんなこと言ったら、「じゃあここにx86のCPUがついているマシンがあるから、この上でバイナリをドーンってしたら運用可能じゃん?」と言ってるのとあんまり変わらないので、そうじゃない。

Dockerでドーンぐらい今どきどの会社でもできる。なにが問題か? Rust採用の難しさ、ここをちゃんと解き明かしていきましょう。

Rustが新しいが故に生じる難しさ

Rust採用の難しさは2つあります。

Rust特有の難しさは、さっき言っていたライフタイムとかトレイトが面倒だとか、そもそも生のメモリに触るようなコードを書いたことがない人間にとってはけっこうだるいとか、いろいろあるわけです。なんですがもう1つ、新しいがゆえの難しさというのもあるわけです。

今日、左半分は話しません。これはWebで検索すれば出てくるので、そっちはやってください。新しいがゆえの難しさというのがなにかという話を掘り下げていきます。

なんで新しいと難しいのかというと、真似で誤魔化せないからなんですよね。みなさん、今日、もしかしたら誰かの真似をしようと思って来てるかもしれないわけですよ。うまくいった人の話を聞いて、「じゃああれを真似してうまくやってみよう」みたいな感じになっているかもしれない。

ですが、事例もまだ数えるほどしかなくて、まだまだ真似だけじゃどうにもならないフェーズなわけです。それこそ俺みたいな説教くさいだけの話をしている人間の話を聞いてもなんの役にも立たないと。

じゃあ、その中でどうやってRustを本番投入していきましょうかという話をこれからしていきます。

慣れているシステム・言語との違いを認識する

「じゃあ真似で誤魔化せないときどうするの?」というと、基礎や原理に頼るしかない。もう、愚直にやっていきましょうと。己の筋肉を信じてやっていくしかない。

例えば、やり方としては「慣れているシステム・言語との違いを認識するというところから手をつけていきましょう」というのがわりとありがちなところです。

うちの会社は、わりと知っているかもしれませんが、RubyとRailsの会社で、超巨大なRailsアプリケーションを運用している知見はわりとあるし、Rubyに詳しい人もたくさんいる会社です。ですが、Rustに詳しい人間は俺を含めて数人しかいませんでした。

その中でRustを導入しようと言っているので、とりあえず今ノウハウのあるRubyとRustの違いを明らかにして、今の我々になにが足りていないのかを明らかにするということをやりました。

すると、「ランタイムほぼなしで動くってどういうことだろう?」とか「本番でクラッシュしたときはコアダンプを読むかもしれないね」とか、当然、CRubyもたまにセグフォでバンッつって入って死ぬことあるので、それを読んでいる人はいるんですけど。

でも、そういうCRubyの開発をやっているような人間がコアを読むのと、末端のソフトウェアを開発している人間が自分のソフトウェアがぶっ壊れたときにコアを読むのって、やっぱりぜんぜん違うわけですよ。なので、その技術というかスキルは必要だよね、みたいな話です。

あと、実行時にCPUやOSを隠蔽しないわけです。まぁ「隠蔽してるってどういうレベルの話なの?」というのもあるんですけど、隠蔽していません。

ここで何回このシステムコールをこういうオプションで叩いているんだな、というのがわからないとどうにもならないデバッグが発生するかもしれない。そうしたときに、トラブルシューティングするときにちゃんとそのへんの知識がないとできませよね。

スクリプト言語の経験しかないと、ここはハードルになりうる。

けど、CとかC++とかでデーモンを書いてゴリゴリやっている人は、ここはわりとサクッといけるんじゃないかなと思います。

運用上必要な機能を整備する

あともう1つ、運用上必要な機能を整備する。実際やっていくなかで、リアルワールドのソフトウェアって、「書いた、動いたから、デプロイ」とはならないという話をさっきしたと思うんですけど。動かないというか動くんですけど、「困った、トラブったときにじゃあどうやってそれを調査するの?」とか、そういうリアルワールドの話がたくさん絡んできます。

例えば、私が書いたようなプッシュ通知の送信基盤。これってつまるところネットワークプロトコルをしゃべるデーモンなんですけど、この場合にはロギングけっこう重要です。

長い時間動いているものなので、それを時系列で追いかけて様子を観察があとからできる必要がある。問題が起きたときに「あれ、この時ってなにが起きていたの?」という調査は当然必要です。だから、アプリケーションログのロギングは必要。

あともう1つは、似たよう話ですが、エラーハンドリングとトレーサビリティについて。Rustの標準の機能だけでざっくりエラーハンドリング書くと、それでもいいんですけど、エラーのバブリングに関する情報ってぜんぜん残らないんですよね。

ここで起きたエラーを、なんだろう、matchなり、tryなり、「?」マークのマクロなりでスッとばらして、もう1回上のレイヤーのエラーの中に包んでとか、手でやっていくとどこかで絶対忘れて失っちゃうんですよ。丁寧にやっていけばいけるんですけど、失うから、そのへんはいいライブラリを見つけてちゃんと使ったほうがいいよねということがあります。

そうだ、言うのを忘れていたんですが、ロギングに関してはいいライブラリがなかったので自分で自作しました。Fluentdにログを流すやつを。これもいろいろ詳しい事情があるので、あとで懇親会でも聞いてください。

面倒くさい話としては、Graceful Shutdown。これは面倒くさいですね。

必要なら実装を考えないといけないので、「どういう方針でGraceful Shutdownしようかな?」みたいな。スレッドがたくさん立っていて、それらがすごく複雑にチャネルでつながっていたりするときに、どの順番でそのスレッドを終了させていくのかを考えなきゃいけないとか。

あとシグナルのハンドリング。これは本当に面倒くさいんですけど、システムコールでブロックしているときに、SIGINT飛んできたらどうしようとか、そうした細かいことを考えなければいけません。まあ困ったら、Linuxプログラミングの教科書をおもむろに取り出すと、わりと解決します。

フレームワークのデファクトスタンダードがない

あと、わりと困りがちなんですけど、フレームワークのデファクトスタンダードがありません。ライブラリを作るときは、Cから呼び出すやつとかCのライブラリを満たすやつを書くときはいいんですが、今みたいなネットワークプロトコルをしゃべるやつを作ろうとすると、ある程度フレームワークは欲しいなって気持ちになるわけですよ。なんですけど、デファクトスタンダードがありません。

つまりそれはどういうことかというと、必要なシステムの設計をゼロから書き起こす必要があるという話です。「じゃあシステムの設計作るときにどうすればいいの?」という話をすると、これも丁寧にやるしかない。なので、設計のゴールを決める。

要は、パフォーマンスにどこまで振らなきゃいけないのかとか、メンテナンスにどこまで振らなきゃいけないのかとか。つまり、DIするといっても、staticにDIするのと、ランタイムにダイナミックにトレイトオブジェクトを使ってDIするのとか、いろいろあるわけですよね。なので、そのへんをトレードオフをちゃんと自分で選択していくというのがけっこう重要です。

あと、自分の場合はあったんですけど、スレッドとイベントループの使い分け。

自分が作った物体はけっこうややこしくて、スレッドプールが2つあって、イベントループが3つ回ってるみたいな意味不明な感じになっていたので、そのへんをどうやって使い分けて構成するかを考えたりしました。

コンピュータサイエンスなどの知識が味方になる

ということでまとめに近づいてくるんですけど、コンピュータサイエンスやソフトウェアエンジニアリングの普遍的な知識が味方になるなというのが、当たり前の結論なわけですね。まぁ俺は大学中退なんですけど。

ここまで聞いてみなさん諦めはつきましたでしょうか? なにも難しくないじゃん?

ということで、まとめです。こういう感じになります。

ご清聴ありがとうございました。

(会場拍手)

エラーハンドリングについて

司会者:小林さんありがとうございました。質問がある方は挙手お願いいたします。

質問者1:エラーハンドリングはどうされたんですか?

小林:エラーハンドリングは、もう脳死状態で、とくに理由のないところはerror-chainでがんばってぐるぐるぐるってあげて。あとはスレッドの中に処理が閉じ込められちゃってるところは、もう面倒くさいからそのまま.expectでアンラップしてしまって、スレッドごと殺しちゃう。スレッドプールを監視しているやつでもう1回スレッドを立て直してあげる、みたいなことをやりました。

司会者:ほかに質問ございますか?

質問者2:Graceful Shutdownはどうやって実装しました?

小林:Graceful Shutdown、ぜんぜん魔法みたいなことはなくて、チャネルの中に「もうお前は死んでいいよ」みたいなメッセージを流して、それを受け取ったやつは自分自身でスレッドを閉じる。そうするとスレッドマネージャーが「こいつは正常終了したな」って1人ずつ正常終了を検出していって、最後にバタッときれいに死ぬみたいなのを丁寧にやりました。

質問者2:ありがとうございます。

司会者:ほかにありますか?

質問者3:今、実際に運用しているんですか?

小林:ある程度。そんなにワークロードが超ヘビーってほどは動いていないです。

質問者3:ちなみにログシステムちゃんと動いていますか?

小林:ログシステムは、まぁ、動いています。

質問者3:ありがとうございます。

小林:はい。

質問者1:動いているんだったらもう1回質問いいですか? さっき言い忘れてしまったんですが、ほかの言語、とくにJVM言語とかGolangによくあるのは、動いているデーモンに対して、なぜかプロファイルができるみたいな。

Rustだと、Cもそうなんですけど、わりと厳しいんですよね。まぁgdbにしてもいいんですけど、そういうことはありませんでした? とくにパフォーマンスがすごい重要なシステムで。

小林:そうですね。

質問者1:なにかうまい方法はありますか?

小林:うまい方法はあんまりなくて、メトリクスを定点観測したい部分に関しては、自分がメトリクスが取れるのを仕込んじゃてるという感じになっています。

質問者1:やっぱそうなりますよね。

小林:それで取れない部分に関して本番で発覚したら、もう気合でgdbアタッチするか、諦めて、似たような状態を手元で再現して眺めるかみたいな感じになっていますね。

質問者1:ありがとうございます。

司会者:では、いったんここで質問は締め切らせていただきます。質問がある方はお後の時間でよろしくお願いします。みなさま、あらためて小林さんに盛大な拍手をよろしくお願いします。小林さん、ありがとうございました。

(会場拍手)