記事公開日:2023年9月19日
※このブログは2023年7月11日に公開されたFlipkart社 (米Walmart傘下のインドのEC最大手で1億人以上のユーザーを有する) のエンジニアであるSachin Japate氏により執筆された英語ブログ「Scaling TiDB To 1 Million QPS」の拙訳です。なお、本ブログはFlipkart社の許可をいただき日本語で提供しています。Flipkart社は、翻訳記事中の情報の欠落、誤訳、または誤りについて、一切の責任を負いません。
翻訳およびPingCAP Japanサイトで記事掲載することについてご快諾いただいたFlipkart社の皆様にこの場をお借りし感謝申し上げます。
TiDB導入の背景と経緯
Flipkartの複数のアプリケーションは、MySQLをデータストアとして利用しています。適切な構成と最適化により、トランザクションの保証、毎秒の高いクエリ数 (QPS) の処理、低レイテンシーの維持が可能になります。
Flipkartのユーザートラフィックが年々増加するにつれ、これらのアプリケーションのQPS要求も増加しました。開発チームは、ベアメタルサーバーでの稼働が必要になるまで、要件を満たすためにMySQLクラスタを垂直方向に拡張し続けました。この時点で、アプリケーションをさらに拡張できる水平スケーラブルなソリューションを検討する必要がありました。
TiDBの検討を開始
このようなMySQLデータベースの制約から、アプリケーションチームは代替ソリューションを探すことになりました。分散型SQLデータベースであるTiDBは、水平スケーラブルでMySQLと互換性があるため、可能性のあるソリューションと考えられました。現在MySQLを使用しているアプリケーションは、アプリケーションレベルでの大幅な変更を必要とすることなく、TiDBに切り替えることができると予測しました。
Flipkartは、DBレベルのQPSで60,000の読み込み、15,000の書き込みという中程度のスループットレベルで、2021年初頭からTiDBをホットストアとして実運用してきました。そして、非常に高いクエリ/秒 (QPS) と低レイテンシーが要求されるユースケースにおいて、TiDBをホットSQLデータストアとして使用することの実現可能性があると見込み、初めて実証することに着手しました。
この記事は、TiDBを高スループット要件に対応させるためにどのようにテストしたかをまとめ、その結果、観察、そして学んだことについて共有することを目的としています。
設定した目標
TiDBに期待されるパフォーマンスを実証するために、Flipkartにおける高スループットのユースケースの1つを選びました:
- 1Mops/s 以上の一貫した読み込みスループットとP99で10ミリ秒未満のレイテンシーを実現すること。
- 100,000ops以上の書き込みスループットと20ミリ秒未満のレイテンシーを実現すること。
検証設定
ベンチマークで選択したユースケースは、負荷の高い読み込みと書き込みのシステムです。読み込みの負荷は、プライマリ・キーまたはセカンダリ・キーのポイント・ルックアップで構成され、書き込みワークロードはINSERTクエリです。
スキーマの概要
テーブル数合計 | 110 |
中核となるエンティティ・テーブル | 15 (読み込みと書き込みの大半が行われる場所) |
平均レコードサイズ | メインテーブルでは1.14 KB (その他のテーブルでは1KB未満) |
プライマリキー | 全てのテーブルはBigInt型のAUTO_INCREMENTが付与されたプライマリーキーです。 |
ユニークなセカンダリ・インデックス | 全てのコア・エンティティ・テーブルには、1つまたは2つのユニークなセカンダリ・インデックスがあります。 |
ユニークでないセカンダリ・インデックス | ほとんどのコア・エンティティ・テーブルには、1~5個のユニークでないセカンダリ・インデックスがあります。このテーブルには13個のユニークでないセカンダリ・インデックスがあります。 |
タイムスタンプインデックス (ホットスポット) | このテーブルにはタイムスタンプ列に3つのインデックスがあります。このようなインデックスは分散DBでは実現が困難です。 |
データ・フットプリント
ユニーク・データ・サイズ | 466 GB |
データの冗長性 | 3 |
合計ストレージサイズ | 1.4 TB |
ベンチマーク詳細
検証トポロジーの概要
ベンチマークには以下のセットアップを使用しました:
- TiDBクラスタは、FlipkartのプライベートクラウドのK8sベアメタル上に展開
- ステートフル・ノードには、NVMe SSDを直接接続
- クライアント負荷ジェネレーター (読み込みはSysbench、書き込みはJavaアプリ) をK8s上に配置
- クライアントはTCPロードバランサー (Haproxyインスタンス) を介してTiDBクラスタに接続
ハードウェアとソフトウェアの詳細
データベースクラスタ
コンポーネント | デプロイ場所 | 台数 | メモリ | ストレージ | バージョン |
PD | K8s ベアメタル | 3 | 20Gi | 20Gi | v6.5.0 |
TiDB | K8s ベアメタル | 32 | 32Gi | - | v6.5.0 |
TiKV | K8s ベアメタル | 12 | 80Gi | 512Gi | v6.5.0 |
合計Pod数:47、合計CPU数: 752、合計RAM:2044Gi、合計ストレージ: 6TB
K8s StorageClass: ベアメタル上の直接接続NVMe SSD
ロードバランサー
コンポーネント | 台数 | CPU | バージョン |
TCP VIP | 1 | 62 | HAProxy - v2.1.4 |
Load Generator
コンポーネント | デプロイ場所 | 台数 | CPU | メモリ | バージョン |
Sysbench (読み込み) | K8s | 10 | 24 | 64Gi | v1.0.20 |
Java Client (書き込み) | K8s | 25 | 7 | 12Gi | NA |
読み込みワークロードの詳細
読み込みの負荷は、12個のSELECT文の組み合わせで構成され、トランザクションを伴いません。
- アプリケーションによって実行された実際のSELECT文を使用
- すべてポイント・ルックアップで、プライマリ・キーかセカンダリ・キーのいずれかを使用
- Sysbenchベンチマークツール用に開発されたカスタムLuaスクリプトは、ファイル内のidリストを反復処理し、データベースにクエリーを発行
- 最適なパフォーマンスを実現するために、このスクリプトをサーバーサイドのプリペアドステートメントを使用するように設計
- SELECTクエリの実行中は、トランザクションは利用しない
書き込みワークロードの詳細
書き込み負荷は、5つの異なるテーブルに対して10個のINSERT文を実行する単一トランザクションで構成されます。
- アプリケーションによって実行された実際のINSERT文を使用
- トランザクションを実行するために開発されたカスタムJavaアプリケーションとLocustを使ってJavaアプリケーションを呼び出し、DBへの負荷を生成
検証計画
- K8sベアメタルにTiDB v6.5.0クラスタをデプロイ
- TiDB-Lightningツールを使ってMySQLからTiDBにデータベースをインポート
- アプリケーションの読み込みクエリ用のカスタムスクリプトと共にSysbenchをデプロイ
- Sysbenchクライアントを使用して、同時実行数を変えながら読み込みワークロードを実行 (各検証を30分間同時に実行)
- スループット、P95およびP99レイテンシーを記録
- 書き込みクエリのためのカスタムJavaアプリのデプロイ
- Locusを使用してユーザー数を100人から50人ずつ変化させながら負荷を発生 (各ユーザーのセットで5分間実行)
- スループット、P95およびP99レイテンシーを記録
結果と考察
読み込みワークロードの結果
まとめ:
- 107万QPSの場合、P95のレイテンシーは4.8ミリ秒、P99のレイテンシーは7.4ミリ秒でした。
- P99とP95の差は、1.07M QPS以下では1ミリ秒で一定でした。
- 1.09M QPSを超えると、TiDBコンポーネントのCPU使用率が高い (65%以上) ため、P99とP95の差はさらに縮まることが分かりました。
書き込みワークロードの結果
まとめ
- 123,000QPSの場合、P95のレイテンシーは6.1ミリ秒、P99のレイテンシーは13.1ミリ秒でした。
- 123,000QPSを超えると、TiKV側のCPUの競合により、P95のレイテンシーが10ミリ秒を超えるようになりました。
- 上記のスループットとレイテンシーは個々のINSERTオペレーションに対するものですが、トランザクションの観点からは、クラスタはP95:~50ミリ秒s、P99:~60ミリ秒で~12,000 TPS (トランザクションあたり10インサート) を達成することができました。
漸進的な努力と進歩的な発見
100万QPS達成に向けた私たちの取り組みには、複数の進歩的な発見がありました。これは、それを実現するために集まったさまざまなチームによる、いくつかの段階的な努力の結果です。以下は、この達成につながった成果をまとめたものです。
ログ・リプレイ・アプローチ:
当初、TiDBのベンチマークにはmysqlslapを使用しました。このツールによって、既存のMySQLの一般的なログをキャプチャし、異なる同時実行数を使用してクエリを再生し、負荷を生成しました。
SHOW SLAVE STATUS、SHOW MASTER STATUSなどのMySQL特有のクエリや、PERFORMANCE_SCHEMA/INFORMATION_SCHEMAに関するクエリは、TiDBではサポートされていませんでした。このアプローチでは、これらのクエリの失敗率が非常に高くなりました。
ログ・リプレイ・アプローチを使用した場合、15ミリ秒のP99レイテンシーで最大152,000 QPSに達することができました。
カスタムロードジェネレータとプリペアドステートメント:
100万QPSという目標を念頭に、パフォーマンスを向上させる機会を積極的に追求しました。PingCAPの提案に従い、クエリ実行におけるオプティマイザのコストを最小化するために、サーバサイドのプリペアド・ステートメントを実装しました。この非機能要件のために、APIからのクエリをシミュレートできるカスタムコードを書くことに投資しました。
最初の試みとして、Locustで使用するPythonスクリプトを作成し、負荷を生成しました。しかし、Pythonコネクタの接続制限により、目的の負荷を生成することができませんでした。
2回目の試みでは、Luaスクリプトでカスタムロードを生成できるSysbenchツールを活用しました。私たちは、実験用のクエリを実行するためのカスタムSysbench Luaスクリプトを開発しました。
サーバーサイドのプリペアドステートメントにより、クエリのパース時間は完全に0ミリ秒に短縮されました。14ミリ秒のP99レイテンシーで156,000 QPSを達成することができました。
ELBのボトルネックを克服
TCP ELBを排除し、sysbenchクライアントを別のTiDBに直接接続しました。この配置により、65ミリ秒のP99レイテンシーで323,000QPSを達成しました。
さらに、TCP ELBのリソースを3コアから12コアに拡張しました。これにより、P99レイテンシー、45ミリ秒で291,000 QPSを達成することができましたが、CPU使用率の高さとレイテンシーオーバーヘッドの増加という課題は残りました。この2つの課題に対処するため、最新バージョンのHAProxyを導入しました。
20台のTiDB PODにリクエストをロードバランシングするために3台のHAProxyを配置し、30台のSysbenchクライアントボックスを使用、10台のクライアントを各HProxyに接続しました。このセットアップで、125ミリ秒のP99レイテンシーで338,000 QPSを達成しました。
このスループットでは、TiDB podがボトルネックになるのが見えたので、TiDB podを20から30に増やし、P99レイテンシー230msで最大358,000QPSを達成しました。
K8sノードのパケット/秒 (PPS) 制限
解析の結果、特定のTiKVストアでKVリクエストの処理時間が長くなり、クエリ全体の待ち時間が長くなっていることが判明しました。この問題の原因は、特定のK8sノード上のTiKV podの分布が偏っているため、パケット処理でノイジー・ネイバーの問題が発生していることでした。
私たちのセットアップでは、K8sベアメタルノードはパケット配信専用に4つのコアを搭載しています。この配置では、ベアメタル上に複数のpodがあり、ネットワーク負荷が高い場合に拡張できませんでした。
クラスタにベアメタルノードを追加し、異なるノードが異なるTiKV podをスケジューリングするようにしました。さらに、HAProxyの新しいバージョンv2.1.4を実行する新しい62コアのTCP ELBで、644,000QPS、3.5ミリ秒 P99、1.9ミリ秒 P95レイテンシーを達成しました。
100万QPS (読み込み) を達成するためのELBの廃止
TCP ELBをスケールアップすることで、644,000の読み込みスループットを達成できました。TCP ELBを使用してQPSをさらにスケールアップすることは困難でした。そこで、ClusterIPサービスを通じてTiDBにアクセスし、ベンチマークをさらに強化しました。このサービスにアクセスするために、カスタムスクリプトをバンドルしたSysbenchツールを、TiDBが稼働しているのと同じK8sクラスタ (ただし別の名前空間) にデプロイしました。
それぞれ24のvCPUと64GBのRAMを搭載した10台のSysbench podをデプロイしました。各Sysbenchクライアントpodで210同時実行で読み込みベンチマークを実行したところ、102万読み込みQPS、P99レイテンシー5.47ミリ秒、P95レイテンシー3.75ミリ秒を達成しました。
その後、K8sプラットフォームチームは、パケット処理に16コアを充てることで、PPSの制限に対処しました。この追加変更とこれまでに述べたその他の改善により、P99レイテンシー 7.43ミリ秒、P95レイテンシー 4.82ミリ秒で107万QPSの読み込みを達成することができました。
まとめ
- これらのベンチマークは、TiDBが高スループット、低レイテンシーのホットストアの性能要件を満たすために拡張できることを示しています。サービスの要件を超え、TiDBが100万QPS以上の非常に高い一貫した読み込みスループットを5ミリ秒のレイテンシーで実現できることを実証しています (P95)。
- 高い読み込みQPSは、リードレプリカを追加してMySQLをスケーリングすることによっても達成できますが、それは質的に異なります。MySQLのリードレプリカは最終的に一貫性のあるリードを提供しますが、TiDBベンチマークは常に一貫性のあるリードを対象としていました。
- ベンチマークはまた、TiDBが6.1ミリ秒 (P95) のレイテンシーで100,000QPS以上という非常に高い書き込みスループットを実現できることを示しています。
- これらの性能レベルは、セカンダリインデックスを含む実際のスキーマで、実際のデータのコピーに対する実際のクエリを使用して達成されたことにも注目して欲しいポイントです。具体的には、メインエンティティテーブルには1つのユニークなセカンダリインデックスと13のユニークでないセカンダリインデックスがあり、そのうち3つはタイムスタンプに関するもので、ホットスポットになりやすい状況でした。
- TiDBの水平スケーラビリティの側面を科学的に定量化することは、今回のベンチマークの一部ではありませんでした (今後の課題として取り上げるつもりです)。しかし、これらのスループットに到達した経験から、TiDBは水平方向にスケーラブルであることが証明されました。より多くのリソースがあれば、TiDBをより大きなクラスタに展開し、はるかに高いスループットを実現することができます。
- 100万以上のQPSに十分な負荷を発生させること自体がチャレンジであり、通常の非機能要件の検証に使われる既存のツールを超える必要がありました。
- 低レイテンシーで100万以上のスループットへの旅は、ボトルネックを明らかにし、スタックの様々な部分 (DB、ELB、K8sノードのネットワーク処理) の改善にもつながりました。これを実現できたのは、複数のチームの協力とPingCAPのサポートのおかげです。