DUICUO

Meitu のオリジナルのオープンソース キーバリュー ストレージである Titan についてお話しましょう。

市場にはオープンソースのキーバリュー(KV)ライブラリが数多く存在します。アーキテクチャ面では、いずれもRocksDBを単一マシンエンジンとして利用し、その上にRedisプロトコルをサポートするプロキシレイヤーを配置するか、特定のビジネスロジックに応じてデータ型をカスタマイズできます。テーブル指向のものもあれば、列指向のストレージもあります。

国内企業の多くは自社ツールを保有しており、第一世代を開発しKPIを達成した後、撤退する。第二世代は不足部分を補い続ける一方で、第三世代、第四世代は周縁化していく。オープンソースであっても、継続的なメンテナンスは困難だ。例えば、本稿で紹介するMeitu Titan[1]は、最適化提案[2]をあまり実装していない。しかし、学習プロジェクトとして学ぶ価値はある。いつか再び開発されるかもしれない。

全体的なアーキテクチャ

Titanは17,000行のコードで構成され、すべてGo言語で実装されています。サーバー層は、ユーザーリクエストの処理と、Redisデータ構造をRocskDBのキー/値ペアにマッピングする役割のみを担っています。基盤層ではTiKVクラスターが使用されています。

Titan は巨人の肩の上に立っているため、データの再バランスを考慮したり、データ ストレージのレプリカの同期を心配したりする必要がなく、そのためコードベースが非常に小さくなっています。

負荷テスト[3]は2018年のデータのみを使用しており、パフォーマンスは平均的です。レイテンシは99パーセンタイルと95パーセンタイルで区別されていません。最新バージョンのTiKVクラスタに基づいてテストを実施すれば、結果は改善される可能性があります。

データ型の実装

現在、データ構造は文字列、セット、zset、ハッシュ、リストのみを実装しており、一部は部分的にしかサポートされていませんが、それでもほとんどのニーズには十分です。

永続的なキーと値のペアを扱う際の課題は、Redisのデータ構造をRocksDBのキーと値のペアにマッピングすることです。単一プロセスアーキテクチャに固有のアトミック性を実現することは困難です。データの維持には複数のキーが関係し、複数のインスタンスプロセスに分散されている場合、分散トランザクションが発生し、スループットが大幅に低下します。

多くの場合、Lua スクリプトを使用してビジネス ロジックをカスタマイズし、ハッシュ タグを使用して複数のキーを処理して単一の Redis スロットを作成しますが、これは Titan では不可能です。

Redisでは通常O(1)演算であるHLEN演算などのパフォーマンス上の問題は、lenレコードがTitanのハッシュメタキーで管理されている場合、高並列性のハッシュ書き込みおよび削除操作中に多数の衝突を引き起こす可能性があります。別の例としては、zsetデータ構造、zrange、zrangebyscore、zrangebylexが挙げられます。これらの構造では、メンバーとスコアを別々にエンコードして保存する必要があるため、メモリ容量と時間をトレードオフすることになります。

String 型には、MetaKey と ExpireKey の 2 種類のキーのみがあります。

MetaKey では、マルチテナント分離を実装するために名前空間が使用されますが、これは論理的なものであり、結局のところ、リソースは共有されます。dbid は Redis の db0、db1 などに似ています。

ExpireKey は、データをアクティブに期限切れにするために使用されます。バックグラウンドタスクが定期的にスキャンします。ExpireKey は各タイプに存在しますが、ここでは省略します。

MetaValueの最初の42バイトには属性情報が含まれており、その後に実際のユーザー値が続きます。timeフィールドは作成、更新、および有効期限のタイムスタンプを示します。パッシブ有効期限の場合はExpireAtがチェックされます。UUIDはキーを一意に識別するために使用され、titanはアクティブガベージコレクションに使用されます。

タイプはデータのタイプを示します。

定数(
ObjectString = ObjectType ( iota )
オブジェクトリスト
オブジェクトセット
オブジェクトZセット
オブジェクトハッシュ

エンコーディングは特定のエンコーディング タイプを示します。

定数(
ObjectEncodingRaw = ObjectEncoding ( iota )
オブジェクトエンコーディングInt
オブジェクトエンコーディングHT
オブジェクトエンコーディングジップマップ
オブジェクトエンコーディングリンクリスト
オブジェクトエンコーディングジップリスト
オブジェクトエンコーディングインセット
オブジェクトエンコーディングスキップリスト
オブジェクトエンコーディングエンバスト
オブジェクトエンコーディングクイックリスト

互換性のため、定義は Redis と一致しています。

セット

String型と同様に、MetaKeyとMetaValueはどちらも50バイトの長さです。最初の42バイトは同じで、最後の8バイトはSetのメンバー数を保持します。つまり、後続のSCARD操作はO(1)ですが、削除と追加はどちらもMetaValueの変更を必要とします。

DataKeyは、セットの一意のUUIDとメンバー情報をエンコードします。セットに必要なのはメンバーのみなので、DataValueは[]byte{0}です。

Zセット

セットと同様に、zset MetaKey/MetaValue の内容は同じです。

DataKeyの内容は基本的に同じですが、DataValueはスコア値です。また、スコア -> メンバーマッピング用のScoreKeyも保持しており、これはzrangebyscoreクエリを容易にするために、メモリ容量と時間を節約するために使用されます。

ハッシュ

ここでのハッシュのMetaValueはメンバーのLen情報を保持していないため、HLENの場合は範囲​​のデータキー空間全体を走査する必要があることに注意してください。なぜこのような処理をするのでしょうか?

Titanの作者は、ハッシュ操作は同時書き込み時に多数のトランザクション競合を引き起こすため、メンテナンスしないことを選択したと述べています。その後、競合を最小限に抑え、HELNのパフォーマンスを向上させるために、MetaKeyを複数のスロットに分割する解決策が提案されましたが、これは実装されませんでした。

リスト

リストには2つの構造があります。1つはziplist(値がpb(複数の要素がまとめてエンコードされた値))で、もう1つはlinkedlistです。現在の実装では、ziplistからlinkedlistへの変換は行われません。実際、永続的なストレージにはlinkedlistで十分です。

MetaValue の最後の24バイトは、それぞれ len、lindex、rindex を保持します。インデックスは float64 型です。なぜ int64 型ではないのですか?

理由は、`Linsert` 演算では (2, 3) の間に挿入すると失敗するのに対し、`float64` を使用すると成功する可能性が高いためです。ただし、`float64` にも精度の問題があるため、失敗する可能性は依然として残ります。

 // calculateIndex 左の間の実際のインデックスを返し ErrPerc =を返します。
function calculateIndex ( left , right float64 ) ( float64 , error ) {
f : = (+) / 2の場合 f !=&& f !={
f nil を返す
}
0 ErrPrecisionを返す
}

DataKey はインデックス情報をエンコードし、DataValue はその値です。

取引の競合

Titanは主に小規模なトランザクションを処理するため、TiKVトランザクションでは1PCとAsyncCommitが有効になり、全体的なスループットが向上します。競合するトランザクションについては、Titanは可能な限り何度も再試行することで、確実に実行を成功させます。

アフィニティの問題に関して言えば、Titanは特定のタイプのキーを単一のTiKVインスタンス内に格納することを目指していますが、これは現在実装されておらず、困難で、課題となっています。TiKVは永続的なキーと値のペアの開発の難易度を軽減する一方で、柔軟性を制限するとも言えます。

GCを削除

削除する場合は、MetaKey を削除します。TTL が存​​在する場合は、ExpireKey を削除します。文字列以外のオブジェクトの場合は、DataKey を sys 名前空間に移動します。

$sys{名前空間}:{sysデータベースID}:GC:{データキー}

バックグラウンドdoGCはgcDeleteRangeを呼び出してデータを段階的に削除します。DataKeyにはUUIDが含まれているため、重複する可能性は低く、ユーザーが同じキーを再作成しても影響を受けません。

Flushdb 操作も非常に重い処理です。理論的には、すべてのキーをエンコードする際にバージョン番号を含めることで、高速なフラッシュとロールバックが可能になります。

運用および保守周辺機器

コードのオープンソース化はほんの第一歩に過ぎません。堅牢なエコシステムを構築するには、多くの熟練した人材が必要です。現在、TiKVの運用とPingCapに関するドキュメントは豊富に存在しており、概ね十分です。重要なのは、パラメータを最適化することです。

監視、トラブルシューティング、およびカオス インジェクション テストの実行。

現時点では、データの一貫性検証、異種 Redis 同期などが欠けています。

まとめ

現在、Titan が真の本番稼働準備を完了するには、まだ数回の P0 障害が残っています。メモリ不足 (OOM) エラーによりメモリオーバーフローが発生し、トラフィックの急増によりクラスタが停止状態に陥っています。

コードにはまだ記述上の欠陥がいくつか残っています。このコードを使用し、二次開発を行う能力のある方は、クラスタ負荷テスト、フォールトインジェクション、レート制限を徹底的に実施してください。デプロイを急がず、いつでもロールバックできるように準備しておいてください。