メインコンテンツまでスキップ
メインコンテンツまでスキップ

OpenTelemetryによるデータ収集の統合

任意の可観測性ソリューションには、ログやトレースを収集し、エクスポートする手段が必要です。この目的のために、ClickHouseはOpenTelemetry (OTel) プロジェクトを推奨します。

「OpenTelemetryは、トレース、メトリクス、ログなどのテレメトリデータを作成し、管理するために設計された可観測性フレームワークおよびツールキットです。」

ClickHouseやPrometheusとは異なり、OpenTelemetryは可観測性バックエンドではなく、テレメトリデータの生成、収集、管理、エクスポートに焦点を当てています。OpenTelemetryの初期の目標は、ユーザーが言語特有のSDKを使用してアプリケーションやシステムを容易に計装できるようにすることでしたが、現在はOpenTelemetryコレクターを通じてログの収集も含まれるようになりました。これは、テレメトリデータを受信、処理し、エクスポートするエージェントまたはプロキシです。

ClickHouse関連コンポーネント

OpenTelemetryは多くのコンポーネントから成り立っています。データおよびAPIの仕様、標準化されたプロトコル、フィールド/カラムの命名規則を提供することに加えて、OTelはClickHouseでの可観測性ソリューションの構築に不可欠な2つの機能を提供します。

  • OpenTelemetryコレクターは、テレメトリデータを受信、処理、エクスポートするプロキシです。ClickHouseを利用したソリューションは、ログ収集とイベント処理の両方にこのコンポーネントを使用します。
  • テレメトリデータの仕様、API、エクスポートを実装する言語SDKです。これらのSDKは、アプリケーションのコード内でトレースが正しく記録されることを保証し、構成要素スパンを生成し、メタデータを介してサービス間でコンテキストが伝播されることを確実にすることで、分散トレースを形成し、スパンが相関できるようにします。これらのSDKは、ユーザーがコードを変更する必要がなく、即時計装を得られるようにする一般的なライブラリやフレームワークを自動的に実装するエコシステムによって補完されています。

ClickHouseを利用した可観測性ソリューションは、これらのツールの両方を活用します。

ディストリビューション

OpenTelemetryコレクターには複数のディストリビューションがあります。ClickHouseソリューションに必要なfilelogレシーバーとClickHouseエクスポーターは、OpenTelemetry Collector Contrib Distroにのみ存在します。

このディストリビューションには多くのコンポーネントが含まれており、ユーザーがさまざまな構成を試すことを可能にします。ただし、運用時には、環境に必要なコンポーネントのみを含むようコレクターを制限することをお勧めします。これにはいくつかの理由があります:

  • コレクターのサイズを減らすことで、コレクターのデプロイメント時間を短縮します。
  • 利用可能な攻撃対象範囲を減らすことで、コレクターのセキュリティを向上させます。

カスタムコレクターは、OpenTelemetry Collector Builderを使用して作成できます。

OTelによるデータの取り込み

コレクター展開の役割

ログを収集し、ClickHouseに挿入するために、OpenTelemetryコレクターの使用をお勧めします。OpenTelemetryコレクターは、主に2つの役割で展開できます:

  • エージェント - エージェントインスタンスは、サーバーやKubernetesノードなどのエッジでデータを収集するか、OpenTelemetry SDKで計装されたアプリケーションからイベントを直接受信します。この場合、エージェントインスタンスはアプリケーションと共に、またはアプリケーションと同じホスト上(サイドカーやDaemonSetなど)で実行されます。エージェントは、データを直接ClickHouseに送信するか、ゲートウェイインスタンスに送信することができます。前者の場合、これはエージェント展開パターンと呼ばれます。
  • ゲートウェイ - ゲートウェイインスタンスは、通常はクラスターごと、データセンターごと、またはリージョンごとのスタンドアロンサービス(Kubernetesのデプロイメントなど)を提供します。これらは、OTLPエンドポイントを介してアプリケーション(またはエージェントとして別のコレクター)からのイベントを受信します。通常、負荷を分散させるために、アウトオブボックスの負荷分散機能を使用されるゲートウェイインスタンスのセットが展開されます。すべてのエージェントとアプリケーションがこの単一エンドポイントに信号を送信する場合、これはゲートウェイ展開パターンと呼ばれることがよくあります。

以下では、シンプルなエージェントコレクターがイベントを直接ClickHouseに送信することを前提とします。ゲートウェイの使用と、その適用時期についての詳細はゲートウェイでのスケーリングを参照してください。

ログの収集

コレクターを使用する主な利点は、サービスがデータを迅速にオフロードできることです。これにより、コレクターが再試行、バッチ処理、暗号化、さらには機密データのフィルタリングなどの追加処理を担当します。

コレクターは、レシーバプロセッサ、およびエクスポータという3つの主要な処理段階の用語を使用します。レシーバはデータ収集に使用され、プル方式またはプッシュ方式のいずれかです。プロセッサはメッセージの変換や強化機能を提供します。エクスポータは、受信したデータを下流のサービスに送信する役割を担っています。このサービスは理論的には別のコレクターでも可能ですが、以下の初期の議論では、すべてのデータが直接ClickHouseに送信されると仮定します。

ログの収集

ユーザーが受信者、プロセッサ、エクスポータの完全なセットに慣れることをお勧めします。

コレクターは、ログを収集するための主に2つのレシーバを提供します:

OTLPを介して - この場合、ログはOpenTelemetry SDKからOTLPプロトコルを介してコレクターに直接送信されます。OpenTelemetryデモはこのアプローチを採用しており、各言語のOTLPエクスポータはローカルコレクターエンドポイントを仮定します。この場合、コレクターはOTLPレシーバで構成する必要があります。上記のデモの構成を参照してください。このアプローチの利点は、ログデータが自動的にトレースIDを含むことであり、ユーザーは後で特定のログに対するトレースを識別できるようになります。

OTLPを介したログの収集

このアプローチでは、ユーザーが適切な言語SDKでコードを計装する必要があります。

  • Filelogレシーバ経由のスクレイピング - このレシーバは、ディスク上のファイルを追跡し、ログメッセージを形成し、それをClickHouseに送信します。このレシーバは、複数行メッセージの検出、ログのロールオーバーの処理、再起動に対する堅牢性のためのチェックポイント、および構造の抽出などの複雑なタスクを処理します。このレシーバは、DockerおよびKubernetesコンテナのログを追跡することもでき、helmチャートとして展開可能で、これらから構造を抽出し、ポッドの詳細でそれを強化することができます。
Filelogレシーバ

ほとんどの展開では、上記のレシーバを組み合わせて使用します。ユーザーは、コレクターのドキュメントを読み、基本概念や構成構造およびインストール方法について理解を深めることをお勧めします。

注記
ヒント: otelbin.io

otelbin.ioは、構成を検証し、可視化するのに役立ちます。

構造化ログと非構造化ログ

ログは構造化されているか非構造化されています。

構造化ログは、JSONのようなデータ形式を使用し、HTTPコードやソースIPアドレスなどのメタデータフィールドを定義します。

{
    "remote_addr":"54.36.149.41",
    "remote_user":"-","run_time":"0","time_local":"2019-01-22 00:26:14.000","request_type":"GET",
    "request_path":"\/filter\/27|13 ,27|  5 ,p53","request_protocol":"HTTP\/1.1",
    "status":"200",
    "size":"30577",
    "referer":"-",
    "user_agent":"Mozilla\/5.0 (compatible; AhrefsBot\/6.1; +http:\/\/ahrefs.com\/robot\/)"
}

非構造化ログは、通常、正規表現パターンを介して抽出可能な固有の構造を持ちながら、ログを純粋に文字列として表現します。

54.36.149.41 - - [22/Jan/2019:03:56:14 +0330] "GET
/filter/27|13%20%D9%85%DA%AF%D8%A7%D9%BE%DB%8C%DA%A9%D8%B3%D9%84,27|%DA%A9%D9%85%D8%AA%D8%B1%20%D8%A7%D8%B2%205%20%D9%85%DA%AF%D8%A7%D9%BE%DB%8C%DA%A9%D8%B3%D9%84,p53 HTTP/1.1" 200 30577 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)" "-"

ユーザーには、可能な限り構造化ログとJSON形式(つまり、ndjson)でログを記録することをお勧めします。これは、後でClickHouseに送信する前にコレクタープロセッサを使用してログを処理する際、もしくは挿入時にマテリアライズドビューを使用する際に、必要なログの処理を簡素化します。構造化ログは、最終的に後の処理リソースを節約し、ClickHouseソリューションで必要なCPUを削減します。

例のために、構造化(JSON)ログと非構造化ログのデータセットを各約10M行で提供します。以下のリンクから入手できます:

以下の例には構造化データセットを使用します。次の例を再現するために、このファイルがダウンロードされ、抽出されていることを確認してください。

以下は、OTelコレクターがこれらのファイルをディスクから読み取り、filelogレシーバを使用して、結果のメッセージをstdoutに出力するためのシンプルな構成を示しています。私たちのログが構造化されているため、json_parserオペレーターを使用します。access-structured.logファイルのパスを変更してください。

ClickHouseでの解析を検討

以下の例では、ログからタイムスタンプを抽出します。これは、全体のログ行をJSON文字列に変換し、その結果をLogAttributesに配置するjson_parserオペレーターの使用を必要とします。これは計算的に高価になる可能性があり、ClickHouseではより効率的に行えます - SQLでの構造抽出を参照してください。等価な非構造化の例は、[regex_parser](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/stanza/docs/operators/regex_parser.md)を使用してこれを実現するものがこちらで見つかります。

config-structured-logs.yaml

receivers:
  filelog:
    include:
      - /opt/data/logs/access-structured.log
    start_at: beginning
    operators:
      - type: json_parser
        timestamp:
          parse_from: attributes.time_local
          layout: '%Y-%m-%d %H:%M:%S'
processors:
  batch:
    timeout: 5s
    send_batch_size: 1
exporters:
  logging:
    loglevel: debug
service:
  pipelines:
    logs:
      receivers: [filelog]
      processors: [batch]
      exporters: [logging]

ユーザーは、公式の指示に従って、ローカルにコレクターをインストールできます。重要なのは、指示をContribディストリビューションを使用するように変更することです(filelogレシーバを含みます)。たとえば、otelcol_0.102.1_darwin_arm64.tar.gzの代わりにユーザーはotelcol-contrib_0.102.1_darwin_arm64.tar.gzをダウンロードする必要があります。リリースはこちらで見つけることができます。

インストール後、OTelコレクターは以下のコマンドで実行できます。

./otelcol-contrib --config config-logs.yaml

構造化ログを使用していると仮定すると、メッセージは以下の形式になります:

LogRecord #98
ObservedTimestamp: 2024-06-19 13:21:16.414259 +0000 UTC
Timestamp: 2019-01-22 01:12:53 +0000 UTC
SeverityText:
SeverityNumber: Unspecified(0)
Body: Str({"remote_addr":"66.249.66.195","remote_user":"-","run_time":"0","time_local":"2019-01-22 01:12:53.000","request_type":"GET","request_path":"\/product\/7564","request_protocol":"HTTP\/1.1","status":"301","size":"178","referer":"-","user_agent":"Mozilla\/5.0 (Linux; Android 6.0.1; Nexus 5X Build\/MMB29P) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/41.0.2272.96 Mobile Safari\/537.36 (compatible; Googlebot\/2.1; +http:\/\/www.google.com\/bot.html)"})
Attributes:
        -> remote_user: Str(-)
        -> request_protocol: Str(HTTP/1.1)
        -> time_local: Str(2019-01-22 01:12:53.000)
        -> user_agent: Str(Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html))
        -> log.file.name: Str(access.log)
        -> status: Str(301)
        -> size: Str(178)
        -> referer: Str(-)
        -> remote_addr: Str(66.249.66.195)
        -> request_type: Str(GET)
        -> request_path: Str(/product/7564)
        -> run_time: Str(0)
Trace ID:
Span ID:
Flags: 0

上記はOTelコレクターが生成した単一のログメッセージを表しています。これらのメッセージは、後のセクションでClickHouseに取り込まれます。

ログメッセージの完全なスキーマは、他のレシーバを使用する場合に存在する可能性のある追加カラムとともに、こちらに保たれています。ユーザーはこのスキーマに慣れることを強くお勧めします。

ここでの重要な点は、ログ行自体がBodyフィールド内に文字列として保持されますが、JSONはAttributesフィールドに自動抽出されていることです。この同じオペレーターが、適切なTimestampカラムにタイムスタンプを抽出するために使用されています。OTelによるログ処理の推奨事項については、処理を参照してください。

オペレーター

オペレーターは、ログ処理の最も基本的な単位です。各オペレーターは、ファイルから行を読み取る、またはフィールドからJSONを解析するなど、単一の責任を果たします。その後、オペレーターはパイプラインでチェーンして、所望の結果を達成します。

上記のメッセージにはTraceIDSpanIDフィールドがありません。ユーザーが分散トレーシングを実装している場合など、これらが存在する場合は、上記と同様の手法でJSONから抽出できます。

ローカルまたはKubernetesのログファイルを収集する必要のあるユーザーには、filelogレシーバの利用可能な構成オプションや、オフセットおよびマルチラインログ解析の取扱いについても理解することをお勧めします。

Kubernetesログの収集

Kubernetesログの収集には、OpenTelemetryのドキュメントガイドを推奨します。Kubernetes Attributes Processorは、ポッドメタデータでログとメトリクスを強化するために推奨されます。これにより、ラベルなどの動的メタデータが生成され、ResourceAttributesカラムに保存される可能性があります。ClickHouseは現在、このカラムに対してMap(String, String)型を使用しています。マップの取り扱いおよび最適化の詳細については、マップの使用およびマップからの抽出を参照してください。

トレースの収集

コードを計装してトレースを収集したいユーザーには、公式のOTelドキュメントに従うことをお勧めします。

ClickHouseにイベントを配信するには、適切なレシーバ経由でOTLPプロトコルを通じてトレースイベントを受信するOTelコレクターを展開する必要があります。OpenTelemetryデモは、サポートされている各言語の計装例を提供し、イベントをコレクターに送信します。以下の構成例は、イベントをstdoutに出力するための適切なコレクター構成を示しています。

トレースはOTLP経由で受信する必要があるため、telemetrygenツールを使用してトレースデータを生成します。インストール手順はこちらを参照してください。

以下の構成は、OTLPレシーバでトレースイベントを受信し、stdoutに送信します。

config-traces.xml

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
processors:
  batch:
    timeout: 1s
exporters:
  logging:
    loglevel: debug
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [logging]

以下のコマンドを使用してこの構成を実行します:

./otelcol-contrib --config config-traces.yaml

telemetrygenを使用してコレクターにトレースイベントを送信します:

$GOBIN/telemetrygen traces --otlp-insecure --traces 300

これにより、以下のようなトレースメッセージがstdoutに出力されます:

Span #86
        Trace ID        : 1bb5cdd2c9df5f0da320ca22045c60d9
        Parent ID       : ce129e5c2dd51378
        ID              : fbb14077b5e149a0
        Name            : okey-dokey-0
        Kind            : Server
        Start time      : 2024-06-19 18:03:41.603868 +0000 UTC
        End time        : 2024-06-19 18:03:41.603991 +0000 UTC
        Status code     : Unset
        Status message :
Attributes:
        -> net.peer.ip: Str(1.2.3.4)
        -> peer.service: Str(telemetrygen-client)

上記はOTelコレクターによって生成された単一のトレースメッセージを表しています。これらのメッセージは、後のセクションでClickHouseに取り込まれます。

トレースメッセージの完全なスキーマは、こちらに保たれています。ユーザーはこのスキーマに慣れることを強くお勧めします。

処理 - フィルタリング、変換、強化

前述のログイベントのタイムスタンプを設定する例で示されたように、ユーザーは必然的にイベントメッセージをフィルタリング、変換、強化したいと考えます。これは、OpenTelemetryのいくつかの機能を使用して実現できます:

ユーザーは、オペレーターや変換プロセッサを使用して過度なイベント処理を行わないことを推奨します。これは、特にJSON解析時にかなりのメモリおよびCPUオーバーヘッドを引き起こす可能性があります。特定の例外を除き、マテリアライズドビューやカラムでClickHouseで挿入時にすべての処理を行うことが可能です - 特に、k8sメタデータの追加などのコンテキスト認識の強化が必要です。詳細については、SQLでの構造抽出を参照してください。

OTelコレクターを使用して処理を行う場合、ゲートウェイインスタンスで変換を行い、エージェントインスタンスでの作業を最小限に抑えることをお勧めします。これにより、サーバー上で動作するエッジのエージェントが必要とするリソースを可能な限り最小限に抑えることができます。通常、ユーザーはフィルタリング(不必要なネットワーク使用を最小限に抑えるため)、タイムスタンプ設定(オペレーターを介して)、コンテキストを必要とする強化をエージェントで実行します。たとえば、ゲートウェイインスタンスが異なるKubernetesクラスターに居住する場合、k8sの強化はエージェント内で発生する必要があります。

以下の構成は、非構造化ログファイルの収集を示しています。ログ行から構造を抽出するためのオペレーター(regex_parser)とイベントをフィルタリングし、イベントをバッチ化およびメモリ使用量を制限するためのプロセッサの使用に注意してください。

config-unstructured-logs-with-processor.yaml

receivers:
  filelog:
    include:
      - /opt/data/logs/access-unstructured.log
    start_at: beginning
    operators:
      - type: regex_parser
        regex: '^(?P<ip>[\d.]+)\s+-\s+-\s+\[(?P<timestamp>[^\]]+)\]\s+"(?P<method>[A-Z]+)\s+(?P<url>[^\s]+)\s+HTTP/[^\s]+"\s+(?P<status>\d+)\s+(?P<size>\d+)\s+"(?P<referrer>[^"]*)"\s+"(?P<user_agent>[^"]*)"'
        timestamp:
          parse_from: attributes.timestamp
          layout: '%d/%b/%Y:%H:%M:%S %z'
          #22/Jan/2019:03:56:14 +0330
processors:
  batch:
    timeout: 1s
    send_batch_size: 100
  memory_limiter:
    check_interval: 1s
    limit_mib: 2048
    spike_limit_mib: 256
exporters:
  logging:
    loglevel: debug
service:
  pipelines:
    logs:
      receivers: [filelog]
      processors: [batch, memory_limiter]
      exporters: [logging]
./otelcol-contrib --config config-unstructured-logs-with-processor.yaml

ClickHouseへのエクスポート

エクスポータは、1つ以上のバックエンドまたは宛先にデータを送信します。エクスポータはプルまたはプッシュ方式にできます。ClickHouseにイベントを送信するには、プッシュベースのClickHouseエクスポータを使用する必要があります。

OpenTelemetry Collector Contribを使用

ClickHouseエクスポータはOpenTelemetry Collector Contribの一部であり、コアディストリビューションの一部ではありません。ユーザーは、contribディストリビューションを使用するか、独自のコレクターを構築できます。

完全な構成ファイルは、以下のように示されています。

[clickhouse-config.yaml](https://www.otelbin.io/#config=receivers%3A*N_filelog%3A*N___include%3A*N_____-_%2Fopt%2Fdata%2Flogs%2Faccess-structured.log*N___start*_at%3A_beginning*N___operators%3A*N_____-_type%3A_json*_parser*N_______timestamp%3A*N_________parse*_from%3A_attributes.time*_local*N_________layout%3A_*%22*.Y-*.m-*.d_*.H%3A*.M%3A*.S*%22*N_otlp%3A*N____protocols%3A*N______grpc%3A*N________endpoint%3A_0.0.0.0%3A4317*N*Nprocessors%3A*N_batch%3A*N___timeout%3A_5s*N___send*_batch*_size%3A_5000*N*N Nexporters%3AN_clickhouse%3AN___endpoint%3A_tcp%3A%2F%2Flocalhost%3A9000QdialtimeoutE10sAcompressElz4Aasync_insertE1*N__H_ttl%3A_72hN___traces_tablename%3A_otel_tracesN___logs_tablename%3A_otel_logsN___create_schema%3A_trueN___timeout%3A_5sN___database%3A_defaultN___sending_queue%3AN_____queue_size%3A_1000N___retry_onfailure%3AN_____enabled%3A_trueN_____initial_interval%3A_5sN_____max_interval%3A_30sN_____max_elapsedtime%3A_300sNNservice%3AN_pipelines%3AN___logs%3A*N_____receivers%3A%5Bfilelog%5D*N_____processors%3A%5Bbatch%5D*N_____exporters%3A%5Bclickhouse%5DN___traces%3AN____receivers%3A%5Botlp%5DN____processors%3A_%5Bbatch%5DN____exporters%3A_%5Bclickhouse%5D%7E&distro=otelcol-contrib%7E&distroVersion=v0.103.1%7E)

receivers:
  filelog:
    include:
      - /opt/data/logs/access-structured.log
    start_at: beginning
    operators:
      - type: json_parser
        timestamp:
          parse_from: attributes.time_local
          layout: '%Y-%m-%d %H:%M:%S'
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
processors:
  batch:
    timeout: 5s
    send_batch_size: 5000
exporters:
  clickhouse:
    endpoint: tcp://localhost:9000?dial_timeout=10s&compress=lz4&async_insert=1
    # ttl: 72h
    traces_table_name: otel_traces
    logs_table_name: otel_logs
    create_schema: true
    timeout: 5s
    database: default
    sending_queue:
      queue_size: 1000
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 300s

service:
  pipelines:
    logs:
      receivers: [filelog]
      processors: [batch]
      exporters: [clickhouse]
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [clickhouse]

以下の主要な設定に注意してください:

  • pipelines - 上記の構成は、ログとトレースのための一連のレシーバ、プロセッサ、エクスポータから成るパイプラインの使用を強調しています。
  • endpoint - ClickHouseとの通信はendpointパラメーターを介して構成されます。接続文字列tcp://localhost:9000?dial_timeout=10s&compress=lz4&async_insert=1により、通信がTCP経由で行われます。ユーザーがトラフィックスイッチの理由でHTTPを好む場合は、こちらで説明されているように、この接続文字列を変更します。ユーザー名およびパスワードをこの接続文字列内で指定する機能が含まれる完全な接続の詳細については、こちらで説明されています。

重要: 上記の接続文字列は、圧縮(lz4)および非同期挿入の両方を有効にします。両方を常に有効にすることをお勧めします。非同期挿入に関する詳細はバッチ処理を参照してください。圧縮は常に指定する必要があり、旧バージョンのエクスポータではデフォルトで無効になっています。

ユーザーが構造化ログファイルを抽出し、ローカルインスタンスのClickHouseが実行されていると仮定すると、以下のコマンドを使用してこの構成を実行できます:

./otelcol-contrib --config clickhouse-config.yaml

トレースデータをこのコレクターに送信するには、以下のコマンドをtelemetrygenツールを使用して実行します:

$GOBIN/telemetrygen traces --otlp-insecure --traces 300

実行中に、シンプルなクエリーでログイベントが存在することを確認します:

SELECT *
FROM otel_logs
LIMIT 1
FORMAT Vertical

Row 1:
──────
Timestamp:              2019-01-22 06:46:14.000000000
TraceId:
SpanId:
TraceFlags:             0
SeverityText:
SeverityNumber:         0
ServiceName:
Body:                   {"remote_addr":"109.230.70.66","remote_user":"-","run_time":"0","time_local":"2019-01-22 06:46:14.000","request_type":"GET","request_path":"\/image\/61884\/productModel\/150x150","request_protocol":"HTTP\/1.1","status":"200","size":"1684","referer":"https:\/\/www.zanbil.ir\/filter\/p3%2Cb2","user_agent":"Mozilla\/5.0 (Windows NT 6.1; Win64; x64; rv:64.0) Gecko\/20100101 Firefox\/64.0"}
ResourceSchemaUrl:
ResourceAttributes: {}
ScopeSchemaUrl:
ScopeName:
ScopeVersion:
ScopeAttributes:        {}
LogAttributes:          {'referer':'https://www.zanbil.ir/filter/p3%2Cb2','log.file.name':'access-structured.log','run_time':'0','remote_user':'-','request_protocol':'HTTP/1.1','size':'1684','user_agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0','remote_addr':'109.230.70.66','request_path':'/image/61884/productModel/150x150','status':'200','time_local':'2019-01-22 06:46:14.000','request_type':'GET'}

1 row in set. Elapsed: 0.012 sec. Processed 5.04 thousand rows, 4.62 MB (414.14 thousand rows/s., 379.48 MB/s.)
Peak memory usage: 5.41 MiB.

Likewise, for trace events, users can check the `otel_traces` table:

SELECT *
FROM otel_traces
LIMIT 1
FORMAT Vertical

Row 1:
──────
Timestamp:              2024-06-20 11:36:41.181398000
TraceId:                00bba81fbd38a242ebb0c81a8ab85d8f
SpanId:                 beef91a2c8685ace
ParentSpanId:
TraceState:
SpanName:               lets-go
SpanKind:               SPAN_KIND_CLIENT
ServiceName:            telemetrygen
ResourceAttributes: {'service.name':'telemetrygen'}
ScopeName:              telemetrygen
ScopeVersion:
SpanAttributes:         {'peer.service':'telemetrygen-server','net.peer.ip':'1.2.3.4'}
Duration:               123000
StatusCode:             STATUS_CODE_UNSET
StatusMessage:
Events.Timestamp:   []
Events.Name:            []
Events.Attributes:  []
Links.TraceId:          []
Links.SpanId:           []
Links.TraceState:   []
Links.Attributes:   []

Out of the box schema

デフォルトで、ClickHouseエクスポーターは、ログとトレースのためのターゲットログテーブルを作成します。これは、設定 create_schema によって無効にすることができます。さらに、ログとトレーステーブルの名前は、上記の設定を通じてデフォルトの otel_logsotel_traces から変更できます。

注記

以下のスキーマでは、TTLが72時間に有効になっていると仮定します。

ログのデフォルトスキーマは以下の通りです(otelcol-contrib v0.102.1):

CREATE TABLE default.otel_logs
(
    `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
    `TraceId` String CODEC(ZSTD(1)),
    `SpanId` String CODEC(ZSTD(1)),
    `TraceFlags` UInt32 CODEC(ZSTD(1)),
    `SeverityText` LowCardinality(String) CODEC(ZSTD(1)),
    `SeverityNumber` Int32 CODEC(ZSTD(1)),
    `ServiceName` LowCardinality(String) CODEC(ZSTD(1)),
    `Body` String CODEC(ZSTD(1)),
    `ResourceSchemaUrl` String CODEC(ZSTD(1)),
    `ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    `ScopeSchemaUrl` String CODEC(ZSTD(1)),
    `ScopeName` String CODEC(ZSTD(1)),
    `ScopeVersion` String CODEC(ZSTD(1)),
    `ScopeAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    `LogAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1,
    INDEX idx_res_attr_key mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_res_attr_value mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_scope_attr_key mapKeys(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_scope_attr_value mapValues(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_log_attr_key mapKeys(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_log_attr_value mapValues(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_body Body TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId)
TTL toDateTime(Timestamp) + toIntervalDay(3)
SETTINGS ttl_only_drop_parts = 1

ここでのカラムは、ここに記載されている OTel公式仕様のログに関連しています。

このスキーマに関するいくつかの重要な注意事項:

  • デフォルトでは、テーブルは PARTITION BY toDate(Timestamp) で日付ごとにパーティション分けされています。これにより、有効期限が切れたデータを効率的に削除できます。
  • TTLは TTL toDateTime(Timestamp) + toIntervalDay(3) により設定され、コレクター設定で設定された値に対応します。 ttl_only_drop_parts=1 は、全ての行が期限切れになっている場合のみ、全体が削除されることを意味します。これは、パーツ内の行を削除するよりも効率的です。行の削除には高コストがかかりますので、常にこの設定を推奨します。詳細は データ管理とTTL をご覧ください。
  • テーブルはクラシックな MergeTree エンジン を使用します。これはログとトレースのために推奨され、変更する必要はありません。
  • テーブルは ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId) で順序付けされています。これにより、ServiceNameSeverityTextTimestamp、および TraceId に対するフィルターに対してクエリが最適化されます - リスト内の早いカラムは遅いカラムよりも早くフィルターされます。例えば、ServiceName でフィルターすることは TraceId でフィルターするよりも遥かに早くなります。ユーザーは、想定されるアクセスポイントに応じてこの順序を調整する必要があります - 主キーの選定 を参照してください。
  • 上記のスキーマは、カラムに ZSTD(1) を適用します。これはログのために最適な圧縮を提供します。ユーザーは、より良い圧縮のためにZSTD圧縮レベル(デフォルトの1以上)を上げることができますが、これはめったに有益ではありません。この値を上げると、挿入時(圧縮中)にCPUオーバーヘッドが増加しますが、データの非圧縮(およびクエリ)は依然として同等のままであるべきです。詳細はこちらをご覧ください。追加のデルタエンコーディング がタイムスタンプに適用され、ディスク上のサイズを削減することを目指しています。
  • ResourceAttributesLogAttributes、および ScopeAttributes がマップとして定義されている点に注意してください。これらの違いについてユーザーは理解する必要があります。これらのマップにアクセスし、キーの最適化されたアクセス方法を見るには、マップの使用 を参照してください。
  • ここでの他のほとんどの型(例えば、ServiceName はLowCardinalityとして)も最適化されています。Bodyは、私たちの例のログでJSONであるため、Stringとして格納されています。
  • ブルームフィルターがマップキーと値、さらに Body カラムにも適用されます。これにより、これらのカラムにアクセスするクエリの時間が改善されますが、通常は必要ありません。詳細はセカンダリーデータスキッピングインデックスを参照してください。
CREATE TABLE default.otel_traces
(
        `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
        `TraceId` String CODEC(ZSTD(1)),
        `SpanId` String CODEC(ZSTD(1)),
        `ParentSpanId` String CODEC(ZSTD(1)),
        `TraceState` String CODEC(ZSTD(1)),
        `SpanName` LowCardinality(String) CODEC(ZSTD(1)),
        `SpanKind` LowCardinality(String) CODEC(ZSTD(1)),
        `ServiceName` LowCardinality(String) CODEC(ZSTD(1)),
        `ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
        `ScopeName` String CODEC(ZSTD(1)),
        `ScopeVersion` String CODEC(ZSTD(1)),
        `SpanAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
        `Duration` Int64 CODEC(ZSTD(1)),
        `StatusCode` LowCardinality(String) CODEC(ZSTD(1)),
        `StatusMessage` String CODEC(ZSTD(1)),
        `Events.Timestamp` Array(DateTime64(9)) CODEC(ZSTD(1)),
        `Events.Name` Array(LowCardinality(String)) CODEC(ZSTD(1)),
        `Events.Attributes` Array(Map(LowCardinality(String), String)) CODEC(ZSTD(1)),
        `Links.TraceId` Array(String) CODEC(ZSTD(1)),
        `Links.SpanId` Array(String) CODEC(ZSTD(1)),
        `Links.TraceState` Array(String) CODEC(ZSTD(1)),
        `Links.Attributes` Array(Map(LowCardinality(String), String)) CODEC(ZSTD(1)),
        INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1,
        INDEX idx_res_attr_key mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
        INDEX idx_res_attr_value mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
        INDEX idx_span_attr_key mapKeys(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
        INDEX idx_span_attr_value mapValues(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
        INDEX idx_duration Duration TYPE minmax GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId)
TTL toDateTime(Timestamp) + toIntervalDay(3)
SETTINGS ttl_only_drop_parts = 1

再度、これは、ここに記載されている OTel公式仕様のトレースに対応するカラムと関連があります。ここでのスキーマは、上記のログスキーマと同様の設定を多く使用しており、スパンに特有の追加Linkカラムがあります。

ユーザーには自動スキーマ作成を無効にして、手動でテーブルを作成することをお勧めします。これにより、主キーおよび副キーの変更、クエリパフォーマンスを最適化するための追加カラムの導入が可能になります。詳細については、スキーマ設計をご覧ください。

Optimizing inserts

高挿入パフォーマンスを達成しながら強力な整合性保証を得るために、ユーザーはコレクターを介してClickHouseに可観測データを挿入する際に、シンプルなルールに従うべきです。OTelコレクターが正しく構成されている場合、以下のルールは簡単に従うことができるはずです。これにより、ClickHouseを初めて使用する際のユーザーが直面する一般的な問題を回避できます。

Batching

デフォルトでは、ClickHouseに送信された各挿入は、ClickHouseが直ちに挿入のデータと保存する必要のあるその他のメタデータを含むストレージのパートを作成させます。したがって、より多くのデータを含む少量の挿入を送信することは、少量のデータを含む多数の挿入を送信することに比べて、必要な書き込みの数を減少させます。ユーザーには、少なくとも1,000行の比較的大きなバッチでデータを挿入することをお勧めします。詳細はこちらをご覧ください。

デフォルトでは、ClickHouseへの挿入は同期的で、同一である場合は冪等性があります。マージツリーエンジンファミリーのテーブルの場合、ClickHouseはデフォルトで自動的に重複排除を行います。これは、次のような場合に挿入が許容されることを意味します:

  • (1) データを受信するノードに問題がある場合、挿入クエリはタイムアウトし(またはより具体的なエラーが発生します)、確認が返されません。
  • (2) ノードによってデータが書き込まれた場合、ネットワークの中断によってクエリの送信者に確認を返すことができない場合、送信者はタイムアウトまたはネットワークエラーを受け取ります。

コレクターの視点から見ると、(1)と(2)は区別が難しいことがあります。しかし、いずれの場合も、未確認の挿入はただちに再試行することができます。再試行した挿入クエリに、同じ順序で同じデータが含まれている限り、ClickHouseは元の(未確認の)挿入が成功した場合に再試行した挿入を自動的に無視します。

ユーザーには、上記の要件を満たす一貫した行のバッチが送信されることを確保するために、先に示したバッチプロセッサを使用することをお勧めします。コレクターが高スループット(秒あたりのイベント数)を持つことが期待される場合、各挿入で少なくとも5000イベントを送信できる場合、これは通常、パイプラインに必要な唯一のバッチ処理です。この場合、コレクターは、バッチプロセッサの timeout に達する前にバッチをフラッシュし、パイプラインのエンドツーエンドのレイテンシが低く保たれるようにし、バッチが一貫したサイズであることを確保します。

Use asynchronous inserts

通常、ユーザーはコレクターのスループットが低いときに小さなバッチを送信せざるを得ず、それでもデータが最低限のエンドツーエンドのレイテンシ内でClickHouseに到達することを期待します。この場合、バッチプロセッサの timeout が期限切れになると小さなバッチが送信されます。これが問題を引き起こす可能性があり、この場合は非同期挿入が必要です。このケースは、エージェント役割のコレクターが直接ClickHouseに送信するように設定されているときに一般的に発生します。ゲートウェイは、集約者として機能することでこの問題を軽減できます - ゲートウェイによるスケーリングを参照してください。

大きなバッチを保証できない場合、ユーザーは非同期挿入を使用してClickHouseにバッチ処理を委任できます。非同期挿入では、データはまずバッファに挿入され、その後、データベースストレージに書き込まれます。

Async inserts

非同期挿入が有効な状態で、ClickHouseが① 挿入クエリを受信すると、クエリのデータが② まずメモリ内バッファに直ちに書き込まれます。③ 次のバッファフラッシュが行われると、バッファのデータはソートされ、データベースストレージにパートとして書き込まれます。データはデータベースストレージにフラッシュされる前はクエリによって検索可能ではない点に注意してください;バッファフラッシュは構成可能です。

コレクター用に非同期挿入を有効にするには、接続文字列に async_insert=1 を追加します。ユーザーには配信保証を得るために wait_for_async_insert=1(デフォルト)を使用することを推奨します - 詳細はこちらをご覧ください。

非同期挿入からのデータは、ClickHouseバッファがフラッシュされた後に挿入されます。これは、async_insert_max_data_sizeを超えた場合、または最初のINSERTクエリからasync_insert_busy_timeout_msミリ秒後に発生します。async_insert_stale_timeout_msが0より大きい値に設定されている場合、データは前回のクエリからasync_insert_stale_timeout_msミリ秒後に挿入されます。ユーザーは、これらの設定を調整してパイプラインのエンドツーエンドのレイテンシを制御できます。バッファフラッシュを調整するために使用できるさらに詳しい設定は、ここに記載されています。一般的に、デフォルトは適切です。

Adaptive Asynchronous Insertsを検討してください

エージェントの数が少なく、スループットが低いが厳しいエンドツーエンドのレイテンシ要件がある場合、適応型非同期挿入が役立つかもしれません。一般的に、これらはClickHouseで見る高スループットの可観測性ユースケースには適用されません。

最後に、ClickHouseへの同期挿入に関連した以前の重複排除動作は、非同期挿入を使用しているときにはデフォルトで有効になりません。必要な場合は、設定async_insert_deduplicateを参照してください。

この機能の構成に関する詳細はこちらにあり、より深い内容はこちらで確認できます。

Deployment architectures

OTelコレクターをClickHouseで使用する際に、いくつかのデプロイメントアーキテクチャが可能です。以下にそれぞれについて、その適用可能性を説明します。

Agents only

エージェントのみのアーキテクチャでは、ユーザーはOTelコレクターをエージェントとしてエッジにデプロイします。これらはローカルアプリケーション(例:サイドカーコンテナ)からトレースを受信し、サーバーやKubernetesノードからログを収集します。このモードでは、エージェントはデータを直接ClickHouseに送信します。

Agents only

このアーキテクチャは、小規模から中規模のデプロイに適しています。主な利点は、追加のハードウェアを必要とせず、ClickHouse可観測ソリューションのリソースフットプリントを最小限に抑え、アプリケーションとコレクターの間にシンプルなマッピングを維持できることです。

エージェントが数百を超えた場合は、ゲートウェイベースのアーキテクチャへの移行を検討すべきです。このアーキテクチャには、スケールが難しいいくつかの欠点があります:

  • 接続のスケーリング - 各エージェントはClickHouseへの接続を確立します。ClickHouseは数百(場合によっては数千)の同時挿入接続を維持することができますが、最終的には制約の要因となり、挿入を効果的でなくします - つまり、接続を維持するためにClickHouseがより多くのリソースを消費するようになります。ゲートウェイを使用すると、接続の数を最小限にし、挿入をより効率的にします。
  • エッジでの処理 - このアーキテクチャでは、エッジまたはClickHouseで変換やイベント処理を行う必要があります。これにより、制約が生じ、複雑なClickHouseマテリアライズドビューや、重要なサービスに影響を与える可能性がある重要な計算をエッジに押し込むことになります。
  • 小さなバッチとレイテンシ - エージェントコレクターは非常に少数のイベントを個別に収集する場合があります。これにより、配信SLAを満たすために設定した間隔でフラッシュする必要が生じます。これにより、コレクターがClickHouseに小さなバッチを送信することになります。これは欠点ですが、非同期挿入で緩和できます - 挿入の最適化を参照してください。

Scaling with gateways

OTelコレクターは、上記の制限に対処するためにゲートウェイインスタンスとして展開することができます。これらは、通常、データセンターや地域ごとのスタンドアロンサービスを提供します。これらは、アプリケーション(またはエージェント役割の他のコレクター)からのイベントを単一のOTLPエンドポイントを介して受信します。通常、一連のゲートウェイインスタンスが展開され、負荷を分散するためにボックスから出たロードバランサーが使用されます。

Scaling with gateways

このアーキテクチャの目的は、エージェントから計算集約処理をオフロードし、リソース使用量を最小限に抑えることです。これらのゲートウェイは、エージェントが行う必要のある変換タスクを実行することができます。さらに、複数のエージェントからのイベントを集約することにより、ゲートウェイはClickHouseに大きなバッチを送信できるようにし、効率的な挿入を可能にします。これらのゲートウェイコレクターは、より多くのエージェントが追加され、イベントスループットが増加するにつれて簡単にスケールできます。以下は、関連するエージェント構成とともに、例のゲートウェイ構成を示します。エージェントとゲートウェイ間の通信にはOTLPが使用されている点に注意してください。

clickhouse-agent-config.yaml

receivers:
  filelog:
    include:
      - /opt/data/logs/access-structured.log
    start_at: beginning
    operators:
      - type: json_parser
        timestamp:
          parse_from: attributes.time_local
          layout: '%Y-%m-%d %H:%M:%S'
processors:
  batch:
    timeout: 5s
    send_batch_size: 1000
exporters:
  otlp:
    endpoint: localhost:4317
    tls:
      insecure: true # Set to false if you are using a secure connection
service:
  telemetry:
    metrics:
      address: 0.0.0.0:9888 # Modified as 2 collectors running on same host
  pipelines:
    logs:
      receivers: [filelog]
      processors: [batch]
      exporters: [otlp]

clickhouse-gateway-config.yaml

receivers:
  otlp:
    protocols:
    grpc:
    endpoint: 0.0.0.0:4317
processors:
  batch:
    timeout: 5s
    send_batch_size: 10000
exporters:
  clickhouse:
    endpoint: tcp://localhost:9000?dial_timeout=10s&compress=lz4
    ttl: 96h
    traces_table_name: otel_traces
    logs_table_name: otel_logs
    create_schema: true
    timeout: 10s
    database: default
    sending_queue:
      queue_size: 10000
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 300s
service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [clickhouse]

これらの構成は、以下のコマンドで実行できます。

./otelcol-contrib --config clickhouse-gateway-config.yaml
./otelcol-contrib --config clickhouse-agent-config.yaml

このアーキテクチャの主な欠点は、コレクターの管理に関連するコストとオーバーヘッドです。

ゲートウェイベースのアーキテクチャを管理するための例とそれに関連する学びの例については、このブログ記事をお勧めします。

Adding Kafka

上記のアーキテクチャがメッセージキューとしてKafkaを使用していないことにお気づきかもしれません。

Kafkaキューをメッセージバッファとして使用することは、ログアーキテクチャで見られる一般的な設計パターンであり、ELKスタックによって普及しました。これにはいくつかの利点があります;主に、強力なメッセージ配信保証を提供し、バックプレッシャーに対処するのに役立ちます。メッセージは、収集エージェントからKafkaに送信され、ディスクに書き込まれます。理論的には、クラスタ化されたKafkaインスタンスは、高スループットメッセージバッファを提供すべきです。これは、メッセージを解析および処理するよりも、ディスクにリニアに書き込む方が少ない計算オーバーヘッドがかかるためです。例えば、Elasticの場合、トークン化とインデックス作成には多くのオーバーヘッドがかかります。データをエージェントから遠ざけることにより、ソースでのログローテーションの影響でメッセージが失われるリスクも減少します。最後に、いくつかのメッセージ再実行およびクロスリージョンの複製機能が提供されており、一部のユースケースにとっては魅力的かもしれません。

しかし、ClickHouseはデータを非常に迅速に挿入でき、適度なハードウェアで毎秒数百万行の挿入が可能です。ClickHouseからのバックプレッシャーは です。しばしば、Kafkaキューを利用することは、より多くのアーキテクチャの複雑さやコストを伴います。ログが銀行取引や他の重要なデータと同じ配信保証を必要としないという原則を受け入れられるのであれば、Kafkaの複雑さは避けることをお勧めします。

さて、高い配信保証やデータの再実行の能力(複数のソースへの可能性)が必要な場合、Kafkaは有用なアーキテクチャ追加となる可能性があります。

Adding kafka

この場合、OTelエージェントは、Kafkaエクスポーター を介してデータをKafkaに送信するように構成できます。ゲートウェイインスタンスは、Kafkaレシーバーを使用してメッセージを消費します。さらなる詳細については、ConfluentおよびOTelのドキュメントをお勧めします。

Estimating resources

OTelコレクターのリソース要件は、イベントのスループット、メッセージのサイズ、および実行される処理の量によって異なります。OpenTelemetryプロジェクトは、リソース要件を推定するためのベンチマークを維持しています。

私たちの経験では、3つのコアと12GBのRAMを持つゲートウェイインスタンスは、毎秒約60kのイベントを処理できます。これは、フィールド名の変更を行う最小限の処理パイプラインが責任を負っている場合の想定です。

イベントをゲートウェイに送信し、イベントのタイムスタンプのみを設定するエージェントインスタンスの場合、ユーザーは予想される毎秒のログに基づいてサイズを考慮することをお勧めします。以下は、ユーザーがスタートポイントとして使用できる近似値を示したものです:

ロギングレートコレクターエージェントに必要なリソース
1k/秒0.2CPU, 0.2GiB
5k/秒0.5 CPU, 0.5GiB
10k/秒1 CPU, 1GiB