Status | Authors | Coach | DRIs | Owning Stage | Created |
---|---|---|---|---|---|
proposed |
@ankitbhatnagar
|
@mappelman
|
@sebastienpahl
@nicholasklick
| monitor observability | 2022-11-09 |
- 要約
- 動機
- 提案
- デザインおよび実施内容
- 参考実装
- 対象環境
-
スキーマ設計
- 提案されたソリューション完全に正規化されたテーブルによる冗長性の削減と読み取り性能の向上
- プライマリ、非正規化データテーブル
- 時系列メタデータ/例題をサポートするメタデータテーブル
- ルックアップテーブル
- リファインメント
- 長所 - 複数のテーブル
- 短所 - 複数のテーブル
- オペレーション特性 - 複数テーブル
- ストレージ - 複数のテーブル
- 圧縮 - 複数のテーブル
- パフォーマンス - 複数テーブル
- 注意点
- 却下された代替案単一の集中テーブル
- 単一集中データテーブル
- 長所 - 単一テーブル
- 短所 - 単一テーブル
- オペレーション特性 - 単一テーブル
- ストレージ - 単一テーブル
- 圧縮 - 単一テーブル
- パフォーマンス - 単一テーブル
- 一般的なストレージに関する考察 - Clickhouse
- SQLによるデータアクセス
- データアクセスの例
- 書き込み
- 読み込み
- 生産準備
- 今後の展望
- ロードマップと次のステップ
GitLab Observabilityバックエンド -メトリクス
要約
Clickhouseを基礎ストレージとして使用し、一般的に広く受け入れられている業界標準フォーマットでフォーマットされた観測可能性データを、長期間のデータ保持と集計をサポートしながら保存しクエリするマルチユーザーシステムを開発。
動機
Observabilityの6つの柱、一般的にTEMPLE
(トレース、イベント、メトリクス、プロファイル、ログ&エラー)と略されるものから、メトリクスは現代のシステムにとってObservabilityデータの最も重要な柱の1つであり、ユーザーがオペレーション状況についてインサイトを収集するのに役立ちます。
一般的に時系列データとして構成されるメトリクスには、以下のような特徴があります:
- 対応するタイムスタンプによってインデックス化されます;
- 継続的にサイズが拡大します;
- 通常、集計、ダウンサンプル、範囲クエリ。
- 書き込み負荷が非常に高い
GitLab Observability Backendでは、お客様がシステムやアプリケーションの観測データを取り込んでクエリするためのサポートを追加し、システムの運用状態を改善できるようにすることを目指しています。
目標
提案システムの開発にあたり、以下の目標を掲げています:
-
反復可能なベンチマークによって性能が証明されたClickhouseに裏打ちされた、スケーラブルで低レイテンシー、かつコスト効率の良いモニタリングシステム。
-
Prometheus/OpenTelemetryフォーマットのメトリクスの長期保存をサポートし、Prometheus remote_write APIでインジェストし、Prometheus remote_read API、PromQL、SQLでクエリし、メタデータとエグザミナーをサポートします。
前述の目標は、さらに以下の4つのサブ目標に分けることができます:
データのインジェスト
- システムが大量の書き込みと読み込みをインジェストできるようにするためには、水平方向にスケーラブルであることと、一度インジェストされた書き込みが削除されないように耐久性を保証することが必要です。
データの永続化
-
私たちは、Prometheus
remote_write
プロトコルを使用して送信されたテレメトリ/データの取り込みをサポートすることを目指しています。データセットのために設計する永続性は、デフォルトでマルチテナントである必要があります。 -
私たちは、Prometheusのコンプライアンス・テスト・スイートがどのように与えられたメトリクス実装の正しさをチェックするかからヒントを得て、CIセットアップの一部としてそれを実行しながら、データの正しさのためのテスト・スイートを開発することを目指しています。
- また、特殊なPrometheusデータ型、例えばPrometheusヒストグラムやサマリーなどの互換性を確保することを目指しています。
データの読み込み
-
私たちはPromQLを使ったデータクエリをサポートすることを目指しています。そのためにはPromQLや MetricsQLパーサーを使用するのがよいでしょう。
- 私たちは、以下の信頼性を条件として、すべての取り込まれたデータをネイティブのClickhouse SQLインターフェースを介して公開することで、さらなる価値を提供することを目指しています:
- クエリ検証、サニテーション
- レート制限
- リソース制限 - メモリ、CPU、ネットワーク帯域幅
- Prometheusコンプライアンス・テスト・スイートを通じて、Prometheusのテスト・スーツに合格することを目標としています。
データの削除
- 私たちは、必要に応じて取り込まれたデータを削除できるようにすることを目指しています。これは、設定されたTTLが切れたり、それぞれの保持ポリシーが適用されたりしたときに、自然にデータを削除することに加えて行われます。私たちはスキーマの中で、ラベルやコンテンツによってデータを削除する方法を構築し、そのために必要なツールを追加しなければなりません。
非ゴール
上記で設定されたゴールと同時に、現在の提案で具体的にどのようなことが非ゴールであるかを設定したいと思います。それは
-
私たちは、OpenTelemetry/OpenMetrics フォーマットを使ったインジェストをサポートするつもりはありません。しかし、私たちのユーザーは、標準的な Prometheus
remote_write
プロトコルを消費する Opentelemetry エクスポーターを内部で使うことができます。詳細はこちら -
最初のイテレーションでは、Prometheusの模範解答のインジェストをサポートするつもりはありません。
提案
GitLab Observability Backendをメトリクス実装のフレームワークとして利用し、スケジューラやテナントオペレータなど既存のKubernetesコントローラでライフサイクルを管理できるようにします。
開発の観点からは、上記の “Application Server “としてマークされたものはこの提案の一部として開発される必要がありますが、残りの周辺コンポーネントは既に存在するか、scheduler
/tenant-operator
の既存のコードを通してプロビジョニングすることができます。
書き込みパスでは、HTTP
/gRPC
Ingress
を介して、例えばエラートラッキングやトレースなど、既存のサービスと同様に受信データを受け取ることが期待されます。
remote_write
API経由でデータを取り込む予定ですので、受信データはProtobufでエンコードされ、Snappyで圧縮されます。そのため、受信したデータはすべて解凍してデコードし、prompb.TimeSeries
オブジェクトのセットに変換する必要があります。また、Clickhouseに小さなデータをたくさん書き込まないようにする必要があるので、Clickhouseに書き込む前にデータをバッチ処理するのが賢明です。
また、特定のストレージ実装への過度な依存を減らすために、インジェストとStorage
。私たちは当面Clickhouseをバックストレージとして使用するつもりですが、将来のビジネス要件で別のバックエンドやテクノロジーを使用することになっても、Clickhouseに縛られすぎないようにするためです。Goでこれを実装する良い方法は、私たちの実装が標準的なインターフェイスに従うことです:
type Storage interface {
Read(
ctx context.Context,
request *prompb.ReadRequest
) (*prompb.ReadResponse, error)
Write(
ctx context.Context,
request *prompb.WriteRequest
) error
}
読み込みパスでは、ユーザーがPrometheusremote_read
APIを remote_read
使用remote_read
し、PromQLとSQLで取り込まれたデータをクエリ remote_read
できるようにすることを目指しています。APIのremote_read
サポートは remote_read
簡単に実装できますが、PromQLのサポートはSQLに変換する必要があります。しかし、既存のPromQL解析ライブラリを利用することができます。
私たちはクエリのバリデーションとサニテーションを実装し,レートを制限し,リソース消費を調整することで,基礎となるシステム,特にストレージが常に健全なオペレーションを維持できるようにすることを目指しています。
サポートされるデプロイメント
メトリクス・バックエンドの最初のイテレーションでは、可能な限り多くの利用状況を把握し、可能な限り早く製品のドッグフードを開始できるように、一般的なデプロイ・モデルをサポートするつもりです。これは前述のアーキテクチャ図によく示されています。
最もシンプルな形では、GitLab Observability BackendのメトリクスサポートはPrometheusのリモート読み書きAPIを使って使うことができます。もしユーザーがモニタリングの抽象化として既にPrometheusを使っているのであれば、このバックエンドを直接使うように設定することができます。
遠隔測定データのスクレイピングに Prometheus インスタンスを使用しないシステムのユーザーは、例えば OpenTelemetry コレクターや Prometheus エージェントのような多数のコレクター/エージェントを介してメトリクスをエクスポートすることができます。しかし、読み込みに関しては、GOB 内で Prometheus を実行し(アプリケーションサーバーと一緒に)、GitLab Observability UI(GOUI) で自動的にフックし、remote_read エンドポイントを利用するように設定するつもりです。
注目すべきは、GOBで実行されるPrometheusインスタンスを使うことができる一方で、クエリを実行するためのremote_read APIしかサポートできないことです。GOUIから直接PromQLやSQLクエリを実行できるようになれば、この追加コンポーネントを完全に取り除くことができるはずです。
グループごとのデプロイ:スケーラビリティの観点から、グループごとに Ingress、Prometheus インスタンス、アプリケーションサーバをデプロイし、各テナントのトラフィック量に応じてスケールできるようにしています。また、マルチテナントのシステムでテナント間のリソース消費を分離するのにも役立ちます。
メトリックの収集と保存
クライアント側でのメトリクス収集と、私たちの側で用意するストレージを分離することが重要です。
ストレージの最新技術
既存の長期Prometheus互換メトリクスベンダーは、Prometheus remote_writeと互換性のあるAPIを提供しています。
Prometheusクライアントの現状
Prometheus 自体、Grafana Cloud Agent、Datadog Agent などのメトリクス収集クライアントは、通常ファイアウォールで保護された環境内からメトリクスのエンドポイントをスクレイピングし、ローカルにスクレイピングされたメトリクスをWrite Ahead Log(WAL)に保存し、Prometheusremote_write
プロトコルを介して外部環境(ベンダーまたは Thanos のような内部管理システムなど)にバッチ送信します。
-
クライアントサイドコレクタは、アーキテクチャ全体の重要な部分ですが、ユーザー環境で実行する必要があるため、顧客/ユーザーが所有します。これにより、エンドユーザーはデータの収集方法と配信先を制御できるため、データを完全に制御できます。
-
ユーザーのファイヤーウォール環境内のエンドポイントにアクセスしてスクレイピングするための認証情報を外部ベンダーに提供することは実現不可能です。
-
また、Prometheusクライアントがアクセスできるように、
remote_write
APIが適切なレート制限ステータスコードで正しく応答することも非常に重要です。
Prometheusremote_write
の背景や歴史、Prometheusベースの観測性における重要性についてはこちらをご覧ください。
デザインおよび実施内容
以下は、提案するソリューションをどのように設計・実装することを目指すかの詳細です。この目的のために、問題の範囲を理解し、私たちの提案が情報に基づいた決定や実験の結果を中心に起草されていることを確認するための初期データを提供するために、参照実装も開発されました。
参考実装
対象環境
現在のオペレーション体制に合わせて、GitLab Observability Backendの一部としてメトリクスをデプロイする予定です:
- クラスター(ローカル開発者向け)
- GKEクラスター(ステージング/プロダクション環境用)
スキーマ設計
提案されたソリューション完全に正規化されたテーブルによる冗長性の削減と読み取り性能の向上
プライマリ、非正規化データテーブル
CREATE TABLE IF NOT EXISTS samples ON CLUSTER '{cluster}' (
series_id UUID,
timestamp DateTime64(3, 'UTC') CODEC(Delta(4), ZSTD),
value Float64 CODEC(Gorilla, ZSTD)
) ENGINE = ReplicatedMergeTree()
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (series_id, timestamp)
時系列メタデータ/例題をサポートするメタデータテーブル
CREATE TABLE IF NOT EXISTS samples_metadata ON CLUSTER '{cluster}' (
series_id UUID,
timestamp DateTime64(3, 'UTC') CODEC(Delta(4), ZSTD),
metadata Map(String, String) CODEC(ZSTD),
) ENGINE = ReplicatedMergeTree()
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (series_id, timestamp)
ルックアップテーブル
CREATE TABLE IF NOT EXISTS labels_to_series ON CLUSTER '{cluster}' (
labels Map(String, String) CODEC(ZSTD)
series_id UUID
) engine=ReplicatedMergeTree
PRIMARY KEY (labels, series_id)
CREATE TABLE IF NOT EXISTS group_to_series ON CLUSTER '{cluster}'' (
group_id Uint64,
series_id UUID,
) ORDER BY (group_id, series_id)
リファインメント
-
複数のテナントに固有のデータを同じデータベーステーブル内に配置する場合、データのインジェスト/パーシング時にテナントごとにシャーディングを考慮する必要があります。物事を単純化するために、テナント固有のデータを専用のテーブルセットに分離することは非常に理にかなっています。
-
テナント間でデータを取り込む際の「タイムスタンプ」の構造的な考慮事項。
-
作成時間と取り込み時間の比較
-
複数のテーブルにまたがる書き込みを効率的に管理するために、ネイティブクライアントではまだトランザクションをサポートしていません。
長所 - 複数のテーブル
-
正規化されたデータ構造により、データの効率的な保存が可能になり、あるタイムスリリーの複数のサンプルにまたがる冗長性が取り除かれます。明らかに、”samples” スキーマの場合、1 メトリックポイントあたり 32 バイトのデータを保存することになります。
-
ラベル/メタデータでタイムスリリーをフィルタリングする際、インデックス化されたカラムを使用することで、検索の複雑さが改善されます。
-
すべてのデータは一意な識別子で識別可能で、テーブル間のデータの一貫性をメンテナーすることができます。
短所 - 複数のテーブル
-
複数のテーブルにまたがる書き込みを考慮すると、書き込みは非常に高価です。
-
テーブルをまたがる書き込みは、データを取り込む際の一貫性を保証するためにトランザクションとして実装する必要があります。
オペレーション特性 - 複数テーブル
ストレージ - 複数のテーブル
書き込みの大部分は、samples
スキーマに行われます。このスキーマには、書き込まれたメトリクス1つにつき、3つのデータポイントを含むタプルが格納されています:
カラム | データ型 | バイトサイズ |
---|---|---|
series_id | UUID | 16バイト |
timestamp | DateTime64 | 8バイト |
value | フロート64 | 8バイト |
したがって、1サンプルあたり32バイトを使用すると推定されます。
圧縮 - 複数のテーブル
主要なスキーマについて、指定された設計で得られる圧縮量を調べると、良い出発点であることがわかります。プライマリテーブルの測定結果を以下に示します:
スキーマ:labels_to_series
12k近いユニークなseries_id
、それぞれが10-12個のラベル文字列ペアにマッピングされています。
SELECT
table,
column,
formatReadableSize(sum(data_compressed_bytes) AS x) AS compressedsize,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed
FROM system.parts_columns
WHERE table LIKE 'labels_to_series_1'
GROUP BY
database,
table,
column
ORDER BY x ASC
Query id: 723b4145-14f7-4e74-9ada-01c17c2f1fd5
┌─table──────────────┬─column────┬─compressedsize─┬─uncompressed─┐
│ labels_to_series_1 │ labels │ 586.66 KiB │ 2.42 MiB │
│ labels_to_series_1 │ series_id │ 586.66 KiB │ 2.42 MiB │
└────────────────────┴───────────┴────────────────┴──────────────┘
スキーマ:samples
約20kのメトリクスサンプルを含み、各サンプルはseries_id
(16バイト)、timestamp
(8バイト)、value
(8バイト)からなるタプルを含みます。
SELECT
table,
column,
formatReadableSize(sum(data_compressed_bytes) AS x) AS compressedsize,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed
FROM system.parts_columns
WHERE table LIKE 'samples_1'
GROUP BY
database,
table,
column
ORDER BY x ASC
Query id: 04219cea-06ea-4c5f-9287-23cb23c023d2
┌─table─────┬─column────┬─compressedsize─┬─uncompressed─┐
│ samples_1 │ value │ 373.21 KiB │ 709.78 KiB │
│ samples_1 │ timestamp │ 373.21 KiB │ 709.78 KiB │
│ samples_1 │ series_id │ 373.21 KiB │ 709.78 KiB │
└───────────┴───────────┴────────────────┴──────────────┘
パフォーマンス - 複数テーブル
私たちのリファレンス実装のプロファイリングから、現在私たちの時間のほとんどはアプリケーションでClickhouseへのデータ書き込みやそれに関連するオペレーションに費やされていることがわかります。実装からサンプリングした “トップ “のpprofプロファイルは以下のようになりました:
(pprof) top
Showing nodes accounting for 42253.20kB, 100% of 42253.20kB total
Showing top 10 nodes out of 58
flat flat% sum% cum cum%
13630.30kB 32.26% 32.26% 13630.30kB 32.26% github.com/ClickHouse/clickhouse-go/v2/lib/compress.NewWriter (inline)
11880.92kB 28.12% 60.38% 11880.92kB 28.12% github.com/ClickHouse/clickhouse-go/v2/lib/compress.NewReader (inline)
5921.37kB 14.01% 74.39% 5921.37kB 14.01% bufio.NewReaderSize (inline)
5921.37kB 14.01% 88.41% 5921.37kB 14.01% bufio.NewWriterSize (inline)
1537.69kB 3.64% 92.04% 1537.69kB 3.64% runtime.allocm
1040.73kB 2.46% 94.51% 1040.73kB 2.46% github.com/aws/aws-sdk-go/aws/endpoints.init
1024.41kB 2.42% 96.93% 1024.41kB 2.42% runtime.malg
768.26kB 1.82% 98.75% 768.26kB 1.82% go.uber.org/zap/zapcore.newCounters
528.17kB 1.25% 100% 528.17kB 1.25% regexp.(*bitState).reset
0 0% 100% 5927.73kB 14.03% github.com/ClickHouse/clickhouse-go/v2.(*clickhouse).Ping
予備的な分析から明らかなように、Clickhouseへのデータ書き込みがボトルネックになる可能性があります。したがって、書き込みパスでは、Clickhouseへの書き込みをバッチ化し、アプリケーションサーバーの作業量を減らして取り込みパスを効率化することが賢明です。
読み込みパスでは、サンプルテーブルの読み込みをseries_id
、またはクエリの開始と終了のタイムスタンプ間の時間ブロック単位で並列化することも可能です。
注意点
-
すでに存在するメトリクスからラベルを削除する場合、新しいラベルはまったく新しい系列として扱い、新しい
series_id
に帰属させます。これにより、系列データや値をマージする必要がなくなります。古い系列は、アクティビティに書き込まれなければ、最終的に保持から外れて削除されます。 -
データの集約はまだ考慮していません。私たちの想定では、(Clickhouseの)バッキングストアによって、「十分な」量のデータを生の状態で保持することができ、クエリのレイテンシSLOの範囲内でクエリを実行できるはずです。
却下された代替案単一の集中テーブル
単一集中データテーブル
CREATE TABLE IF NOT EXISTS metrics ON CLUSTER '{cluster}' (
group_id UInt64,
name LowCardinality(String) CODEC(ZSTD),
labels Map(String, String) CODEC(ZSTD),
metadata Map(String, String) CODEC(ZSTD),
value Float64 CODEC (Gorilla, ZSTD),
timestamp DateTime64(3, 'UTC') CODEC(Delta(4),ZSTD)
) ENGINE = ReplicatedMergeTree()
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (group_id, name, timestamp);
長所 - 単一テーブル
-
すべてのメトリクスデータが1つの大きなテーブルに格納されます。
-
データのクエリは、複数のテーブルにまたがってデータをクエリする必要がなく、SQLクエリを書くことで簡単に表現できます。
短所 - 単一テーブル
-
名前、ラベル、メタデータなどの属性は、採取されたサンプルごとに繰り返し保存されるため、データ構造には膨大な冗長性があります。
-
マップ/配列でバックアップされた場合、ラベル/メタデータの値がどのように保存されるかを考えると、時系列を検索するのは自明ではありません。
-
クエリごとに大量のデータをスキャンする必要があるため、クエリの待ち時間が長くなります。
オペレーション特性 - 単一テーブル
ストレージ - 単一テーブル
カラム | データ型 | バイトサイズ |
---|---|---|
group_id | UUID | 16バイト |
name | 文字列 | - |
labels | マップ(文字列, 文字列) | - |
metadata | マップ(文字列, 文字列) | - |
value | フロート64 | 8バイト |
timestamp | DateTime64 | 8バイト |
圧縮 - 単一テーブル
スキーマ:metrics
それぞれがgroup_id
,metric name
,labels
,metadata
,timestamp
と対応するvalue
で構成される約20kのメトリクスサンプルを含むコンテナ。
SELECT count(*)
FROM metrics_1
Query id: e580f20b-b422-4d93-bb1f-eb1435761604
┌─count()─┐
│ 12144 │
SELECT
table,
column,
formatReadableSize(sum(data_compressed_bytes) AS x) AS compressedsize,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed
FROM system.parts_columns
WHERE table LIKE 'metrics_1'
GROUP BY
database,
table,
column
ORDER BY x ASC
Query id: b2677493-3fbc-46c1-a9a7-4524a7a86cb4
┌─table─────┬─column────┬─compressedsize─┬─uncompressed─┐
│ metrics_1 │ labels │ 283.02 MiB │ 1.66 GiB │
│ metrics_1 │ metadata │ 283.02 MiB │ 1.66 GiB │
│ metrics_1 │ group_id │ 283.02 MiB │ 1.66 GiB │
│ metrics_1 │ value │ 283.02 MiB │ 1.66 GiB │
│ metrics_1 │ name │ 283.02 MiB │ 1.66 GiB │
│ metrics_1 │ timestamp │ 283.02 MiB │ 1.66 GiB │
└───────────┴───────────┴────────────────┴──────────────┘
前述のスキーマの圧縮率は良好ですが、対応するデータセットを保存するために必要なストレージ量は約300MiBです。また、スキーマの設計自体に冗長性があるため、このフットプリントは直線的に増加すると予想されます。
パフォーマンス - 単一テーブル
(pprof) top
Showing nodes accounting for 12844.95kB, 100% of 12844.95kB total
Showing top 10 nodes out of 40
flat flat% sum% cum cum%
2562.81kB 19.95% 19.95% 2562.81kB 19.95% runtime.allocm
2561.90kB 19.94% 39.90% 2561.90kB 19.94% github.com/aws/aws-sdk-go/aws/endpoints.init
2374.91kB 18.49% 58.39% 2374.91kB 18.49% github.com/ClickHouse/clickhouse-go/v2/lib/compress.NewReader (inline)
1696.32kB 13.21% 71.59% 1696.32kB 13.21% bufio.NewWriterSize (inline)
1184.27kB 9.22% 80.81% 1184.27kB 9.22% bufio.NewReaderSize (inline)
1184.27kB 9.22% 90.03% 1184.27kB 9.22% github.com/ClickHouse/clickhouse-go/v2/lib/compress.NewWriter (inline)
768.26kB 5.98% 96.01% 768.26kB 5.98% go.uber.org/zap/zapcore.newCounters
512.20kB 3.99% 100% 512.20kB 3.99% runtime.malg
0 0% 100% 6439.78kB 50.13% github.com/ClickHouse/clickhouse-go/v2.(*clickhouse).Ping
0 0% 100% 6439.78kB 50.13% github.com/ClickHouse/clickhouse-go/v2.(*clickhouse).acquire
このスキーマに対する書き込みは、1つのテーブルに集中し、サイド・テーブルからseries_id
を検索する必要がないため、計算の面ではるかに優れたパフォーマンスを発揮します。
一般的なストレージに関する考察 - Clickhouse
以下のセクションでは、スキーマ設計の特徴や、データベースシステムであるClickhouseとの相互作用について深く掘り下げていきます。
-
テーブルエンジン
-
効率的なパーティショニングとシャーディング
- 適切なパーティショニング・キーでスキーマを設定し、データを読み戻す際にスキャンされるブロックの量を最小限にします。
- ここでいうシャーディングとは、クラスターが常に最適なバランスを保つようにデータの配置戦略を設計する方法を指します。
-
データ圧縮
前述の予備的な結果からもわかるように、文字列と浮動小数点数については、それぞれディクショナリエンコーディングとデルタエンコーディングで良好な圧縮結果が得られています。Map
LowCardinality(String)
sのラベルを保存する場合、効率的にデータをパックすることができました。
- マテリアライズド・ビュー
必要に応じて動的に更新できるため、読み取りパスのパフォーマンスが向上します。
-
非同期インサート
-
バッチインサート
-
リテンション/TTL
私たちは、あらかじめ決められた期間のみデータを保存し、その後はデータを削除するか、集計するか、アーカイブストアへ発送することで、長期間のデータ保存にかかるオペレーションコストを削減します。
-
データ集約/ロールアップ
-
インデックスの粒度
-
インデックスをスキップ
-
max_server_memory_usage_to_ram_ratio
SQLによるデータアクセス
私たちのデータコーパスはPromQLでクエリ可能ですが、SQLインターフェースも「一般的に利用可能」にすることが賢明です。この機能により、常駐データへのクエリの可能性が広がり、ユーザーは自分のデータセットを好きなように、あるいは必要なように切り刻むことができます。
課題
- リソースとコストのプロファイリング。
- クエリの検証とサニテーション。
データアクセスの例
書き込み
書き込みパスでは、まず、指定されたセットラベルを一意のseries_id
に登録すること、および(または)過去にそのタイムシリーズを見たことがある場合は、そのラベルを再利用することを確認します。例えば
redis{region="us-east-1",'os':'Ubuntu15.10',...} <TIMESTAMP> <VALUE>
スキーマ: labels_to_series
SELECT *
FROM labels_to_series_1
WHERE series_id = '6d926ae8-c3c3-420e-a9e2-d91aff3ac125'
FORMAT Vertical
Query id: dcbc4bd8-0bdb-4c35-823a-3874096aab6e
Row 1:
──────
labels: {'arch':'x64','service':'1','__name__':'redis','region':'us-east-1','os':'Ubuntu15.10','team':'LON','service_environment':'production','rack':'36','service_version':'0','measurement':'pubsub_patterns','hostname':'host_32','datacenter':'us-east-1a'}
series_id: 6d926ae8-c3c3-420e-a9e2-d91aff3ac125
1 row in set. Elapsed: 0.612 sec.
その後、samples
テーブルの各メトリックポイントを、対応するseries_id
に登録します。
スキーマ:サンプル
SELECT *
FROM samples_1
WHERE series_id = '6d926ae8-c3c3-420e-a9e2-d91aff3ac125'
LIMIT 1
FORMAT Vertical
Query id: f3b410af-d831-4859-8828-31c89c0385b5
Row 1:
──────
series_id: 6d926ae8-c3c3-420e-a9e2-d91aff3ac125
timestamp: 2022-11-10 12:59:14.939
value: 0
読み込み
読み取りパスでは、まず、検討中のラベルを検索して、すべてのタイムスリリー識別子をクエリします。すべてのseries_id
(s)を取得したら、クエリの開始タイムスタンプと終了タイムスタンプの間の対応するすべてのサンプルを検索します。
使用例:
kernel{service_environment=~"prod.*", measurement="boot_time"}
これは、最初に関連するすべての時系列を探すことに変換されます:
SELECT *
FROM labels_to_series
WHERE
((labels['__name__']) = 'kernel') AND
match(labels['service_environment'], 'prod.*') AND
((labels['measurement']) = 'boot_time');
今調べたラベルに対応するseries_id
(s)の束が得られます。
補足:このほとんど静的なデータセットは、キャッシュして徐々にメモリ内に蓄積することで、2回目以降の待ち時間を減らすことができます。
このキャッシュをメンテナーする際に、新しい書き込みを考慮するためです:
-
帯域外のプロセス/ゴルーチンにこのキャッシュをメンテナーさせることで、いくつかのクエリが最新のデータを取り逃しても、後続のクエリが最終的に追いつきます。
-
キーにTTLを設定し、キーごとにジッターを設定することで、新しい書き込みを考慮した十分な頻度で再構築できるようにします。
どのクエリに対してクエリを実行するのかが分かれば、以下のようなクエリで簡単に全サンプルを検索することができます:
SELECT *
FROM samples
WHERE series_id IN (
'a12544be-0a3a-4693-86b0-c61a4553aea3',
'abd42fc4-74c7-4d80-9b6c-12f673db375d',
…
)
AND timestamp >= '1667546789'
AND timestamp <= '1667633189'
ORDER BY timestamp;
というクエリで簡単に全てのサンプルを検索することができます。
そして、これらをprometheus.QueryResult
オブジェクトの配列にレンダリングし、prometheus.ReadResponse
オブジェクトとして呼び出し元に返します。
生産準備
バッチ処理
大量の小さな書き込みをClickhouseに取り込む前にデータをバッチ処理する必要があることを考慮すると、パフォーマンスを向上させ、テーブルエンジンが正常にデータを永続化し続けるためには、所定のサイズのバッチでClickhouseに取り込む前に受信データをローカルでバッチ処理できるように、アプリローカル永続化を考慮した設計が必要です。
アプリローカルバッチングを実装するために、以下の選択肢を検討しました:
- インメモリ - 耐久性なし
- BadgerDB - 耐久性、組み込み、高性能
- Redis - 簡単、外部依存
- Kafka - 自明ではなく、外部依存ですが、他の複数のユースケースを補強し、GitLabの他の問題領域を助けることができます。
注:同様の課題は、CHインタラクション(errortracking
)でも表面化しています - サブシステムは現在の実装で持っています。このMRはインメモリ代替案を実装し、このMRはオンディスク代替案を試みました。
この問題領域で行われた作業は、エラート ラッキングやロギングなど、他のサブシステムにも役立ちます。
スケーラビリティ
理想的には、1秒間に1Mポイントをインジェストするために、基盤となるバックエンドを設計する必要がありますが、私たちは、最初の仮説をテスト/確立するために、1秒間に10Kメトリクスポイントで提案された実装のテストを開始するつもりです。
ベンチマーク
提案する実装をベンチマークする際に、以下の3つの次元をテストすることを提案します:
- データ取り込みパフォーマンス
- オンディスク・ストレージ要件(該当する場合はレプリケーションを考慮)
- 平均クエリ応答時間
パフォーマンスを理解するためには、まず、テスト用にインジェストしたデータから、そのようなクエリのリストをコンパイルする必要があります。その際、Clickhouseのクエリロギングが非常に役に立ちます。
過去の仕事と参考文献
- ClickHouseのメトリクスベンチマーク
- Incubation:APM ClickHouseの評価
- Incubation:APM ClickHouseメトリクス・スキーマ
- TimescaleDBに関する研究
- Thanosベースのセットアップにおける現在のワークロード
- スケーリング-200mシリーズ
コスト見積もり
-
私たちの最大のフットプリントがClickhouseと基礎となるストレージであることを考えると、システムが高価になりすぎないようにすることを目指しています。
-
特に、複数の記憶媒体の利用を考慮しなければなりません:
- 階層型ストレージ
- オブジェクトストレージ
ツール
-
私たちは、カーディナリティの高いメトリクスを可視化することで、使用されていないメトリクスの刈り込み/削除を行い、データベースを健全に保つことを支援することを目指しています。
-
同様に、すべての読み取り要求を解析し、使用統計を構築することによって、簡単かつ動的にシステムに組み込むことができます。
-
エンドユーザーが必要としない、あるいは有用と思わない量のデータをインジェストしていないことを確認するために、メトリクスごとのスクレイプ頻度の監視を追加することを目指しています。
今後の展望
テレメトリーの柱、模範を超えた連携
私たちは、インジェストされたデータを、トレース、ログ、エラーなどの他のテレメトリーの柱と相互参照できるようにメトリクス・システムを構築しなければなりません。
ユーザー定義のSQLクエリによるデータの集約やマテリアライズド・ビューの生成
Prometheusの記録ルールが既存のメトリクスからカスタムメトリクスの生成を支援するのと同様に、システムのユーザーがユーザー定義のアドホッククエリを実行できるようにする必要があります。
ライト・アヘッド・ログ(WAL)
インジェスト・アプリケーションのローカルにデータをバッファリングする必要性を感じたり、データを永続化するためにClickhouseから移行したりする場合、他のモニタリング・システムの間でよく使用されていることから、オンディスクWALは良い方向に進むだろうと考えています。
カスタムDSLまたはクエリビルダー
PromQLを直接使用することはユーザーにとって急な学習曲線になる可能性があります。(Grafanaで一般的なように)クエリビルダーがあれば、実行すると予想される典型的なクエリを構築したり、利用可能なメトリクスを探索したりすることができます。これはまた、DSLを学習する方法としても機能するので、より複雑なクエリを後で作成することができます。
ロードマップと次のステップ
以下のセクションでは、GitLab Observability Serviceにメトリクスサポートを組み込むという前述の提案をどのように実装するつもりかを列挙します。対応するドキュメントやイシューには、それぞれの次のステップがどのように実行される予定なのか、さらなる詳細が記載されています。
- To-Doone 調査と設計案・要件の草案
- 進行中 システム/スキーマ設計(提案)の提出とフィードバックの収集
- 進行中 テーブル定義やストレージ・インターフェースの開発
- 進行中 リファレンス実装のプロトタイプ作成、主要メトリクス測定
- Clickhouseおよび/または提案スキーマのベンチマーク、Clickhouse Inc.からの専門的アドバイスの収集
- 書き込みパスの開発 -
remote_write
API - 読み取りパスの開発 -
remote_read
API、PromQL
-based querier。 - 反復可能なベンチマーク/テストのためのテストベッドのセットアップ。
- 必要であれば、スキーマ設計やアプリケーションサーバーの改善
- 本番準備 v1.0-α/beta
- バンガード・ロールアウト/ステージ・ロールアウトの実装
- アルファ/ベータテストの実施
- v1.0のリリース