目次
Falcon ストレージの最適化: 高性能インメモリ TSDB の誕生
Falcon ストレージの最適化: 高性能インメモリ TSDB の誕生
TSDB
まずTSDB(Time Series Database)とは何かについてお話します 以下の定義はWikipediaより引用
時系列データベース (TSDB) は、時系列データ、つまり時間 (日時または日時の範囲) によってインデックス付けされた数値の配列を処理するために最適化されたソフトウェア システムです。
Influxdb
、、、などはGraphite
、現在一般的な TSDB 実装です。 RRDtool
OpenTSDB
TSDB は主に監視システムなどのシナリオで使用されます。詳細については、私のコード フォーラムをフォローしてください...
バックグラウンド
Didi の監視システムのストレージ部分の中核は open-falcon をベースに開発されていると述べました。
いくつかの最適化がコアに対して行われていますRRDtool
が、その固有の欠点があるため、否定的なレビューを覆すことは非常に困難です。詳細については、私のコード フォーラムをフォローしてください...graph
高资源需求
低性能产出
グラフの死
1. グラフのリソース要件io
. Didi の監視量の観点から見ると、グラフ インスタンスのピーク IOPS はそれと同じくらい高くなります50k+
. Didi グラフで使用されるモデルは、nvme ディスクを使用する最高構成モデルであるため、よく使用されます。予算の申請、チャレンジ。单 series 单文件
根本的には、使用されるストレージ モデルに他なりません。
2. キャッシュ データ構造の選択では、container/list
コア データ構造としてグラフが選択されます。小規模なプロジェクトでは、container/list
キュー シーンによく使用される選択項目の 1 つです。ただし、containerlist の改善で説明されているように、一時メモリの消費量も非常に多くなります。
3. TSDB のコアは、行形式、列形式など、どのように構成されていてもkey
+ 構造にすぎません。 (t, v)...
を生成する方法key
、つまりseries
ペアの一意の識別子が必要です。graph
で大量に使用されており、sha1
この md5
ような世代への加密hash
参加は非常に無駄であり、鶏を殺すには多額の費用がかかります。key
4. まだ議論中ですkey
。グラフの識別には tsdb series
、いわゆるラベルlabels
、つまり k1=v1, k2=v2...
この種のキーと値のペアが使用されます。グラフ内のキーとラベル間のマッピングは、 ラベル の字符串连接
++ によって生成されます。この変換 ( ) は、読み取りおよび書き込みパス全体の各モジュールに多数存在します。そして、この種の操作はメモリ消費量が多いか、CPU キラーのどちらかです。sort()
md5()
map <-> string
監視データが継続的に増加するため、一連のデータが到着すると3亿+
、グラフのメモリ、CPU、その他の指標は楽観的ではなくなり、IO は高いままになります。その結果、クエリのレイテンシーが徐々に増加し、写真、マーケット、その他の機能の表示エクスペリエンスが低下しました。
上記のプレッシャーに直面して、私たちは一連の分析を実施しましたが、統計によると、クエリ リクエストの一部は80%
過去 2 時間のデータをクエリするリクエストでした。これがgraph
最大のストレス源でもあります。
したがって、必要なのは简单
、 、のモジュール高性能
、易维护
および短期データ用の高性能読み取りおよび書き込みソリューションです。
柳は暗く、花は明るい
調査中に、prometheus の新しいストレージ エンジンのリリース中に prometheus の作者によって公開された記事「Writing a Time Series Database from Scratch」を見つけました。記事の内容自体はとても良いものなので、皆さんもぜひたくさん読んでみてください。詳細については、私のコード フォーラムをフォローしてください...
この記事で私が最も興味を惹かれたのは、次の一文です。
Gorilla TSDB に関する Facebook の論文では、同様のチャンクベースのアプローチについて説明し、16 バイトのサンプルを平均 1.37 バイトに削減する圧縮形式を導入しています。
時系列データベースの基本構成として、(t, v)
最も基本的な点を表していることが分かります。一般にメモリを消費します16 bytes
が、圧縮できれば1.37 bytes
圧縮率は非常に高くなります。
Facebook のこの論文「Gorilla: A Fast, Scalable, In-Memory Time Series Database」をお読みください。その高い圧縮率を要約する主な手段は次のとおりです。
1. dod (デルタのデルタ) エンコードと圧縮を使用します timestamp
。本来は保存する必要があります 1551532883, 1551532890, 1551532900, 1551532910
が、エンコード後は保存するだけで 1551532883, 7, 3, 0
圧縮効果が見られます。falconシステムでは、タイムスタンプの剰余アライメントを行うフロントモジュール(転送)があるため、dodアルゴリズムの効果が非常に優れています。詳細については、私のコード フォーラムをフォローしてください...
2. XOR エンコード、圧縮を使用します value
。これは一連のタイムスタンプに基づいており、value
隣接するタイムスタンプのほとんどはほとんど変化しません。XOR エンコードでは、2 つの隣接するタイムスタンプが同じ場合 、「0」を格納し、1 ビットのみを占有し、value
エンコード前のfloat64
スペースの2 ビットを占有します。
高い圧縮率に加えて、gorilla
説明されている In-Memory TSDB
アーキテクチャも非常に参考になる、典型的な数据分片
+ 多blocks轮转
構造です。論文を何度も読んで学ぶことをお勧めします。
キャッシュサーバーの誕生
圧縮アルゴリズムの実装
私も論文を読んで原理を理解し、次のステップはそれを実現することでした。車輪の再発明をしないのが私たちの素晴らしい伝統でした。既製のホイールを検索するには、github のゲイハブを起動します。私は dgryski/go-tsz ライブラリを見つけました。これは(t, v)
Facebook の論文で説明されているエンコード部分を完全に実装しています。この Damian Gryski は多くの優れたアルゴリズムを実装しており、それだけの価値があります follow
。しかし、このライブラリには小さな問題があります。キー構造体の一部のフィールドがエクスポートされず、golang の rpc 仕様要件を満たしていないためです。そこで、いくつかの小さな変更を加えて、 devtoolkits/go-tsz に追加しました。
データモデルの実装
理解を容易にするために、キャッシュサーバーのデータ モデルをボトムアップで説明します。
シリーズデータフロー
データの最下層は、シリーズのエンコードを通じて取得される、シリーズのデータ ストリーム (ビット) です(t, v)...
。
チャンク/チャンク
いわゆるチャンクは时间窗口
、シリーズ内の特定のシリーズのビット ストリーム データであり、チャンクはその名前が示すように、複数のチャンクをカプセル化したものです。一般に、チャンクのタイム ウィンドウが長いほど、出力圧縮結果は小さくなります。エンコードおよびデコードの効率などの要素を考慮すると、タイム ウィンドウの設定は次の原則を参照できます。詳細については、コード フォーラムに従ってください。 。
1. 異なる時間枠でそれぞれが占める平均スペースをテストします (t, v)
。テスト結果によるとfacebook
、時間ウィンドウは に設定されており 120 分钟
、(t, v)
それぞれが占める平均スペースはおよそ です 1.37bytes
。より大きな時間ウィンドウの圧縮効果はあまり明らかではありません。簡単なテストの後、キャッシュサーバーのチャンク時間ウィンドウも 120 分に設定されます。
2. 理論上、チャンクはチャンクを含むスライスですが、頻繁に追加されるスライスは gc にとって不向きです。したがって、不要なチャンクを確実に削除できるメカニズムが必要です清理
。キャッシュサーバー内のチャンクは、 Ringbuffer
チャンクを格納するための構造と、固定的空间
複数の時間ウィンドウを格納するためのシリーズ ストリームを使用します。
この部分の実装の大部分は grafana/metrictank から借用したものなので、興味のある学生は参照してください。
キャッシュ構造
以上が1系列の構成である。複数シリーズのストレージ、つまり 複数key
+ (t, v)...
のストレージ設計について話しましょmap
う もちろん、golang のデータ構造はこのシナリオに最適です。
しかし、別の問題があり、cacheserver の各インスタンスはシリーズを保存できるように設計されています 1000w ~ 2000w
。このレベルでは、マップの光学ストレージを使用することに問題はありませんが、このマップでは多数の読み取り/書き込み操作も必要となるため、読み取り/書き込みロックを使用する必要があります。当社のビジネス シナリオでは、一度に数万のシリーズがクエリされるシナリオが多く、連続書き込み操作と相まって、ロックの競争は非常に熾烈になります。この問題をどうやって解決すればいいでしょうか?
一般的な方法は、 を使用することです分片锁
。いわゆるシャード ロックは、マップのキーを実行して分片
から読み取り/書き込み操作を組み込み、最初に分片
アルゴリズムを実行してキーが配置されているシャードを見つけてから、取得したシャードをロックします。
読み取りを例にとると、元の から RLock -> Read -> RUnlock
、シャードロックの最適化後、 になります getShard -> shard.RLock -> Read -> shard.RUnlock
。後者は、同時実行性が高いシナリオでははるかに効率的です。
シャーディングのアルゴリズムも非常に重要で、key
整数の場合はシャードの数に応じて余りを計算するだけです。しかし、私たちのものkey
は文字列です。文字列から整数型への変換メソッドはたくさんあります。必要なのはメソッドです速度快
。占用内存低
コアコードは次のとおりです:
func fnv32(key string) uint32 {
hash := uint32(2166136261)
const prime32 = uint32(16777619)
for i := 0; i < len(key); i++ {
hash *= prime32
hash ^= uint32(key[i])
}
return hash
}
上記はキャッシュサーバーの中核となるデータモデルですが、具体的なデータの流れをリードパスとライトパスの2つの観点から簡単に説明します。
書き込みパス
rpc Call
--> cache getshard
--> chunks push
--> chunk locate
--> chunk push
読み取りパス
rpc Call
--> cache getshard
--> chunks get
--> chunk locate
--> get Iters
Iters をいつ解凍するか
読み取りパスでは、 dataIter
を含むものが取得される ため、圧縮パッケージをどこで解凍するかには 2 つのオプションがあります。 (t, v)...
压缩包
1. によって rpc server
、つまり cacheserver
解凍します。
2. rpc client
減圧による。
サーバーで解凍する利点は、クライアントが平文 (t, v)...
データを取得できることです。これは直感的で便利です。
クライアントで解凍する利点は、サーバーのリソースを節約できることです。
もう一度考えてみましょう。キャッシュサーバーのリソースオーバーヘッドを節約するために、後者、つまり rpc client
キャッシュ内で解凍を実行します。その後、uber のオープンソースm3db
にもこの側面の記述があり、私の考えと一致しました 以下は uber のブログ記事「The Billion Data Point Challenge: Building a Query Engine for High Cardinality Time Series Data」からの引用です
評価プロセスから得られた重要な洞察の 1 つは、内部でデータを圧縮したままにするストレージ バックエンドを扱う場合、フェッチ時にデータを解凍すべきではないということでした。これはまさに M3DB がデータを保存する方法です。解凍をできるだけ長く遅らせれば、メモリ フットプリントを削減できる可能性があります。
RPC エンコード/デコード
gob
golang のデフォルトの rpc エンコードおよびデコード スキームとしては、そのパフォーマンスが比較的低く、pprof によって生成されたフレーム グラフでの多数の寛大な gob エンコード/デコード呼び出しによっても証明されています。キャッシュサーバーでは、その特徴であるmsgpackコーデックを使用します It's like JSON. but fast and small.
。
キーの構成
前述したように、グラフはkey
組み合わせlabels
によって計算され、ビジネス上の意味を持ちます。cacheserver
それは異なります。key
ビジネス上の意味はなく、単にシリーズを識別するだけであり、上流モジュールと下流モジュール (転送とクエリ) の間で合意に達することだけが必要であり、キャッシュサーバーは気にしません。これにより、より一般的になるだけでなく、ストレージとインデックス作成の間の結合が分離され、map <-> string
既存のソリューションでの変換によって生じる不必要なオーバーヘッドも削減されます。
なぜ注文しないのか
上で説明したように、私たちのアーキテクチャ全体は に基づいており内存
、いかなる説明も必要としません持久化
。理由はいくつかあります。
1. 永続化によりキャッシュサーバーの複雑さが増す 永続化にはファイルの読み書きやWALなどを考慮する必要があり、比較的面倒です。開発コストと時間コストは比較的高くなります。
2. 永続化後、キャッシュサーバーは完全な TSDB になりますが、これは本来の意図ではありません。次の章で詳しく説明します。
グラフおよび関連統計との関係
cacheserver
本来の設計意図はグラフの補足です。目標は、グラフへの負担を軽減するサービス高性能
を作成することです。永続ストレージ ソリューションとして、graph は、、などのロジックを実装します。 低成本
提供热数据查询
将采
归档
持久化
リンクを読み取るときは、まずキャッシュサーバーを読み取り、キャッシュサーバー 数据不足
が OK の場合は出现异常
、次にグラフから読み取ります。
書き込みリンクはopen-falconの設計に従い、転送時に二重書き込みを行い、グラフへの書き込み時にキャッシュサーバーにも書き込みます。
20
インスタンス をデプロイします。
-
ストアド
2亿活跃 series
、インスタンスごとの平均ストレージ1000w series
-
使用される合計メモリは
1 TB
、平均して各インスタンスよりも少ない です50 GB
-
latency(tp95)
以下にお問い合わせください200ms
_
終わり
詳細については、私のコード フォーラムをフォローしてください...
この記事は、創造を尊重する Open-Falcon オープン ソース コミュニティからのものです。