在前兩篇文章中,我們探討了 Data Lakehouse 的概念演進,以及 Apache Iceberg 如何透過三層架構實現高效的 metadata 管理。但無論架構設計多麼精巧,最終決定查詢效能的關鍵仍然是底層的資料儲存格式。
Apache Parquet 是 Iceberg 支援的資料儲存格式之一,本身也是欄式(columnar)儲存的檔案格式,其特性使得在處理一定規模的資料時,將更有效率。而列式(row-based)與欄式(columnar)的差異,我們在Day 04 - Observability 2.0 的資料觀點:統一的結構化日誌已有提及,因此本文將不會重複說明。
欄式儲存格式中,除了 Parquet 還有 ORC、CarbonData 等選擇。Parquet 除了是目前主流的 Data Lakehouse 資料格式之一,並具有良好的資料壓縮能力以外,在 AWS 等雲端平台上也有良好的支援。例如 AWS 去年底推出的 S3 Tables 就是以 Parquet 儲存結構化資料。
因此,本篇文章將從 Parquet 的資料特性出發,接著透過實作來了解 Parquet 的底層設計,並進一步地了解欄式儲存格式是如何提高分析資料時的效能。
Parquet 本身優勢在於高效的資料處理,因此在設計的時候,已經考慮到後續的資料批次處理、壓縮、查詢優化等議題。
接下來,讓我們設計一個實驗,透過實際生成 OTLP 資料並將之轉換為 Parquet 格式,進一步了解 Parquet 的架構設計理念。
首先在本地起一個 node exporter 用來暴露本機上的 system level metrics,接著,起一個 OpenTelemetry Collector 來 pull 這些 metrics。透過 Collector,將這些 metrcis 先匯出成 JSON 格式。
以下為 Otel Collector 的設定檔:
receivers:
prometheus:
config:
scrape_configs:
- job_name: 'node-exporter'
scrape_interval: 5s
static_configs:
- targets: ['localhost:9100']
processors:
batch:
timeout: 10s
send_batch_size: 1024
exporters:
# 匯出到 HTTP 端點 (用於接收處理過的資料)
otlphttp:
endpoint: http://host.docker.internal:8080
headers:
"Content-Type": "application/json"
encoding: json
compression: none # 為降低實驗中處理資料的複雜度,先不壓縮資料
service:
pipelines:
metrics:
receivers: [prometheus]
processors: [batch]
exporters: [otlphttp]
接著用 FastAPI 建立一個HTTP server 來接收 metrics 並直接轉換成 Parquet 格式:
from fastapi import FastAPI, Request
import pandas as pd
import uvicorn
from datetime import datetime
app = FastAPI()
@app.post("/v1/metrics")
async def receive_metrics(request: Request):
"""接收 OTel metrics 並轉換成 Parquet"""
data = await request.json()
metrics_records = extract_metrics_data(data)
if metrics_records:
df = pd.DataFrame(metrics_records)
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ns')
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')[:-3]
filename = f'metrics_{timestamp}.parquet'
df.to_parquet(filename, compression='snappy', index=False) # 透過 pandas 套件將 json 轉換成 parquet
print(f"接收 {len(metrics_records)} 筆 metrics → {filename}")
return {"status": "success", "records_processed": len(metrics_records)}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8080)
讀者應該有注意到,我們將原本 OTel 傳送進來的 json 檔,使用 extract_metrics_data
這個 function 重新整理,這個 extrac_metrics_data 主要用來攤平(flatten) OTLP 格式的 JSON 檔。
由於怕文章過於冗長,這裡先不附上這個函式的程式碼,以下將會講解這段程式碼的實作邏輯。
回顧Day 09中提及的 Metrics OTLP 格式,會發現它是一個巢狀架構,而這樣的架構是無法被欄式儲存格式給讀懂的。因此,在轉換成 Parquet 之前,我們會先選定好需要的資料以及欄位,接著再把這個巢狀資料給攤平:
攤平前的 OTLP 格式:
{
"resourceMetrics": [{
"resource": {
"attributes": [{"key": "service.name", "value": {"stringValue": "node_exporter"}}]
},
"scopeMetrics": [{
"scope": {"name": "prometheus"},
"metrics": [{
"name": "node_cpu_seconds_total",
"gauge": {
"dataPoints": [{
"attributes": [
{"key": "cpu", "value": {"stringValue": "0"}},
{"key": "mode", "value": {"stringValue": "idle"}}
],
"timeUnixNano": "1703145600000000000",
"asDouble": 1234.56
}]
}
}]
}]
}]
}
攤平後的平面 JSON 格式:
[
{
"timestamp": "2023-12-21T10:00:00",
"metric_name": "node_cpu_seconds_total",
"value": 1234.56,
"service.name": "node_exporter",
"attr_cpu": "0",
"attr_mode": "idle"
}
]
這樣的平面結構轉換成 pandas DataFrame 後,就能形成如下的表格格式:
timestamp | metric_name | value | service.name | attr_cpu | attr_mode |
---|---|---|---|---|---|
2023-12-21 10:00:00 | node_cpu_seconds_total | 1234.56 | node_exporter | 0 | idle |
攤平的核心邏輯就是:遍歷巢狀結構中的每個 data point,將相關的屬性都提升到同一層級。
簡單來說,我們要做的就是:
resourceMetrics[].resource.attributes
提取服務資訊scopeMetrics[].metrics[].gauge.dataPoints[]
提取實際的 metric 資料點這樣原本需要層層解析的巢狀資料,就變成了一個個獨立的資料記錄。
透過這個攤平過程,原本深度巢狀的 JSON 結構變成了平面的 JSON 陣列,再透過 pandas DataFrame 轉換成表格格式,最後儲存為 Parquet。這樣每一行都是一個獨立的 metric data point,每一欄都是可以被 Parquet 高效查詢的欄位。
poetry run python fastapi_server.py
docker-compose up -d
,這裡包含了 node_exporter
和 otel-collector
兩個container,礙於篇幅這裡就不多放置 docker compose 的設定檔。Collector 會從 node_exporter 拉取 metrics,處理後發送到我們的 FastAPI server,server 會立即將資料轉換成 Parquet 檔案儲存到本地。
為了能更好地理解 Parquet 內部的架構,以及理解 OTLP 資料是如何被轉換成 Parquet 格式,設計了一個實驗來完善這個流程。雖然在實驗中,我們僅透過程式碼來完成資料的 ETL,但實際上,目前已有許多成熟的雲端服務能協助我們做這些資料轉換,這我們在後續的章節也將會一一介紹。
明天,讓我們透過今天轉換出來的 Parquet,實際來解析 Parquet 檔案內部的架構吧!
Alex Merced - All About Parquet Part 02 — Parquet’s Columnar Storage Model
Amo Chen - Apache Parquet 深度介紹與說明
opentelemetry-collector - otlphttpexporter
opentelemetry-collector-contrib - prometheusreceiver