Rowkey設計は、ApsaraDB for HBaseのパフォーマンスに影響を与える重要な要素です。次のテキストでは、一般的な問題とその解決策について説明します。
行は、rowkeyによって辞書順にソートされます。この設計により、スキャンが最適化され、一緒に読み取られる関連行または隣接行を格納できます。ただし、不適切なrowkey設計は、ホットスポッティングの一般的な原因です。ホットスポッティングは、大量のトラフィックがクラスター内の1つまたは少数のノードに集中した場合に発生します。トラフィックとは、読み取りや書き込みなどの操作を指します。リージョンをホストするサーバーにトラフィックが過負荷になると、リージョンのパフォーマンスが低下し、リージョンが使用できなくなることさえあります。また、サーバーが要求された負荷に対してサービスを提供できないため、同じリージョンサーバー内の他のリージョンにも悪影響を与える可能性があります。したがって、クラスター全体に負荷を均等に分散できるデータアクセスパターンを設計することが重要です。
書き込み操作中のホットスポッティングを防ぐには、できるだけ多くのリージョンに同時にデータを書き込めるようにrowkeyを設計する必要があります。データが1つのリージョンにある必要がある場合を除き、1つのリージョンにのみデータを書き込むことは避けてください。
以下のセクションでは、ホットスポッティングを防ぐための一般的な方法について説明します。各メソッドには、それぞれ長所と短所があります。
HBaseでのソルティングとは、rowkeyの先頭に乱数を配置することです。この操作により、各rowkeyにプレフィックスがランダムに割り当てられ、通常とは異なるソートが行われます。可能なプレフィックスの数は、データを分散するリージョンの数に対応します。他のより均等に分散された行に繰り返し表示されるrowkeyパターンに気付いた場合は、ソルティングを使用することをお勧めします。次の例では、ソルティングによって複数のリージョンサーバーに負荷が分散されます。この例では、ソルティングが読み取り操作に及ぼす悪影響についても示しています。
次の表は、プレフィックスに基づいてリージョンに分割されたいくつかのrowkeyを示しています。たとえば、プレフィックス「a」を持つrowkeyはリージョンAに分散され、プレフィックス「b」を持つrowkeyはリージョンBに分散されます。次の表のrowkeyはすべて「f」で始まります。したがって、これらの行は単一のリージョンに分散されます。
foo0001
foo0002
foo0003
foo0004
異なるリージョンに均等に行を分散するには、a、b、c、dの4つのソルトが必要です。各文字プレフィックスは異なるリージョンに対応し、4つの異なるリージョンにrowkeyを分散します。次のrowkeyにはそれぞれ異なる文字のプレフィックスが付いており、4つの異なるリージョンに同時に書き込まれます。スループットは、すべてのデータを1つのリージョンに書き込む場合の4倍です。
a-foo0003
b-foo0001
c-foo0004
d-foo0002
新しい行を挿入すると、4つの可能なソルト値のいずれかからランダムなプレフィックスが行に割り当てられます。
a-foo0003
b-foo0001
c-foo0003
c-foo0004
d-foo0002
ソルティングが実行されると、プレフィックスがランダムに割り当てられ、書き込み操作のスループットが向上します。ただし、行の元の順序が影響を受け、読み取り操作のワークロードが増加します。
ハッシュ
ソルティングと比較して、ハッシュは、ランダムに生成されたプレフィックスではなく、一方向ハッシュを使用して一貫性のあるプレフィックスを生成することです。これにより、リージョンサーバー全体に負荷を分散する方法で特定の行に同じプレフィックスを指定できますが、読み取り操作中に予測可能性を確保できます。確定的なハッシュを使用して、クライアント上でrowkeyをリファクタリングし、GET操作を使用して行を取得できます。
ソルティングのセクションで説明した例を見てみましょう。一方向ハッシュを使用してrowkey foo0003を取得し、プレフィックス a を予測できます。次に、rowkeyとプレフィックスを組み合わせて行を取得できます。このメソッドはさらに最適化できます。たとえば、特定のrowkeyペアを常に同じリージョンに配置します。
キーを反転する
ホットスポッティングを防ぐために使用されるもう1つの一般的な方法は、固定長または数値のrowkeyを反転して、最も頻繁に変更される部分(最下位桁)を前面に配置することです。これにより、行の順序を犠牲にしてrowkeyがランダム化されます。
単調に増加するrowkeyまたは時系列データ
ApsaraDB for HBaseクラスターにデータが書き込まれると、プロセスはロックされます。この間、すべてのクライアントはリージョン(単一ノード)のロックが解除されるのを待ちます。書き込み操作が完了すると、サイクルが再び開始されます。この問題は、単調に増加するデータまたは時系列データがrowkeyとして使用される場合によく発生します。これは、順次rowkeyにも当てはまります。順次rowkeyは、順次でないデータを順次に並べ替え、ホットスポッティングを引き起こします。したがって、タイムスタンプまたはシーケンス(たとえば、1、2、3)をrowkeyとして使用することは避けてください。
時間順に並べ替えられたファイル(ログなど)をApsaraDB for HBaseにインポートする必要がある場合は、OpenTSDBドキュメントを参照することをお勧めします。ドキュメントには、ApsaraDB for HBaseのパターンについて説明したページが含まれています。OpenTSDBのrowkeyの形式は[metric_type] [event_timestamp]です。一見、これはタイムスタンプをrowkeyとして使用しないという考え方に矛盾しているように見えます。ただし、OpenTSDBはevent_timestampの前にmetric_typeを配置します。負荷をリージョン全体に分散するのに十分な数百のmetric_type値があります。したがって、連続データ入力ストリームにもかかわらず、PUT操作は引き続きテーブルのさまざまなリージョンに分散できます。
行と列のサイズを最小化する
ApsaraDB for HBaseでは、値はシステムのセルとして格納されます。セルを見つけるには、行、列名、およびタイムスタンプを知る必要があります。通常、行または列名のサイズが大きすぎる場合、または値のサイズよりも大きい場合、興味深いシナリオが発生する可能性があります。ApsaraDB for HBaseのストアファイルには、値へのランダムアクセスを容易にするために使用されるインデックスがあります。ただし、セルにアクセスするために必要な座標が大きすぎると、インデックスが大量のメモリを消費し、最終的に使い果たされる可能性があります。この問題を解決するには、リージョンサイズを大きく設定するか、より小さい行と列名を使用します。圧縮を使用してこの問題をより大きく解決することもできます。
ほとんどの場合、わずかな非効率性はパフォーマンスに大きな影響を与えません。ただし、ビッグデータのシナリオでは、無視することはできません。列ファミリー、プロパティ、およびrowkeyは、データ内で数億回繰り返される可能性があるためです。
列ファミリー
列ファミリーの名前はできるだけ短くしてください。1文字のみを使用することをお勧めします。(たとえば、fを使用します)
プロパティ
詳細なプロパティ名(myVeryImportantAttributeなど)は理解しやすいですが、ApsaraDB for HBaseでは短いプロパティ名(viaなど)を使用することをお勧めします。
Rowkeyの長さ
rowkeyは読み取り可能な程度に短くしてください。これはデータの取得に役立ちます。(たとえば、GetとScan)。短いrowkeyはデータアクセスには役に立ちませんが、get / scanの取得機能の向上については、長いrowkeyよりも優れているわけではありません。rowkeyを設計する際にはトレードオフが必要です。
バイトパターン
long型は8バイトです。8バイト以内に最大18,446,744,073,709,551,615の符号なし整数を保存できます。上記の数字を文字列として格納する場合、各文字が1バイトを占めると仮定すると、数字を格納するために必要なバイト数は元の約3倍になります。
次のサンプルコードを使用して、これをテストできます。
// long 型
//
long l = 1234567890L;
byte[] lb = Bytes.toBytes(l);
System.out.println("long 型のバイト長: " + lb.length); // 8 を返します
String s = String.valueOf(l);
byte[] sb = Bytes.toBytes(s);
System.out.println("文字列としての long 型の長さ: " + sb.length); // 10 を返します
// ハッシュ
//
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(Bytes.toBytes(s));
System.out.println("md5 ダイジェストのバイト長: " + digest.length); // 16 を返します
String sDigest = new String(digest);
byte[] sbDigest = Bytes.toBytes(sDigest);
System.out.println("文字列としての md5 ダイジェストの長さ: " + sbDigest.length); // 26 を返します
ただし、バイナリ表現では、コード以外でデータを読み取ることが難しくなります。次の例では、値を追加するときにシェルが表示されます。
hbase(main):001:0> incr 't', 'r', 'f:q', 1
COUNTER VALUE = 1
hbase(main):002:0> get 't', 'r'
COLUMN CELL
f:q timestamp=1369163040570, value=\x00\x00\x00\x00\x00\x00\x00\x01
1 row(s) in 0.0310 seconds
シェルは文字列を出力しようとしますが、この場合は16進数しか出力できません。rowkeyがリージョン内にある場合も同じことが起こります。格納されている内容がわかっていれば問題ありませんが、同じセルにデータを入れることができる場合、結果を理解するのが難しい場合があります。これが最も重要な考慮事項です。
タイムスタンプを反転する
データベースの一般的な問題は、行の最新バージョンを見つけることです。この場合、反転されたタイムスタンプをrowkeyの一部として使用して、ソートを容易にすることができます。このテクノロジーには、キーの末尾に(Long.MAX_VALUE-timestamp)を追加することが含まれます。たとえば、[key] [reverse_timestamp]です。
[key]を使用して、テーブル内の最新の[key]の値をスキャンし、最初のレコードを取得できます。ApsaraDB for HBaseのrowkeyは順番にソートされるため、キーは最初であり、他の古いrowkeyの前にあります。
この手法は、すべてのバージョンを永続的に保存するために(または長期間にわたって)バージョン番号を要求する代わりに使用できます。さらに、同じスキャン手法を使用して他のバージョンをすばやく取得できます。
Rowkeyと列ファミリー
rowkeyは列ファミリー内にあります。したがって、同じrowkeyは、競合することなく、同じテーブルの各列ファミリーに存在できます。
Rowkeyは変更できません
Rowkeyは変更できません。rowkeyを変更する唯一の方法は、削除してから新しいrowkeyを挿入することです。最初から(または大量のデータを挿入する前に)適切に設計されたrowkeyを使用することをお勧めします。
rowkeyとリージョンスプリットの関係
テーブルが事前に分割されている場合、次の手順は、rowkeyがリージョン境界全体にどのように分散されているかを理解することです。rowkeyの重要な部分として表示可能な16ビット文字を使用して、その重要性を説明することを検討してください。たとえば、0000000000000000からffffffffffffffffまでです。Bytes.splitを使用してキー範囲を指定することにより、10個のリージョンを取得できます。これは、Admin.createTable(byte [] startKey、byte [] endKey、numRegions)でリージョンを作成するときの分割方法です。
48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 // 0
54 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 // 6
61 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -68 // =
68 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -126 // D
75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 72 // K
82 18 18 18 18 18 18 18 18 18 18 18 18 18 18 14 // R
88 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -44 // X
95 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -102 // _
102 102 102 102 102 102 102 102 102 102 102 102 102 102 102 102 // f
問題は、データが最初の2つのリージョンと最後のリージョンに格納されるため、データ分布が不均一であるためにこれらのリージョンに大きなワークロードが発生することです。理由を理解するには、ASCIIテーブルを参照できます。ASCIIテーブルに基づくと、0は48番目、fは102番目です。[0-9]と[a-f]の範囲の値のみが意味を持つため、58番目から96番目の範囲の値はキースペースに表示されず、この範囲内の中央のリージョンは使用されません。この例でキースペースを事前に分割するには、分割をカスタマイズする必要があります。
チュートリアル1:テーブルの事前分割は良い習慣です。ただし、テーブルを事前に分割する場合は、すべてのリージョンに対応するキースペースがあることを確認してください。前の例は16ビットキーのキースペースに関するものですが、解決策は他のキースペースにも適用できます。
チュートリアル2:16ビットキーは推奨されませんが(通常は表示できるデータに使用されます)、すべてのリージョンに対応するキースペースがある場合は、事前分割テーブルで使用できます。
次のケースは、16ビットrowkeyを事前に分割する方法を示しています。
public static boolean createTable(Admin admin, HTableDescriptor table, byte[][] splits)
throws IOException {
try {
admin.createTable( table, splits );
return true;
} catch (TableExistsException e) {
logger.info("テーブル " + table.getNameAsString() + " は既に存在します");
// テーブルは既に存在します...
return false;
}
}
public static byte[][] getHexSplits(String startKey, String endKey, int numRegions) {
byte[][] splits = new byte[numRegions-1][];
BigInteger lowestKey = new BigInteger(startKey, 16);
BigInteger highestKey = new BigInteger(endKey, 16);
BigInteger range = highestKey.subtract(lowestKey);
BigInteger regionIncrement = range.divide(BigInteger.valueOf(numRegions));
lowestKey = lowestKey.add(regionIncrement);
for(int i=0; i < numRegions-1;i++) {
BigInteger key = lowestKey.add(regionIncrement.multiply(BigInteger.valueOf(i)));
byte[] b = String.format("%016x", key).getBytes();
splits[i] = b;
}
return splits;
}