出前館のオブザーバビリティ基盤の課題とClickHouse Cloudを用いた未来では、ClickHouse Cloudをオブザーバビリティ基盤の候補としてPoCを進めている背景をまとめました。今回はその一環として、Hot/ColdのログをClickHouse Cloudから単一のUIで参照できるかを検証した結果を共有します。
背景
私たちのログは、OpenTelemetry Collector などから、既存のオブザーバビリティ基盤(以下、既存基盤)とS3の二系統に配送しています。二系統にしている理由は、既存基盤に保持期間の上限があるためです。既存基盤では直近のログ(30日)を用いて可視化・検索・アラートを行い、上限を超えるデータはコスト最適化のためS3にアーカイブします。長期の調査や監査対応ではAthenaでS3上のログをクエリしています。
現行構成でも要件は満たしていますが、UIとクエリ言語が既存基盤とAthenaで二重化し、利用者の負担が大きい状態でした。 そこで本検証では、Hot層=ClickHouse Cloud(直近の高速検索)、Cold層=S3(長期保管)としつつ、両層をClickHouse CloudのUIから統一的に検索できるか、さらにHyperDXからも同様に検索できるかを確認しました。
アーキテクチャ
HotをClickHouse(SharedMergeTree)、ColdをS3(Apache Iceberg)に分離します。Icebergの細かい説明は省きますが、オープンなテーブルフォーマットなのでAthenaやSparkなどから扱え、もちろんClickHouseからも利用できます。ColdはS3上のIcebergテーブルに長期保管し、ClickHouseから参照します。UIは共通で、ClickHouse Cloudのコンソール/HyperDXを使用し、用途に応じてHot用テーブルまたはCold用テーブルを選ぶだけ、という運用です。
HotはOpenTelemetry Collector ContribのClickHouse Exporterが作成するテーブルを使用します。ColdはAWS Glueのテーブル/カラム名の制約に合わせて小文字でスキーマを定義し、そのうえでCold側にのみViewを用意して、timestamp_time -> TimestampTimeなど列名をリネームし、Hotと同じSQLでクエリできるようにします。
なお、ClickHouse CloudとS3は同一リージョンに揃えました。レイテンシを抑え、リージョン間転送の追加コストを避けるためです。
Hot(ClickHouse / SharedMergeTree)
CREATE TABLE otel.otel_logs ( `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)), `TimestampTime` DateTime DEFAULT toDateTime(Timestamp), `TraceId` String CODEC(ZSTD(1)), `SpanId` String CODEC(ZSTD(1)), `TraceFlags` UInt8, `SeverityText` LowCardinality(String) CODEC(ZSTD(1)), `SeverityNumber` UInt8, `ServiceName` LowCardinality(String) CODEC(ZSTD(1)), `Body` String CODEC(ZSTD(1)), `ResourceSchemaUrl` LowCardinality(String) CODEC(ZSTD(1)), `ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)), `ScopeSchemaUrl` LowCardinality(String) CODEC(ZSTD(1)), `ScopeName` String CODEC(ZSTD(1)), `ScopeVersion` LowCardinality(String) CODEC(ZSTD(1)), `ScopeAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)), `LogAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)), ... ) ENGINE = SharedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}') PARTITION BY toDate(TimestampTime) TTL TimestampTime + toIntervalDay(30) ...
Cold(S3 / Iceberg)
CREATE TABLE otel_iceberg.otel_logs ( timestamp_time timestamp, trace_id string, span_id string, trace_flags int, severity_text string, severity_number int, service_name string, body string, resource_schema_url string, resource_attributes map<string,string>, scope_schema_url string, scope_name string, scope_version string, scope_attributes map<string,string>, log_attributes map<string,string> ) PARTITIONED BY (day(timestamp_time));
ClickHouse CloudからはIceberg table engineで参照用テーブルを作成します。S3への認証にはIAMロールの委譲を使えるため、アクセスキーを埋め込まずに済みます。参照時はテーブル定義やクエリでextra_credentialsにrole_arnを渡す方式です。
CREATE TABLE otel.otel_logs_iceberg ( `timestamp_time` DateTime64(9), `trace_id` String, `span_id` String, `trace_flags` UInt8, `severity_text` LowCardinality(String), `severity_number` UInt8, `service_name` LowCardinality(String), `body` String, `resource_schema_url` LowCardinality(String), `resource_attributes` Map(LowCardinality(String), String), `scope_schema_url` LowCardinality(String), `scope_name` String, `scope_version` LowCardinality(String), `scope_attributes` Map(LowCardinality(String), String), `log_attributes` Map(LowCardinality(String), String) ) ENGINE = IcebergS3( 's3://<bucket-name>/otel_iceberg/', 'Parquet', extra_credentials('role_arn'='arn:aws:iam::<account-id>:role/<role-name>') );
そのうえで、Cold専用のViewを作ります。以降はこのViewを選べば、Hotと同じ列名でクエリできます。
CREATE VIEW otel.otel_logs_cold AS SELECT timestamp_time AS TimestampTime, trace_id AS TraceId, span_id AS SpanId, trace_flags AS TraceFlags, severity_text AS SeverityText, severity_number AS SeverityNumber, service_name AS ServiceName, body AS Body, resource_schema_url AS ResourceSchemaUrl, resource_attributes AS ResourceAttributes, scope_schema_url AS ScopeSchemaUrl, scope_name AS ScopeName, scope_version AS ScopeVersion, scope_attributes AS ScopeAttributes, log_attributes AS LogAttributes FROM otel.otel_logs_iceberg;
参照する際はパーティションプルーニングを有効化するためにuse_iceberg_partition_pruning=1を設定します。毎回ユーザーに意識させないよう、Settings Profileを作成してロール/ユーザーに付与します。
CREATE SETTINGS PROFILE iceberg_defaults SETTINGS use_iceberg_partition_pruning = 1; CREATE ROLE iceberg_role SETTINGS PROFILE iceberg_defaults; SET DEFAULT ROLE <existing_default_role>, iceberg_role TO <user>;
使い方
ClickHouse Cloud
テーブルを変えるだけです。直近はotel.otel_logs、長期はotel.otel_logs_cold。列名は揃えてあるので同じSQLがそのまま動きます。見る期間に応じてどちらかのテーブルを選ぶだけです。
# <table_name> を otel.otel_logs(直近) または otel.otel_logs_cold(長期)に置き換えるだけ SELECT * FROM <table_name> WHERE TimestampTime >= <from_ts> AND TimestampTime < <to_ts> AND ServiceName = 'my-service' AND SeverityNumber >= 17 LIMIT 10;
また、Hot/Coldを透過的に単一データソースとして扱いたい場合は、UNION ALLを使ったビューでまとめるやり方もあります。
CREATE VIEW otel.otel_logs_union AS SELECT TimestampTime, ServiceName, SeverityText, SeverityNumber, Body FROM otel.otel_logs UNION ALL SELECT TimestampTime, ServiceName, SeverityText, SeverityNumber, Body FROM otel.otel_logs_cold
ただしクエリごとにIceberg側のメタデータ参照でS3リクエストが増えやすく、レイテンシ/コスト面で最善とは言い切れません。基本はテーブル切替、透過ビューは用途を限定して使うのがよさそうです。
HyperDX
HyperDXはClickHouse上の任意テーブルをSourceとして登録し、Search / Dashboard から横断的に使えます。今回は Hot=otel.otel_logs、Cold=otel.otel_logs_coldの2つをそれぞれ別Sourceとして作成し、用途に応じてSourceを切り替える運用にします。

HyperDXのSourceでは、どの列をタイムスタンプ/サービス名/ログレベルとして扱うかを定義します。たとえば、Timestamp ColumnにはTimestampTimeやtimestamp_timeなど、スキーマに合わせて柔軟に設定できます。
Search画面では、左上のSourceドロップダウンから作成済みのSource(Hot=otel.otel_logs / Cold=otel.otel_logs_cold)に切り替えるだけで検索できます。直近の調査はHot、長期の調査はColdを選ぶという運用で統一できます。

活用ユースケース
データをIceberg化しておけば、オブザーバビリティ以外のデータ、たとえば売上や顧客行動といった事業データとも連携できます。 ストレージはS3のままClickHouse Cloudから直接参照でき、UI/クエリ言語を統一したまま横断分析が可能です。ツール切替や記法の違いによる負担を抑えつつ、サービス品質の変動が主要KPIにどう響いたかなどを、データ移送なしに同一クエリで検証できます。
おわりに
ClickHouse Cloud + IcebergによるHot/Coldログの単一UI化は、UI/クエリ言語切替の負担を下げる有効な選択肢になり得ると分かりました。今後は本番相当データでのベンチマークと詳細なコスト分析を行い、その結果に基づき採用可否を判断します。