DUICUO

統合コンテンツプラットフォームビジネスのためのHBase最適化プラクティス

I. 事業概要

統合コンテンツプラットフォームは、vivoコンテンツエコシステムにおけるコンテンツレビュー、コンテンツ理解、インテリジェントコンテンツ作成、コンテンツ配信といったコア機能を主に担っています。インターネット上のコンテンツを集約することで、業界レベルのビジネスプラットフォームとコンテンツエコシステムを構築し、上流から下流までのビジネスに高品質で信頼性の高いワンストップサービスを提供しています。現在、動画サービスや一般的な情報フローサービスなどのビジネスにサービスを提供しています。

コンテンツプラットフォームとして、配信ニーズを満たすために、毎日大量のテキスト、画像、動画コンテンツを保存する必要があります。同時に、これらのコンテンツを処理する際に生成される基本情報、カテゴリタグ、レビュー情報などのデータも保存する必要があります。さらに、保存するデータ量が膨大であるだけでなく、読み書き操作も非常に頻繁に発生します。現在、データベースの読み書き操作は主に以下の2つの側面に集中しています。

  • コアリンクのコンテンツ処理には、コンテンツ機能情報に対する多数の読み取りおよび書き込み操作が含まれます。
  • 外部の関係者に提供されるクエリ サービスでは、ソース データベースをクエリする多くの操作が生成されます。

長年の蓄積により、現在保存されているデータの量はますます膨大になっており、今後もデータ量は拡大し続けることが予想されます。そのため、サービスの安定性と拡張性を確保するために信頼性の高いストレージオプションを選択することが、現在のプロジェクトアーキテクチャの最優先事項となっています。

II. 既存の問題

HBaseを選択する前は、コンテンツプラットフォームのコアデータは主にMongoDBを使用して保存されていました。しかし、日常的な使用において、ストレージ側に以下の問題点が見つかりました。

  • コアデータが大量で、テーブルが 20 TB を超え、ストレージの合計が 60 TB を超える場合、MongoDB のストレージ アーキテクチャは優れたスケーラビリティの要件を満たすことができません。
  • アクセス クエリ トラフィックが大きく、クエリ インターフェースの高パフォーマンスを維持しながら、スマート プッシュ、一般情報フロー、ビデオ推奨側からの大量の back-to-origin クエリ トラフィックを処理する必要があります。
  • MongoDB の安定性を維持するためには、MongoDB データベースのマスターノードとスレーブノードを定期的に切り替えてインスタンスを再構築する必要があり、これには長期的な運用保守投資が必要となり、保守コストが高くなります。

したがって、ビジネス要求とストレージ サイズに対する需要の増大に対応するために、現在のシナリオにより適したデータベースを早急に見つける必要があり、高性能、高安定性、スケーラビリティ、低メンテナンス コストという特性を備えていることが求められています。

III. ストレージの選択

調査の結果、HBase の一部の機能が現在のシナリオの要件を十分に満たすことができることがわかりました。

(1)高性能

HBaseは、行ベースのデータベースであるMongoDBとは対照的に、キー/バリュー型の列指向ストレージアプローチを採用しています。同じ列ファミリーのデータは単一のファイルに保存され、ファイルが大きくなるにつれて分割され、他のマシンに分散されます。そのため、データ量が増加しても、読み書きパフォーマンスが低下することはありません。HBaseはミリ秒レベルの読み書きパフォーマンスを誇ります。大量のデータを書き込む場合は、バルクロードを使用して効率的にデータをインポートできます。

(2)高いスケーラビリティと高い耐障害性
HBaseストレージは、分散ファイルシステム(HDFS)を実装するHadoopをベースとしています。HDFSのレプリケーションメカニズムは高い耐障害性を備え、フェデレーションメカニズムは高いスケーラビリティを実現します。Hadoopをベースとしているということは、HBaseが本質的に優れたスケーラビリティと高い耐障害性を備えていることを意味します。

(3)強い一貫性

HBaseのデータは強力な一貫性を備えています。CAP定理によれば、HBaseはCP(一貫性、可用性、パーティションマジック)原則に該当します。CAP定理は、ネットワークパーティションが発生した場合、一貫性と可用性はどちらか一方を優先する必要があることを示しています。HBaseはデータを書き込む際、まず操作記録を先行書き込みログ(WAL)に書き込み、その後Memstoreにロードします。ノードに障害が発生しても、WALデータはHDFSに保存されるため、データが失われることはありません。先行書き込みログを読み出すことでコンテンツを復元できます。

(4)列の値は複数のバージョンをサポートする
HBaseのマルチバージョン機能を使用すると、列ファミリーのバージョン番号の数を制御できます。デフォルトは1で、各キーに1つのバージョンが格納され、同じ行キーの場合、後続の列値によって前の値が上書きされます。列ファミリーのバージョン番号の数は動的に変更できます。各バージョンにはタイムスタンプが付与されます。デフォルトは書き込み時刻ですが、書き込み操作中にタイムスタンプを指定することもできます。

これらすべての特性を考慮すると、HBase は現在のプロジェクトのデータベース選択要件に非常に適しています。

IV. HBaseの最適化プラクティス

プロジェクト全体を通してHBaseの利用が拡大するにつれ、いくつかの使用上の問題とクエリパフォーマンスの問題が明らかになりました。具体的には、頻繁なクエリスパイク、夜間のコンパクション時の高いレイテンシ、トラフィックピーク時の少数のリクエストの遅延などです。これらの問題に対処するため、以下の4つの側面でHBaseの利用を最適化しました。

4.1 クラスタのアップグレード

HBaseを使い始めた当初は、HBaseクラスターバージョン1.2を使用していました。このバージョンには、RIT(Region-In-Transition)問題の頻発、リクエストレイテンシのスパイク、テーブルの作成と削除の速度の低下、メタテーブルの安定性の低さ、ノード障害からの回復の遅さなど、多くの欠点がありました。私たちが遭遇した主な問題は、応答時間のスパイクでした。この問題により、オリジンサーバーに戻る際にリアルタイムクエリインターフェースで頻繁にタイムアウトが発生し、応答時間のスパイクが発生し、下流のビジネスによってサーキットブレーカーが切断され、ビジネスクエリに影響を与えていました。HBaseチームとの議論と評価の後、私たちはビジネスで使用しているクラスターをHBaseバージョン2.4.8にアップグレードすることを決定しました。このバージョンは、会社の多くのビジネスシナリオで検証されており、バージョン1.2.0のほとんどの問題点を解決し、読み取りと書き込みのパフォーマンスを大幅に向上させ、読み取りスパイクを効果的に削減し、単一マシン処理パフォーマンスの向上によりマシンコストを約20%削減しました。

以下は、クラスターアップグレード後の平均読み取り時間と書き込み時間の比較グラフです。ご覧のとおり、アップグレード前は平均応答時間が頻繁に急上昇し、10秒を超えることもありました。アップグレード後は、このような急上昇はほぼ見られなくなり、10ミリ秒未満にとどまり、時折発生する場合でも数十ミリ秒程度となっています。

アップグレード前

アップグレード後

4.2 接続プールの使用と接続の事前加熱

HBase Connectionオブジェクトの作成は、単一のソケット接続に対応するだけでなく、Zookeeper、HMaster、およびRegionServerとの接続を確立する必要があります。そのため、このプロセスは非常に多くのリソースを消費します。通常、Connectionインスタンスは1つだけ作成され、他のコンポーネントはこのインスタンスを共有します。Connectionの初期化後、Connectionクラスの`getTable`メソッドを使用してテーブルに接続します。テーブルへの接続によるネットワークオーバーヘッドを削減するため、クライアントとサーバーの接続を管理するために、テーブルごとに接続プールを構築します。大まかなフローチャートを以下に示します。

接続プールを確立すると、次の 3 つの利点が得られます。

(1)相互干渉を防ぐためにテーブル間のリソース分離を実装した。

(2)接続が再利用されるため、接続作成時のネットワークオーバーヘッドが削減される。

(3)急激な流量増加による影響を防止し、スムーズな流量処理を実現する。

さらに、図に示すように、プログラム起動フェーズでHBaseテーブルへの接続を事前に確立しておくことができます。事前にテーブルへの接続を確立しておくことで、プログラム起動フェーズで大量の接続を確立することによる読み取りおよび書き込みの応答時間の長時間化を効果的に回避し、全体的なパフォーマンスへの影響を軽減できます。

接続プールは、Apache Commons Poolが提供するGenericObjectPoolを使用して実装されています。GenericObjectPoolは豊富な設定オプションを提供し、アイドル状態のオブジェクトを定期的に回収し、オブジェクトの検証をサポートし、強力なスレッドセーフ性とスケーラビリティを誇ります。各テーブルの接続プールオブジェクトは、ローカルキャッシュであるLoadingCacheに配置されます。LoadingCacheはLRUアルゴリズムを使用して、最も古く、最も使用頻度の低いデータを削除することで、未使用のテーブル接続が速やかに解放されるようにします。サードパーティ製のオブジェクトプールとローカルキャッシュを使用することで、HBaseテーブル用の接続プールが確立され、事前ロードが実装されます。これにより、HBaseへの読み取りと書き込みのオーバーヘッドが軽減され、読み取り/書き込み時間が短縮され、サービス起動時の読み取り/書き込みスパイクが改善されます。

4.3 列ごとに読む

HBase でテーブルを作成する場合、列は変更可能で柔軟性が高いため、定義する必要がありません。決定する必要があるのは、列ファミリだけです。有効期限、データ ブロックのキャッシュ、圧縮するかどうかなど、テーブルの多くの属性は、テーブル レベルや列レベルではなく、列ファミリ レベルで定義されます。このアプローチは、従来のデータベースとは大きく異なります。同じテーブル内の異なる列ファミリは、まったく異なる属性構成を持つことができますが、同じ列ファミリ内のすべての列は同じ属性を持ちます。列は列ファミリの存在に依存しているため、列ファミリのないテーブルは意味がありません。したがって、HBase では、列名には常に列ファミリ名が含まれます。列ファミリの存在により、HBase は同じ列ファミリの列を可能な限り同じマシンに配置できますが、異なる列ファミリの列は異なるマシンに分散されます。

一般的に、クライアントがデータの読み取り要求を開始してからデータが返されるまでのプロセスには、次の手順が含まれます。

  1. クライアントは、メタ テーブルが配置されている regionServer ノード情報を ZooKeeper から取得します。
  2. クライアントは、メタ テーブルが配置されている regionServer ノードにアクセスして、リージョンが配置されているノードに関する情報を取得します。
  3. クライアントは、特定のリージョンが配置されている regionServer にアクセスし、対応するリージョンを見つけます。
  4. まず、blockCacheからデータを読み取ります。存在する場合はそれを返します。存在しない場合は、memstoreからデータを読み取ります。存在する場合はそれを返します。存在しない場合は、storeFile(HFile)からデータを読み取ります。存在する場合は、まずblockCacheにデータを書き込んでからデータを返します。存在しない場合はnullを返します。

簡略化した図を以下に示します。

処理中に読み取るフィールドが多すぎる場合、またはフィールド長が長すぎる場合、すべての列からデータを返すと大量の無効データ転送が発生し、クラスターネットワーク帯域幅などのシステムリソースが大量に占有され、読み取りパフォーマンスの低下を招きます。そのため、不要なフィールドクエリの数を減らす必要があります。

Getクラスは、HBaseが提供する公式クエリクラスです。このクラスは主に、読み取るフィールド数を削減するための以下のメソッドを提供します。

  • addFamily : 取得する列ファミリを追加します。
  • addColumn : 取得する列を追加します。
  • setTimeRange : 取得するバージョンの範囲を設定します。
  • setMaxVersions : 取得するバージョンの数を設定します。

現在のプロジェクトでは、HBaseのバージョン範囲とバージョン番号機能は利用していませんが、主要なシナリオで使用されるテーブルには多数のフィールド(例:基本的なコンテンツ属性には数百のフィールドが含まれる場合があります)や、フィールドが非常に大きい(例:コンテンツ解析における一部のベクターフィールド)ものがあります。以前は、クエリがすべてのフィールドを直接読み取っていたため、多くの不要なフィールドが読み取られ、パフォーマンスが低下していました。列ベースの読み取りに切り替え、シナリオごとに異なるフィールドをクエリすることで、半分以上の不要なフィールドが返されることを回避し、平均応答時間も短縮されました。

4.4 コンパクト最適化

HBaseは、LSMツリーストレージモデルに基づく分散型NoSQLデータベースです。様々なデータベースで一般的に使用されているB+ツリーと比較して、LSMツリーは信頼性の高いランダム読み取り性能を維持しながら、より高いランダム書き込み性能を実現します。読み取り要求を行う際、LSMツリーは複数のサブツリーをマージします(B+ツリー構造に似ています)。そのため、マージされるサブツリーが少ないほど、クエリ性能は向上します。MemStoreが閾値を超えると、HDFSにフラッシュされ、HFileが生成されます。書き込みが続くとHFileの数が増加し、前述のようにHFileの数が多すぎると読み取り性能が低下します。読み取り性能への影響を回避するために、これらのHFileに対して圧縮操作を実行し、複数のHFileを1つのHFileにマージすることができます。圧縮操作では、HBaseデータに対して複数の読み取りおよび書き込み操作が必要になるため、大量のI/Oが発生します。基本的に、圧縮操作はI/O操作を犠牲にして、後続の読み取り性能を向上させます。

HBaseの圧縮は、HRegion内のHFileファイルに対して行われます。圧縮には、メジャーとマイナーの2種類があります。メジャー圧縮では、すべてのHFileを1つのHFileに圧縮しますが、削除済みとしてマークされたKeyValueペアは無視されます(これらのKeyValueペアは、圧縮プロセス中にのみ実際に「削除」されます)。ご想像のとおり、メジャー圧縮では大量のI/O操作が発生し、HBaseの読み取りおよび書き込みパフォーマンスに影響を与えます。一方、マイナー圧縮では、少数のHFileのみを選択して1つのHFileに圧縮します。マイナー圧縮は一般的に高速で、I/Oも比較的少なくなります。ビジネスのピーク時にはメジャー操作は無効になり、オフピーク時に定期的に実行されます。

`hbase.hstore.compaction.throughput.higher.bound` は、HBase の HFile ファイル圧縮速度を制御するパラメータの 1 つです。これは、1 秒あたりにマージできる HFile ファイルの最大サイズ(バイト単位)を指定します。HFile ファイルがこの制限を超えると、HBase はマージを高速化するためにファイルを小さなファイルに分割しようとします。このパラメータを調整することで、HBase が HFile ファイルのマージを開始する条件を制御できます。値が小さいほどファイルのマージ頻度が高くなり、HBase のパフォーマンスが低下する可能性があります。一方、値が大きいほど HFile ファイルの容量が急速に大きくなり、読み取りパフォーマンスに影響を与える可能性があります。

`hbase.hstore.compaction.throughput.lower.bound` も、HBase の HFile ファイルのマージ速度を制御するパラメータの 1 つです。これは、HFile ファイルの 1 秒あたりにマージされるデータの最小量(バイト単位)の下限を指定します。マージ速度がこの下限に達すると、HBase は小さい HFile ファイルのマージを停止し、次のデータが到着するまで待ってからマージ操作を再開します。`higher.bound` パラメータと比較すると、`lower.bound` パラメータはファイルマージの頻度とパフォーマンスに大きな影響を与えます。値が高すぎると、ファイルマージの回数が少なくなり、HFile ファイルのサイズが大きくなるため、読み取りパフォーマンスと書き込みの同時実行性に影響します。逆に、値が低すぎると、ファイルマージの頻度が過度に高くなり、CPU とディスク I/O リソースが過剰に消費され、HBase クラスタ全体のパフォーマンスに影響します。 Compactが業務処理時間に与える影響に対処するため、Compact操作にレート制限を導入しました。複数のテストにおいて、前述の2つのレート制限しきい値を調整することで、優れた結果を達成しました。Compact中の処理時間は70%以上短縮されました。次の図は、レート制限導入前後の時間比較を示しています。

4.5 フィールドレベルのバージョン管理

上記の最適化に加えて、将来の最適化に向けてHBaseの他の機能もいくつか調査しました。前述のように、HBaseから列単位でデータを読み取ることで、`get`クエリにかかる時間を短縮できます。一般的に、列(つまり各フィールド)は各データエントリの最も基本的な単位です。しかし、HBaseのデータの粒度は従来のデータ構造よりも細かくなっています。同じ場所にあるデータは、さらに複数のバージョンに分割されます。1つの列に複数のバージョンの値を格納でき、それらは複数のセルに格納されます。バージョンはバージョン番号によって区別されます。したがって、結果を一意に識別する式は、`rowkey:column family:column:version` である必要があります。ただし、バージョン番号は通常オプションです。書き込み時にバージョン番号が指定されていない場合、各列またはセルの値にはタイムスタンプが割り当てられます。これはシステムにデフォルトで設定されます。もちろん、ユーザーが書き込み時に特定のバージョン番号を明示的に指定することもできます。バージョン番号を指定せずにクエリを実行した場合、HBaseはデフォルトでデータの最新バージョンを返します。あるいは、バージョン番号を指定して、データの他のバージョンを返すこともできます。簡略化した図を以下に示します。

データバージョンの過剰による不要な負担を回避するため、HBase はデータバージョンの再利用に 2 つの方法を提供しています。1 つは、過去 n バージョンを数量単位で保存する方法、もう 1 つは、1 か月分など、最新のバージョンデータを時間単位で保存する方法です。複数のバージョンを同時に保存することは、時系列データが必要なシナリオで非常に役立ちます。バージョンのタイムスタンプを指定することにより、新しいデータが更新されても古いデータが上書きされるのを防ぎます。現在、私たちのテーブルでは 1 つのバージョンのみを指定しており、タイムスタンプをバージョン番号として持つデフォルトバージョンを使用しており、バージョン管理対策は実装していません。異なるセルに複数のバージョンを記録する機能は、フィールドを更新する際にデータの複数のバージョンを記録する際に活用することが考えられます。これにより、読み書きの効率に影響を与えることなく、関連するログがない場合でも最新の更新を簡単に遡ることができ、ユーザーが以前のバージョンに復元できるため、誤操作やデータ破損を防ぐことができます。さらに、私たちのシステムには、メッセージキューを介した非同期更新シナリオがいくつかあります。この場合、メッセージ本文のタイムスタンプを現在のバージョン番号として使用できます。これにより、低いバージョン番号は高いバージョン番号を更新できないため、マルチスレッドのシナリオでも消費の適時性が保証されます。

V. 要約

本稿では、統合コンテンツプラットフォームのデータベース選定分析と最適化に基づき、HBaseの実用化における最適化手法をいくつか簡単に紹介します。最適化後、プロジェクト全体の読み取り・書き込みパフォーマンスが大幅に向上し、統合コンテンツプラットフォームの事業安定性が確保され、事業側の運用コストも大幅に削減されます。HBase自体は、ビッグデータ分野において強力な機能と独自の優位性を有しています。しかし、HBaseに求められる要件はビジネスシナリオによって異なります。具体的な状況に応じて、使用するデータベースバージョン、基盤となるHBaseメカニズムのパラメータ調整、クライアント側呼び出しメカニズムの最適化など、様々な側面を考慮し、より適切なアプローチを模索することをお勧めします。本稿で紹介した最適化手法が、読者の皆様に少しでもヒントとなれば幸いです。