■ 1. 背景と問題設定
- 記事の経緯:
- Kaigi on Rails中の雑談でRDBに対して秒間1000コミットぐらいで処理が詰まってる場合の対処法について質問を受けた
- せっかくだから記事にまとめることにした
- 関連トークとの関係:
- 数十億のレコードを持つ5年目サービスの設計と障害解決の話が関連する
- ユーザーの行動履歴はユーザー数とNとタイムスパンで増えていくレコードなので書き込みとデータ量が爆発しがちである
- トランザクションで堅牢に処理しなければいけないケースもそこまで多くないためRDBだと書き込みに対する処理が過剰なケースが多い
- この手のデータはイベントログでログ収集と分析基盤で処理できる方が良い
- pixivの話では履歴情報が提供機能と密接に結びついているのでその辺り難しそうだった
- 記事の焦点:
- 機能要求については置いておいてこのデータ量を捌いて永続的なデータストアに入れる時にどういう考え方をしているのかを書く
- タイトルにRailsと入ってるがぶっちゃけ余りRails自体とは関係が無い
- これから書くのはWeb業界の話でエンプラの世界だったらOracleに何億か払うといういつもの選択肢がある
- 最初に考えるべきこと:
- 書き込みリクエストに対してどれぐらいのレイテンシで結果を返さなければいけないのかという点
■ 2. 書き込みリクエストから結果の反映まで一定時間待てる場合
- 適用条件:
- かつ1000tpsが瞬間的なピークの場合
- それなりに待てるなら非同期処理に逃がして書き込みペースを調整すれば問題ない
- 言い換えると結果整合性が取れていれば良い場合
- 使える道具:
- AWSのSQSやKinesisやRedis
- SQSの活用:
- 書き込みが羃等なのであればSQSが安全かつスケールが簡単なのでオススメ
- SQSは基本的にat least onceなので稀に同じキューが重複して取得されてしまう可能性がある
- 実際に起きるのはかなり稀だがそれが許容できない場合はSQSのFIFOキューを使うかRedisで頑張るかSQSとRedisを組み合わせて現実的に発生しない程度に確率を下げる策が取れる
- Redisの活用:
- Railsの世界で一番楽なのはsidekiq-proを使ってワーカーの数で書き込みペースを調整するという形
- proじゃないと駄目なのは通常のsidekiqだとジョブロストの可能性が無視できないので安全に構成できないから
- 非同期処理のよくあるユースケースの一つとして完了通知などが挙げられるがこういった例では稀にジョブがロストしたとしても致命的な問題にならないのでそこまで気を使わなくていい場合がある
- RDBに書いて安全な記録を残す手前の部分でsidekiqを使っている場合は滅多に無いことでもジョブロストしたらデータの不整合や消失に繋がる
- こういった万が一のジョブロストも避けたい場合はproを使った方がいい
- Kinesisの位置づけ:
- 単純なキューではなくてKafkaなどに近い
- 複数のサービスでそのデータが必要とかでない限りは選択の優先順位は高くない
- SQSより遥かに扱うのが難しいサービス
■ 3. 同期処理で結果を反映しなければならない場合
- 適用条件:
- 書き込みリクエストから同期処理で結果を反映しなければならない場合
- もしくは定常的に1000tps以上が必要な場合
- 3つの選択肢:
- 札束を積むかRDBを分割するかRDB以外のものに書き込むか
■ 4. 札束を積む選択肢
- Auroraの性能:
- ワークロードに依るがAuroraに金を積めば秒間1000コミットぐらいは普通に処理できる
- 職場のRDSのメトリックを見ると秒間1000から1500コミットぐらいは1インスタンスで普通に処理できていてまだ余裕がある感じ
- インスタンスサイズはr6g.16xlarge
- r6iなら32xlargeまで札束を積めるので本当にギリギリまで引っ張れば秒間4000コミットぐらいまでなら普通に処理できる可能性が高い
- 限界となるケース:
- 1コミットで影響するレコードが多い場合
- ロック競合するパターンが多い場合
- トランザクションにおけるisolationに依存した読み込みが非常に多い場合
- こういったケースだとAuroraにお金を積むだけでは無理な可能性はある
■ 5. RDBを分割する選択肢
- 概要:
- ワークロードに合わせて利用するDBをアプリケーション側で切り替えるやり方
- ゲーム系では昔からよく聞く方法だが基本的に茨の道
- ワークロードのパターンによって垂直分割と水平分割のやり方がある
- Railsの対応状況:
- 現在のRailsはmulti DB対応が組込まれているので昔に比べれば本体のメンテナンスに乗ることができる分やり易くなった
- それでも複数DBは本当に必要になるまでは回避したい選択
- リスクと課題:
- 改修要求の結果複数DBに跨るトランザクションやジョインが求められた場合は即破綻する
- アプリケーション側でそれを上手いこと回避する様に実装しても高確率でバグから逃れられない
- 後からデータ不整合が発覚した場合の修正コストは大抵の場合非常に高く付く
- 後から分割数を増やすのがとても大変だという落とし穴もある
- 必要な対応:
- 本当に必要になったのならドメインモデルとしっかり向き合う
- 将来のアプリケーションの成長まで加味して慎重にモデルの区分けを行う必要がある
- この辺りは実質的にマイクロサービス化と同じ考え方になる
■ 6. 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はそう多くないので知見が全然見つからないというのはザラ
- 自分でアーキテクチャをちゃんと理解し狙ったパフォーマンスが出る様にチューニングと実験をし運用ラインを整える必要がある
- お金の力があればエンタープライズサポートに頼ることで開発負荷を下げることはできる
■ 7. 現実的な対応方針
- オンライントランザクションの必要性の検討:
- 真の意味でオンライントランザクションが必要なのかどうかで大量のアクセスを捌く難易度は大きく変わる
- 一番現実的なのは本当にそこでユーザーを待たせてはいけないのかという話をちゃんとすること
- 5分待たせて良いんだったら全然難易度が違ってくる
- 非同期処理化は別の複雑さに繋がる可能性はある
- 大量のアクセスがあって本当にオンライントランザクションが要るケースはそこまで多くない
- 現実的な開発に合わせて仕様をコントロールするのも重要な仕事
- 優先順位:
- 顧客価値のためにどうしてもオンライントランザクションが要るとなったらまずはAWSかGoogleに金を積む
- 分かり易くキャッシュが減ってしまうが実際はこれが一番安くつくしとにかくすぐに対応可能
- その他の手段への対応:
- RDBの分割や分散DBの採用はぶっちゃけ個別の地獄にそれぞれ頑張って対処するしかない
- 気合入れて調査して実験して計測してチューニングして社内向けのドキュメントや勉強会を準備してとそれ相応に時間とコストがかかる
- トラフィックが追い付かない内に次の一手が打てる様に準備する
- RDBでは色々な意味でコストが合わなくなる時はしばしばやってくる
- そういう時のための選択肢を常に調査して手遅れになる前に対処することが大事