共计 2732 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点:TB 级时序数据的处理困境
在物联网和监控领域,我们经常需要处理 TB 级的时序数据(比如 ck+ 数据集)。这类数据有几个典型特征:

- 数据量增长快:单个设备每天可能产生数百万条记录
- 写入吞吐高:经常需要支持数万甚至数十万 TPS 的写入
- 查询模式固定:80% 的查询都是基于时间范围的聚合分析
传统关系型数据库在这种场景下会面临三大挑战:
- 查询延迟高:全表扫描 TB 级数据可能需要分钟级响应
- 存储成本高:行式存储的压缩率通常不超过 3:1
- 并发写入瓶颈:B+ 树索引在高并发写入时会产生严重锁争用
技术选型:为什么选择 ClickHouse
我们对比了几种常见的时序数据处理方案:
| 方案 | 写入吞吐 | 查询延迟 | 压缩率 | 学习成本 |
|---|---|---|---|---|
| Elasticsearch | 中等 | 秒级 | 2-4x | 高 |
| Druid | 高 | 亚秒级 | 5-8x | 很高 |
| ClickHouse | 很高 | 毫秒级 | 10-20x | 中等 |
ClickHouse 的胜出关键在于:
- 列式存储:相同数据类型的数据连续存储,压缩率可达 20:1
- 向量化执行:利用 SIMD 指令并行处理数据块
- 稀疏索引:每个数据块只需记录区间范围,索引内存占用极小
核心实现方案
数据分片策略设计
对于时间序列数据,我们采用双重分片策略:
- 按天分区:PARTITION BY toYYYYMMDD(event_time)
- 按设备 ID 分片:通过集群配置实现数据分布
这种设计带来两个好处:
- 时间范围查询只需扫描特定分区
- 相同设备的数据集中在同一分片,减少分布式查询
MergeTree 引擎调优
建表示例包含关键参数说明:
CREATE TABLE metrics (
event_time DateTime,
device_id UInt32,
metric_name String,
metric_value Float64
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (device_id, metric_name, event_time)
SETTINGS
index_granularity = 8192, -- 每 8192 行一个主键索引
min_bytes_for_wide_part = 256MB -- 小分区自动使用 wide 格式
重要参数解析:
index_granularity:影响索引密度和查询性能的平衡min_bytes_for_wide_part:控制存储格式,大分区用 wide 格式提升扫描效率
物化视图预聚合
对于常用的分钟级聚合查询,我们创建物化视图自动计算:
CREATE MATERIALIZED VIEW metrics_1min
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (device_id, metric_name, toStartOfMinute(event_time))
AS SELECT
device_id,
metric_name,
toStartOfMinute(event_time) AS minute_time,
avgState(metric_value) AS avg_value,
maxState(metric_value) AS max_value
FROM metrics
GROUP BY
device_id,
metric_name,
toStartOfMinute(event_time)
代码实战示例
批量数据导入脚本
使用 Python 通过 native 协议高效导入:
import clickhouse_driver
from datetime import datetime, timedelta
import random
client = clickhouse_driver.Client('localhost')
def generate_data(device_count, days):
for day in range(days):
base_time = datetime.now() - timedelta(days=day)
for device in range(device_count):
values = []
for minute in range(1440): # 1440 分钟 / 天
for metric in ['cpu', 'mem']:
values.append((base_time + timedelta(minutes=minute),
device,
metric,
random.uniform(0, 100)
))
# 批量插入 10 万行 / 批次
client.execute(
'INSERT INTO metrics VALUES',
values,
types_check=True
)
高频查询模板
典型查询及其执行计划分析:
-- 查询某设备最近 24 小时的 CPU 最大值
EXPLAIN
SELECT
device_id,
max(max_value)
FROM metrics_1min
WHERE
device_id = 12345 AND
metric_name = 'cpu' AND
event_time >= now() - INTERVAL 24 HOUR
GROUP BY device_id
/* 执行计划关键节点:1. 只扫描 20231115 分区(分区裁剪)2. 使用主键索引定位 device_id=12345 的数据
3. 直接读取预聚合的 max_value 值
*/
生产环境建议
硬件资源配置
根据经验推荐配置:
- 内存:数据热集的 1.5 倍(如查询常访问最近 7 天数据,则需 7 天数据量的 1.5 倍内存)
- 磁盘:原始数据量的 3 倍空间(考虑压缩和临时文件)
避免 JOIN 的实践方案
ClickHouse 的 JOIN 性能较差,推荐以下替代方案:
- 预先关联:ETL 阶段处理好维度关联
- 字典表:使用
DICTIONARY引擎加载小维度表 - 嵌套结构:将常用维度作为嵌套字段存储
关键监控指标
需要持续关注的指标:
system.parts表中的 parts 数量(过多影响合并性能)system.metrics中的MemoryTracking(内存使用量)system.events中的SelectedRows(反映索引效率)
性能对比验证
我们使用 1TB 的 ck+ 数据集测试,与传统方案对比结果:
| 查询类型 | MySQL(ms) | ClickHouse(ms) | 提升倍数 |
|---|---|---|---|
| 单设备 24 小时统计 | 4200 | 23 | 182x |
| 全平台 1 小时聚合 | 68000 | 120 | 566x |
| 条件筛选 100 万条 | 3100 | 8 | 387x |
总结
通过合理的分片策略、MergeTree 调优和预聚合设计,ClickHouse 能够轻松应对 TB 级时序数据的处理需求。在实践中我们还需要注意:
- 根据查询模式设计主键顺序
- 控制分区大小在 1 -10GB 范围内
- 定期执行
OPTIMIZE TABLE FINAL合并小分区
对于需要处理海量时序数据的团队,ClickHouse 是目前性价比最高的选择之一。
正文完
发表至: 未分类
近两天内
