This page contains information related to upcoming products, features, and functionality. It is important to note that the information presented is for informational purposes only. Please do not rely on this information for purchasing or planning purposes. As with all projects, the items mentioned on this page are subject to change or delay. The development, release, and timing of any products, features, or functionality remain at the sole discretion of GitLab Inc.
StatusAuthorsCoachDRIsOwning StageCreated
proposed @ankitbhatnagar @mappelman @sebastienpahl @nicholasklick monitor observability 2022-11-09

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つのサブ目標に分けることができます:

データのインジェスト

  • システムが大量の書き込みと読み込みをインジェストできるようにするためには、水平方向にスケーラブルであることと、一度インジェストされた書き込みが削除されないように耐久性を保証することが必要です。

データの永続化

  • 私たちは、Prometheusremote_write プロトコルを使用して送信されたテレメトリ/データの取り込みをサポートすることを目指しています。データセットのために設計する永続性は、デフォルトでマルチテナントである必要があります。

  • 私たちは、Prometheusのコンプライアンス・テスト・スイートがどのように与えられたメトリクス実装の正しさをチェックするかからヒントを得て、CIセットアップの一部としてそれを実行しながら、データの正しさのためのテスト・スイートを開発することを目指しています。

note
remote_write_senderは、私たちのケースのようにリモート書き込みレシーバ自体の正しさをテストするものではありませんが、このプロジェクトの範囲内でリモート書き込みレシーバを実装/開発するためのヒントを与えてくれます。
  • また、特殊なPrometheusデータ型、例えばPrometheusヒストグラムやサマリーなどの互換性を確保することを目指しています。

データの読み込み

  • 私たちはPromQLを使ったデータクエリをサポートすることを目指しています。そのためにはPromQLや MetricsQLパーサーを使用するのがよいでしょう。

  • 私たちは、以下の信頼性を条件として、すべての取り込まれたデータをネイティブのClickhouse SQLインターフェースを介して公開することで、さらなる価値を提供することを目指しています:
    • クエリ検証、サニテーション
    • レート制限
    • リソース制限 - メモリ、CPU、ネットワーク帯域幅
  • Prometheusコンプライアンス・テスト・スイートを通じて、Prometheusのテスト・スーツに合格することを目標としています。

データの削除

  • 私たちは、必要に応じて取り込まれたデータを削除できるようにすることを目指しています。これは、設定されたTTLが切れたり、それぞれの保持ポリシーが適用されたりしたときに、自然にデータを削除することに加えて行われます。私たちはスキーマの中で、ラベルやコンテンツによってデータを削除する方法を構築し、そのために必要なツールを追加しなければなりません。

非ゴール

上記で設定されたゴールと同時に、現在の提案で具体的にどのようなことが非ゴールであるかを設定したいと思います。それは

  • 私たちは、OpenTelemetry/OpenMetrics フォーマットを使ったインジェストをサポートするつもりはありません。しかし、私たちのユーザーは、標準的な Prometheusremote_write プロトコルを消費する Opentelemetry エクスポーターを内部で使うことができます。詳細はこちら

  • 最初のイテレーションでは、Prometheusの模範解答のインジェストをサポートするつもりはありません。

note
私たちは、メトリックラベルをモデル化しているのと同じように、エグザンプラーをモデル化するつもりなので、同じデータ構造の上に構築することで、メタデータ/エグザンプラーのサポートを簡単に実装できるはずです。

提案

GitLab Observability Backendをメトリクス実装のフレームワークとして利用し、スケジューラやテナントオペレータなど既存のKubernetesコントローラでライフサイクルを管理できるようにします。

Architecture

開発の観点からは、上記の “Application Server “としてマークされたものはこの提案の一部として開発される必要がありますが、残りの周辺コンポーネントは既に存在するか、scheduler/tenant-operator の既存のコードを通してプロビジョニングすることができます。

書き込みパスではHTTP/gRPC Ingress を介して、例えばエラートラッキングやトレースなど、既存のサービスと同様に受信データを受け取ることが期待されます。

note
さらに、Prometheusremote_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
}
note
これはPrometheusのデータフォーマット/リクエストタイプと実装を結合していることは理解していますが、より多くのデータフォーマットをサポートするためにインターフェースにメソッドを追加することは、コードの変更を最小限に抑えながら、将来的には些細なことであるはずです。

読み込みパスでは、ユーザーが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)

リファインメント

  • 複数のテナントに固有のデータを同じデータベーステーブル内に配置する場合、データのインジェスト/パーシング時にテナントごとにシャーディングを考慮する必要があります。物事を単純化するために、テナント固有のデータを専用のテーブルセットに分離することは非常に理にかなっています。

  • テナント間でデータを取り込む際の「タイムスタンプ」の構造的な考慮事項。

  • 作成時間と取り込み時間の比較

  • 複数のテーブルにまたがる書き込みを効率的に管理するために、ネイティブクライアントではまだトランザクションをサポートしていません。

note
少し非自明ですが、ClickHouse/ch-goを直接使用する可能性を調査することができます。

長所 - 複数のテーブル

  • 正規化されたデータ構造により、データの効率的な保存が可能になり、あるタイムスリリーの複数のサンプルにまたがる冗長性が取り除かれます。明らかに、”samples” スキーマの場合、1 メトリックポイントあたり 32 バイトのデータを保存することになります。

  • ラベル/メタデータでタイムスリリーをフィルタリングする際、インデックス化されたカラムを使用することで、検索の複雑さが改善されます。

  • すべてのデータは一意な識別子で識別可能で、テーブル間のデータの一貫性をメンテナーすることができます。

短所 - 複数のテーブル

  • 複数のテーブルにまたがる書き込みを考慮すると、書き込みは非常に高価です。

  • テーブルをまたがる書き込みは、データを取り込む際の一貫性を保証するためにトランザクションとして実装する必要があります。

オペレーション特性 - 複数テーブル

ストレージ - 複数のテーブル

書き込みの大部分は、samples スキーマに行われます。このスキーマには、書き込まれたメトリクス1つにつき、3つのデータポイントを含むタプルが格納されています:

カラムデータ型バイトサイズ
series_idUUID16バイト
timestampDateTime648バイト
valueフロート648バイト

したがって、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_idUUID16バイト
name文字列-
labelsマップ(文字列, 文字列)-
metadataマップ(文字列, 文字列)-
valueフロート648バイト
timestampDateTime648バイト
note
文字列は任意の長さで、長さに制限はありません。その値はヌルバイトを含む任意のバイトセットを含むことができます。これらのカラムに何を書き込むかは、アプリケーション側で調整する必要があります。

圧縮 - 単一テーブル

スキーマ: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 オブジェクトとして呼び出し元に返します。

note
クエリは、初期の実験/反復の間だけ、複数のクエリに分割されましたが、本番/ベンチマークでは、データベースへの同じラウンドトリップ内でサブクエリを使用することが賢明です。

生産準備

バッチ処理

大量の小さな書き込みをClickhouseに取り込む前にデータをバッチ処理する必要があることを考慮すると、パフォーマンスを向上させ、テーブルエンジンが正常にデータを永続化し続けるためには、所定のサイズのバッチでClickhouseに取り込む前に受信データをローカルでバッチ処理できるように、アプリローカル永続化を考慮した設計が必要です。

アプリローカルバッチングを実装するために、以下の選択肢を検討しました:

  • インメモリ - 耐久性なし
  • BadgerDB - 耐久性、組み込み、高性能
  • Redis - 簡単、外部依存
  • Kafka - 自明ではなく、外部依存ですが、他の複数のユースケースを補強し、GitLabの他の問題領域を助けることができます。

:同様の課題は、CHインタラクション(errortracking )でも表面化しています - サブシステムは現在の実装で持っています。このMRはインメモリ代替案を実装し、このMRはオンディスク代替案を試みました。

この問題領域で行われた作業は、エラート ラッキングやロギングなど、他のサブシステムにも役立ちます。

スケーラビリティ

理想的には、1秒間に1Mポイントをインジェストするために、基盤となるバックエンドを設計する必要がありますが、私たちは、最初の仮説をテスト/確立するために、1秒間に10Kメトリクスポイントで提案された実装のテストを開始するつもりです。

ベンチマーク

提案する実装をベンチマークする際に、以下の3つの次元をテストすることを提案します:

  • データ取り込みパフォーマンス
  • オンディスク・ストレージ要件(該当する場合はレプリケーションを考慮)
  • 平均クエリ応答時間

パフォーマンスを理解するためには、まず、テスト用にインジェストしたデータから、そのようなクエリのリストをコンパイルする必要があります。その際、Clickhouseのクエリロギングが非常に役に立ちます。

note
理想的には、<1秒未満でほとんどのクエリを一貫して提供しながら、>1Mメトリックポイント/秒をインジェストできるようにシステムをベンチマークすることを目指しています。

過去の仕事と参考文献

コスト見積もり

  • 私たちの最大のフットプリントがClickhouseと基礎となるストレージであることを考えると、システムが高価になりすぎないようにすることを目指しています。

  • 特に、複数の記憶媒体の利用を考慮しなければなりません:

    • 階層型ストレージ
    • オブジェクトストレージ

ツール

  • 私たちは、カーディナリティの高いメトリクスを可視化することで、使用されていないメトリクスの刈り込み/削除を行い、データベースを健全に保つことを支援することを目指しています。

  • 同様に、すべての読み取り要求を解析し、使用統計を構築することによって、簡単かつ動的にシステムに組み込むことができます。

  • エンドユーザーが必要としない、あるいは有用と思わない量のデータをインジェストしていないことを確認するために、メトリクスごとのスクレイプ頻度の監視を追加することを目指しています。

今後の展望

テレメトリーの柱、模範を超えた連携

私たちは、インジェストされたデータを、トレース、ログ、エラーなどの他のテレメトリーの柱と相互参照できるようにメトリクス・システムを構築しなければなりません。

ユーザー定義のSQLクエリによるデータの集約やマテリアライズド・ビューの生成

Prometheusの記録ルールが既存のメトリクスからカスタムメトリクスの生成を支援するのと同様に、システムのユーザーがユーザー定義のアドホッククエリを実行できるようにする必要があります。

ライト・アヘッド・ログ(WAL)

インジェスト・アプリケーションのローカルにデータをバッファリングする必要性を感じたり、データを永続化するためにClickhouseから移行したりする場合、他のモニタリング・システムの間でよく使用されていることから、オンディスクWALは良い方向に進むだろうと考えています。

カスタムDSLまたはクエリビルダー

PromQLを直接使用することはユーザーにとって急な学習曲線になる可能性があります。(Grafanaで一般的なように)クエリビルダーがあれば、実行すると予想される典型的なクエリを構築したり、利用可能なメトリクスを探索したりすることができます。これはまた、DSLを学習する方法としても機能するので、より複雑なクエリを後で作成することができます。

ロードマップと次のステップ

以下のセクションでは、GitLab Observability Serviceにメトリクスサポートを組み込むという前述の提案をどのように実装するつもりかを列挙します。対応するドキュメントやイシューには、それぞれの次のステップがどのように実行される予定なのか、さらなる詳細が記載されています。