Kafka / Spark / Flink / Iceberg で時刻(ts)の扱いを統一する設計
~2026年7月版(完全版)~
本記事は、Kafka・Hive・Spark・Flink・Iceberg・Trino・Grafanaで構成したログ分析基盤で、
時刻(ts)の扱いを統一するまでの検証結果をまとめたものです。
はじめに
ログ分析基盤では「時刻」は最も重要なデータです。
Kafka、Hive、Spark、Flink、Iceberg、Trino、Grafanaを組み合わせると、
各コンポーネントでタイムスタンプの扱いが少しずつ異なります。
私の環境でも、
- Sparkでは正しく見える
- TrinoではUTCに見える
- Grafanaだけ9時間ずれる
- 前日指定すると14時までしか表示されない
など、様々な問題が発生しました。
最終的には原因を切り分け、現在は全コンポーネントで統一できています。
システム構成
Kafka(JSON)
│
├─ Hive Raw
│ │
│ ├─ Hive View
│ │
│ ├─ Hive Curated(Parquet)
│ │
│ └─ Spark
│ │
│ ▼
│ Iceberg(syslog_iceberg)
│
└─ Flink(SQL)
│
▼
Iceberg(syslog_events)
│
▼
Trino
│
▼
Grafana
Kafkaの元データ
{
"ts":"2026-07-11T01:05:14+09:00",
"src_host":"raspi4-3",
"program":"tubu-frontend",
"message":"..."
}
ts は ISO8601(+09:00)で送信されています。
現在の設計方針
ts
イベント発生時刻そのもの。
全コンポーネントで同じ意味を持つ列です。
dt
JSTの日付。
パーティション・検索高速化用です。
hr
JSTの時間。
時間帯集計用です。
つまり
列 用途
ts イベント発生時刻
dt JST日付
hr JST時間帯
という役割分担にしています。
Hive Curated では、Parquet の TIMESTAMP を Spark/Iceberg へ直接渡すと
実行環境のタイムゾーン解釈に影響されることがあります。
そのため Hive Curated では次の列を分けて持ちます。
-
ts... Hive 内での確認・集計用 -
ts_text... Spark/Iceberg へ渡すためのyyyy-MM-dd HH:mm:ss文字列 -
ts_raw... Kafka JSON 由来の元文字列
Spark/Iceberg へ投入するときは、ts_text を TIMESTAMP_NTZ に変換します。
当初疑った原因
Hive Viewでは
cast(regexp_replace(substr(ts_raw,1,19),'T',' ') as timestamp)
としていました。
そのため
- offsetを捨てている
- absolute timeが失われる
と考えていました。
しかし検証した結果、
Spark→Iceberg→Trinoまでのデータ自体は問題ありませんでした。
本当の原因
最終的な原因はGrafana側でした。
以前は
at_timezone(ts,'Asia/Tokyo')
を使用していました。
Grafana自身もブラウザタイムゾーンへ変換するため、
結果として二重変換になっていました。
これにより
- 9時間ずれる
- 日付境界がおかしい
- 前日指定で14時までしか表示されない
という現象が発生しました。
現在のGrafanaクエリ
現在は余計な変換は行いません。
SELECT
CAST(date_trunc('hour', ts) AS timestamp) AS time,
COUNT(*) AS value
FROM iceberg.logs.syslog_iceberg
WHERE dt BETWEEN CAST(from_iso8601_timestamp('${__from:date:iso}') AS date)
AND CAST(from_iso8601_timestamp('${__to:date:iso}') AS date)
AND ts BETWEEN from_iso8601_timestamp('${__from:date:iso}')
AND from_iso8601_timestamp('${__to:date:iso}')
GROUP BY 1
ORDER BY 1;
これでGrafanaも正しく表示されます。
Spark側
SparkではHive CuratedからIcebergへ投入します。
投入時には
- ts
- dt
- hr
を生成します。
余計なタイムゾーン変換は行いません。
Flink側
FlinkでもTIMESTAMPとして保持しています。
Sparkと意味が一致するため、
リアルタイム系・バッチ系で同じクエリを書けます。
Trino
Trinoではtsをそのまま利用します。
通常は
SELECT ts
FROM iceberg.logs.syslog_iceberg;
で十分です。
不要な at_timezone() は使用しません。
ts_rawは必要か
以前はts_raw保持を推奨していました。
しかし現在は
- Kafka
- HDFS Raw
- Hive Raw
に元データが残るため、
Iceberg側では必須ではありません。
デバッグ用途で必要な場合のみ保持すれば十分です。
最終設計
Kafka(JSON +09:00)
│
▼
Hive Raw
│
▼
Hive View
│
▼
Hive Curated
│
▼
Spark/Flink
│
▼
Iceberg
│
▼
Trino
│
▼
Grafana
設計ルールは非常にシンプルです。
- ts = イベント発生時刻
- dt = JST日付
- hr = JST時間帯
役割を混ぜないことが重要です。
まとめ
今回分かったことは次のとおりです。
- SparkとFlinkの時刻は現在統一済み
- Icebergではtsをそのまま保持する
- Trinoでもそのまま利用する
- Grafanaでat_timezone()を重ねない
- dt/hrは検索・集計専用として持つ
この設計により、KafkaからGrafanaまで一貫した時刻管理が実現できました。
おわりに
時刻の問題は「保存」「検索」「表示」のどこで変換されるかを切り分けることが重要です。
今回の構成では、保存側ではなく表示側(Grafana)が原因でした。
同様の構成を構築する方の参考になれば幸いです。