なぜRubyの会社でRustを使うのか 小林秀和氏：本日はCookpad TechConfにお越しいただき、ありがとうございます。私の発表は、今話題のRustについてです。みなさんはRustを知っていますか？ あるいは書いたことがありますか？ 書いたことがあるという方、挙手をお願いしてもよろしいでしょうか？ （会場挙手） ちらほらいますね。ありがとうございます。ご覧のとおり、Rustはまだまだマイナーな言語です。そんなRustがクックパッドでなぜ・どのように活用されているのかをご紹介します。 あらためまして自己紹介いたします。本名は小林秀和で、ハンドルネームを「KOBA789」といいます。ふだんはKOBA789名義で活動をしており、社内でも「KOBA」と呼ばれています。 私は昨年、新卒として入社しました。クックパッドで仕事を始めてからおよそ1年になります。昨年の5月に、1ヶ月の研修期間のあと、今のチームに配属されました。 さて、話を始める前に、このタイトルについて解説をしましょう。クックパッドはご存じのとおり、Rubyの会社です。多くのサービスがRails、つまりRubyで書かれています。しかし、だからといってすべてのソフトウェアをRubyで書いているわけではありません。ミドルウェアには、GoやJavaといった言語で書かれているものもあります。 クックパッドには、Dockerにパッケージングしたアプリケーションを簡単にデプロイできる「Hako」という仕組みがあり、どんな言語で書いたソフトウェアでも同じようにデプロイをして運用できるようになっています。ですから、Rustも例外ではありません。たまたま私が初めてだったというだけで、Rustを選択したこと自体は特別なことではなかったのです。

プッシュ通知の配信を一手に担う基盤の改善 つまるところ、クックパッドはRubyの会社ではありますが、Rubyだけの会社ではありません。私は新卒1年目からRuby以外の言語を書いて仕事をしたわけですが、適材適所で言語を使い分けるということを、技術的にも文化的にもできる環境が整っています。 そんな中で私が配属直後に任された仕事は、プッシュ通知配信基盤を改善することでした。プッシュ通知配信基盤について説明する前に、まずクックパッドにおけるプッシュ通知について少し説明します。 クックパッドのサービスでは、毎日たくさんのプッシュ通知を配信しています。プッシュ通知とひと口に言っても、大きく分けて2つの種類があります。 1つが都度配信です。こちらはイベント発生ごとに、ユーザーにそのイベントを文字通り通知するためのプッシュ通知です。 もう1つは一斉配信です。これはなんらかのデータによってターゲティングされたユーザーに一斉に配信されるものです。例えば、最近アプリを使っていないユーザーに対してプッシュ通知を送ることで利用を促す、という用途がこちらに該当します。 このような通知の場合、ターゲティングした対象ユーザーが実際にスマートフォンを使っているユーザーかどうか、また、通知を受信する設定になっているかどうかを事前に見積もることがとても大切です。そして、これらのプッシュ通知の配信を一手に担っているのが、プッシュ通知配信基盤です。

サービスの発展にともなってシステムの改良が必要に 私がRustで書き直す前のプッシュ通知配信基盤はこのような構成でした。 アプリケーションがユーザーに通知する流れを、順を追って説明していきます。 アプリケーションはまずMySQLにある受信設定のテーブルをクエリし、受信拒否されていなければ次にARNのテーブルをクエリし、そしてメッセージをプッシュ通知基盤のS3に書き込みます。一方、プッシュ通知配信基盤はS3で書かれたメッセージを読み出してAmazon SNSに送り出します。 ちなみにARNというのは、Amazon SNSのエンドポイントのARNのことです。この発表では、単にプッシュ通知の宛先デバイスを指定するためのIDだと思ってください。 図がごちゃごちゃしているので、少しまとめましょう。プッシュ通知を配信するまでの流れは次のとおりでした。 受信設定をクエリ、ARNのクエリ、S3に書き込む、S3から読み出す、そして最後にSNSへ送信です。 しかし、S3の読み書きは本質的な処理ではないので省略します。すると、プッシュ通知に必要な処理はわずか3ステップしかないことがわかります。そのうち基盤はSNSへの送信をしているだけで、ほとんどの部分はアプリケーションが担っているのです。 これがまさにプッシュ通知配信基盤を改善しなければならない理由でした。 なぜ、基盤というにはあまりに機能が少なすぎる設計なのか？ また、なぜ今になってこの設計が問題になっているのか？ それには、クックパッドのソフトウェアの歴史が大きく関係しています。

マイクロサービス化でロジックが散らばってしまっていた みなさんご存じのとおり、かつてクックパッドは、大きなMonolithic Rails Applicationでした。現在では、中央にまだまだ巨大なRailsアプリケーションがそびえ立っていることに変わりはないものの、マイクロサービス化が進んでいます。 細かな機能は巨大なRailsアプリケーションを離れ、独立したアプリケーションとして実装されるようになりました。その結果、プッシュ通知を配信するアプリケーションが1つではなくなってしまったのです。 プッシュ通知を配信するアプリケーションが1つしかなかった時代は、基盤の機能が貧弱でも問題になりませんでした。コードがすべて共有されていますから、基盤の足りない部分も1つのアプリケーションだけでカバーすることができたのです。 しかし現在は違います。さまざまなマイクロサービスがプッシュ通知の配信をしようとします。場合によっては、そのアプリケーションの実装言語はRubyではないかもしれません。実際、AWS Lambdaを用いてJavaScriptで実装されているマイクロサービスもあります。 こうなると、プッシュ通知のロジックはさまざまなところに散らばってしまいます。ましてや、受信設定やARNを管理しているデータベースを共有することは避けたい。そのため、より豊富な機能を持つプッシュ通知配信基盤が求められていました。 もう一度、基盤に必要な機能を整理するため、操作を振り返りましょう。プッシュ通知を行うときの処理は、受信設定をクエリ、ARNをクエリ、SNSに送信の3つでした。新しいプッシュ通知配信基盤では、当然これら3つの処理をすべて担います。 これは言い換えると、ARN指定ではなくuser_id指定で送信ができ、また、ユーザーの受信設定に基づいて自動的に配信先ユーザーをフィルタできるということです。 こうしてプッシュ通知に必要なすべての処理を基板で巻き取ったことで、アプリケーションはロジックをコピペすることもデータベースを共有することもなく、user_idとメッセージさえ放りこめばプッシュ通知を配信できるようになりました。

速いソフトウェアを作るのはRustではなくプログラマの仕事 しかし、これだけでは機能は足りません。このスライドを覚えているでしょうか？ 一斉配信では配信対象者の見積もりが大切でした。見積もりするためだけにデータベースに直接つないでクエリを投げなければならないのでは、せっかくデータベースの共有を防いだことが台無しになってしまいます。 そこで、数百万通規模の配信でも数分で完了するほどの極めて高速な「dry-run」を実装しました。このように、性能要件の厳しいソフトウェアをRubyで実装するのは大変です。Rubyにはもっと適切な使い方があります。 一方で、Rustは速度・安全性・並行性にフォーカスしたシステムプログラミング言語です。Rubyのように派手なメタプログラミングをできる柔軟性はありません。しかし、Rubyよりも高速なソフトウェアを書きやすい言語です。 最も大きな特徴は、マルチスレッドプログラミングにおいてデータ競合の可能性のあるコードを、コンパイラがコンパイル時に怒ってくれるということです。これによって、より安全にマルチコアのパワーを活かすソフトウェアを開発することができます。 また、Rustは静的型付け言語ですが、その型システムはトレイトやジェネリクスといった強力な機能を持っています。 さて、RustはRubyに比べれば確かに多少速い言語です。ですが、Rustで書いたからといって、ソフトウェアが勝手に高速になるわけではありません。Rustはプログラマがより速いプログラムを書こうとしたとき、それをサポートしてくれるというだけです。速いソフトウェアを書くのはプログラマの仕事です。 高速化の基本は、ボトルネックを見つけて、それを改善することです。新しいプッシュ通知基盤の処理の内容をもう一度振り返っておきましょう。必要な処理は受信設定をクエリ、ARNをクエリ、SNSに送信の3つでした。このうちdry-runで必要なのは前の2つです。そして実際にボトルネックになるのもその2つです。 この2つでは、それぞれ1発ずつMySQLに向かってSELECT文を発行しますが、これにそれぞれ6ms（ミリセック）ほどかかります。仮に100万通のメッセージに対してすべてを直列に実行したとすると、これだけで3時間はかかってしまう計算になります。 当然この6msはほぼすべての時間が単なるI/O待ちです。つまり、コードを工夫して計算量を減らすような最適化ではなく、クエリの戦略を工夫して時間を削る必要があるということです。

プログラムを高速化するアイデア そこで私が参考にしたのは、Facebookの「DataLoader」というクエリを最適化するためのライブラリでした。このライブラリはJavaScript向けのライブラリですが、アイデアは言語を問わず参考にできます。 DataLoaderの基本的なアイデアは、クエリをまとめることです。「クエリをまとめる」とは、いったいどういうことなのでしょう？ この上に書いてあるクエリのように、IDを指定してレコードを引くだけの単純なクエリはよくあります。 実際にプッシュ通知配信基盤で用いているものも同じようなつくりです。このクエリのID部分をIN演算子にまとめて放りこみ、1つにしてしまおう、というのが基本的なアイデアです。 では、いったいどれぐらいの数のクエリをまとめるのが最適なのでしょうか？ ベンチマークを取り、調べてみることにしました。 このIN演算子の括弧の中に並べる値の数をバッチサイズと呼びますが、ベンチマークの結果、このバッチサイズは数千ぐらいが適切だとわかりました。たった1つのIDについて問い合わせるクエリが6msかかる一方で、IDを1,000個並べたクエリでも、わずか11msでクエリが完了します。

ライブラリを組み合わせて高速化を実現する しかし、また次の疑問が生まれます。もしクエリがたくさんあったらどうするのでしょうか？ ごく単純に実装すると、このような図になります。 メインスレッドがたくさん並んでいます。つまり、バッチサイズの分だけメインスレッドが必要になってしまうのです。 図中ではバッチサイズはたかだが3でしたが、先ほど言ったとおり、最適なバッチサイズは数千に及びます。数千のスレッドを立ち上げるのは現実的ではありません。 そこでTokioというライブラリを組み合わせます。 Tokioはイベントループの実装を提供するライブラリです。そのイベントループでは複数のFutureを1つのスレッドで実行することができます。 また、futuresはFutureトレイトを提供するライブラリです。このFutureはJSのPromiseと同様な概念で、将来的に計算結果が求まることを表す値です。 先ほど登場した入口と出口のoneshotもこのfuturesが提供する機能であり、この出口は実はFutureだったのです。つまりTokioのイベントループを使えば、1つのスレッドでたくさんの出口を待ち合わせることができるのです。 雰囲気としてはこのような図になります。 メインスレッドの数を増やさずにたくさんのクエリをまとめることができました。 このoneshotの入口をキューに入れるパターンは、キューの向こう側から1件ずつ値を返してもらいたいときに使えるパターンです。見方を変えると、ブロッキングな処理をFutureに変換しているとみなすこともできます。覚えておくと地味に便利です。 こうしてクエリをまとめることで、無事にプッシュ通知配信基盤の高速化に成功しました。