Railsで秒間1000コミットを捌くにはどうすればいいのか (Kaigi on Railsのフリースペースより)

先日のKaigi on Rails中の雑談として @ima1zumi さんから、RDBに対して秒間1000コミットぐらいで処理が詰まってる場合ってどうするのが良いのか、という質問を受けまして、雑談の中で色々答えてたんですが、せっかくだから記事にまとめておこうと思います。 ちょっとしたKaigi Effectって感じですね。

今回のKaigi on Railsトークの中では、 数十億のレコードを持つ5年目サービスの設計と障害解決 by KNR - Kaigi on Rails 2023 の話なんかは割と関連がありますね。ユーザーの行動履歴というのは、ユーザー数 * N * タイムスパンで増えていくレコードなので、書き込みとデータ量が爆発しがちです。トランザクションで堅牢に処理しなければいけないケースもそこまで多くないので、RDBだと書き込みに対する処理が過剰なケースが多い。実際のところこの手のデータってイベントログなんですよね。ログ収集と分析基盤で処理できる方が良いんですが、今回のpixivさんの話では履歴情報が提供機能と密接に結びついているので、その辺り難しそうな感じでしたね。 この記事では、とりあえず機能要求については置いておいて、このデータ量を捌いて永続的なデータストアに入れる時にどういう考え方をしているのか、ということを書いていきます。

ちなみに、タイトルにRailsと入ってますが、ぶっちゃけ余りRails自体とは関係が無いですw 強いて言えばこれから書くのはWeb業界の話だということです。エンプラの世界だったらOracleに何億か払うといういつもの選択肢があります。

さて、まず第一に考えておかなければいけないのは、書き込みリクエストに対してどれぐらいのレイテンシで結果を返さなければいけないのか、という点です。

書き込みリクエストから結果の反映まで一定時間待てる場合 (かつ1000tpsが瞬間的なピークの場合)

ここでそれなりに待てるなら、非同期処理に逃がして書き込みペースを調整すれば問題ないでしょう。言い換えると結果整合性が取れていれば良い場合です。

その場合に使える道具としては、AWSのSQSやKinesis、Redisなどです。

書き込みが羃等なのであればSQSが安全かつスケールが簡単なのでオススメです。SQSは基本的にat least onceなので、稀に同じキューが重複して取得されてしまう可能性があります。 実際に起きるのはかなり稀ですが、それが許容できない場合はSQSのFIFOキューを使うか、Redisで頑張るか、SQSとRedisを組み合わせて現実的に発生しない程度に確率を下げるか、という策が取れます。

Redisを活用する場合は、Railsの世界で一番楽なのはsidekiq-proを使ってワーカーの数で書き込みペースを調整するという形かと思います。proじゃないと駄目なのは、通常のsidekiqだとジョブロストの可能性が無視できないので、安全に構成できないからです。

追記: ちょっと誤解を招きそうだなと思ったので追記しておきます。これは常にsidekiq-proを使うべきという話ではありません。非同期処理のよくあるユースケースの一つとして完了通知などが挙げられますが、こういった例では稀にジョブがロストしたとしても致命的な問題にならないのでそこまで気を使わなくていい場合があります。上記の例ではまずRDBに書いて安全な記録を残す手前の部分でsidekiqを使っているので、滅多に無いことでもジョブロストしたらデータの不整合や消失に繋がります。こういった万が一のジョブロストも避けたい場合はproを使った方がいいという感じですね。

ちなみに、Kinesisは単純なキューではなくてKafkaなどに近いので、複数のサービスでそのデータが必要とかでない限りは選択の優先順位は高くありません。SQSより遥かに扱うのが難しいサービスです。

書き込みリクエストから同期処理で結果を反映しなければならない場合、もしくは定常的に1000tps以上が必要な場合

問題なのはこのケースで、これについてはある意味非常に単純な3つの道を選ぶことになります。

札束を積むか、RDBを分割するか、RDB以外のものに書き込むか。

札束を積む

ワークロードに依りますが、Auroraに金を積めば秒間1000コミットぐらいは普通に処理できます。さっき職場のRDSのメトリックを見てみましたが秒間1000〜1500コミットぐらいは1インスタンスで普通に処理できていて、まだ余裕がある感じです。インスタンスサイズはr6g.16xlargeです。これはGravitonの最大サイズですが、r6iなら32xlargeまで札束を積めるので、本当にギリギリまで引っ張れば秒間4000コミットぐらいまでなら普通に処理できる可能性が高いです。 Kaigi on Rails中の雑談では、Auroraで何とかなるか微妙なラインかもなーと答えてしまいましたが、改めて弊社の様子を見る分にはよっぽど1コミットが重くない限りは十分お金で対処可能だと思いました。もし1コミットで影響するレコードが多い、ロック競合するパターンが多い、トランザクションにおけるisolationに依存した読み込みが非常に多い、といったケースだと、Auroraにお金を積むだけでは無理な可能性はあります。

RDBを分割する

ワークロードに合わせて利用するDBをアプリケーション側で切り替えるやり方です。ゲーム系では昔からよく聞く方法ですが、これは基本的に茨の道ですね。ワークロードのパターンによって垂直分割と水平分割のやり方があります。現在のRailsはmulti DB対応が組込まれているので、昔に比べれば本体のメンテナンスに乗ることができる分やり易くなったとは言えます。 しかし、それでも複数DBは本当に必要になるまでは回避したい選択です。もし改修要求の結果複数DBに跨るトランザクションやジョインが求められた場合は即破綻しますし、アプリケーション側でそれを上手いこと回避する様に実装しても高確率でバグから逃れられないでしょう。後からデータ不整合が発覚した場合の修正コストは大抵の場合、非常に高く付きます。加えて、後から分割数を増やすのがとても大変だという落とし穴もあります。 もし本当に必要になったのなら、ドメインモデルとしっかり向き合いましょう。将来のアプリケーションの成長まで加味して慎重にモデルの区分けを行う必要があります。というか、この辺りは実質的にマイクロサービス化と同じ考え方になると思います。

RDB以外のものに書く

これは本当の所はトランザクションが必要無い場合に採用可能です。この場合は各種分散データストアの導入を検討することになります。弊社ではKafkaやCassandraを利用している箇所がありますが、レプリカ書き込みを含めて秒間100万件ぐらいの書き込みが発生しています。例えばCassandraは読み込みよりも書き込みの方がスケールさせやすい作りになっていて、クラスタの台数を増やせばこれぐらいの書き込みペースは普通に捌くことができます。弊社ではそれなりにメモリとストレージを積んだ20台ぐらいで済んでます。当然、複数台のクラスタを管理するコストが増えるし、新しいミドルウェアの知識やパフォーマンスチューニングも必要だし、複数DBと実質的に同じ問題を抱えることになるので、開発業務に与える負荷は非常に高い選択肢です。しかし、求められる書き込み量の桁が2桁とか3桁とか上がってしまうと、RDBに書くという手段ではどうにもならなくなるのでやらざるを得ないという感じですね。この分野で他に使えそうなミドルウェア/サービスは、ScyllaDB、DynamoDB、BigTable辺りでしょうか。メモリキャッシュに近い方向ならHazelcastとかIgniteとかAerospikeみたいなのもあります。

どうしてもトランザクションが必要だが書き込み件数が秒間数万件を越えるという非常にハードな現場だった場合、SpannerやCockroachDB、TiDBなどの分散SQLデーターベースの採用を検討することになるかと思います。例えばCockroachDBはマルチリージョン構成も可能なグローバルスケールのサービス展開を想定したデータベースで、PostgreSQLプロトコルと互換性があります。つまりRailsからならpgドライバで接続可能という訳ですね。 この辺りの選択肢は、とにかく技術調査力が必要です。業界全体を見ても、ここまでのスケーラビリティが求められるOLTPというのはそう多くないので、知見が全然見つからないというのはザラです。自分でアーキテクチャをちゃんと理解し、狙ったパフォーマンスが出る様にチューニングと実験をし、運用ラインを整える必要があるでしょう。お金の力があれば、エンタープライズサポートに頼ることで開発負荷を下げることはできると思います。

現実的にどうするのか

ざっとまとめさせてもらいましたが、真の意味でオンライントランザクションが必要なのかどうかで、大量のアクセスを捌く難易度は大きく変わります。一番現実的なのは「本当にそこでユーザーを待たせてはいけないのか?」という話をちゃんとすることです。5分待たせて良いんだったら全然難易度が違ってきます(非同期処理化は別の複雑さに繋がる可能性はありますが)。 あくまで自分の感覚ですが、大量のアクセスがあって本当にオンライントランザクションが要るケースはそこまで多くないと思っています。現実的な開発に合わせて仕様をコントロールするのも重要な仕事です。

そして、顧客価値のためにどうしてもオンライントランザクションが要るとなったら、まずはAWSGoogleに金を積みましょう。分かり易くキャッシュが減ってしまいますが、実際はこれが一番安くつきますし、とにかくすぐに対応可能です。

残りの手段であるRDBの分割や分散DBの採用は、ぶっちゃけ個別の地獄にそれぞれ頑張って対処するしかないんですよね。気合入れて調査して実験して計測してチューニングして社内向けのドキュメントや勉強会を準備して、とそれ相応に時間とコストがかかるので、トラフィックが追い付かない内に次の一手が打てる様に準備しましょう。実際、RDBでは色々な意味でコストが合わなくなる時というのはしばしばやってきます。そういう時のための選択肢を常に調査して、手遅れになる前に対処することが大事です。まあ、それができれば苦労しないというやつですが……。

ちなみに、Repro株式会社では、こういったことに興味があるエンジニアを募集しております。ちょっとハードなトラフィックを扱う経験が積みたいという方は、カジュアル面談からでも良いので是非ご応募ください。(現職のことも書いてるので、一応やっとかんと……)

company.repro.io