当我们谈论可观测性数据时,总绕不开指标(Metrics)、日志(Logs)和追踪(Traces)这三个核心要素。就像医生需要体温、血液报告和 CT 影像来诊断病情,这三个数据维度能帮助我们全面洞察系统的健康状态。今天我们就来聊聊,如何用 GreptimeDB 为这些数据打造一个既高效又灵活的“病历系统”。
🧩 理解数据特性:先摸清家底再设计
在设计表结构前,我们需要先理解两个核心概念:
1. 列的基数:你的数据有多“独特”?
想象你正在组织一个图书馆:
- 低基数列就像“图书分类”——科幻、历史、艺术等,通常不超过几千个值,常见的有:
region
(地域)cluster_name
(集群名称)http_method
(请求方法)
- 高基数列则像每本书的 ISBN 码或每个读者的 ID,可能有数百万到亿万个不同值,例如:
trace_id
(追踪 ID)user_id
(用户 ID)request_id
(请求 ID)
在某电商监控项目中,region
(区域)是低基数列(只有 7 个值),而 user_id
是高基数列(亿级用户)。这个区别看似简单,但对性能影响巨大。
2. 三种列类型:数据的角色定位
在 GreptimeDB 中,每一列都扮演着特定角色:
- Tag:定义主键,确定数据如何排序和去重;
- Field:存储实际的测量值和其他非主键数据;
- Timestamp:通过
TIME INDEX
标识的时间列。
CREATE TABLE http_logs (
access_time TIMESTAMP TIME INDEX, -- 🕒 时间戳(必须项)
application STRING, -- 🏷️ 标签(建议作为主键)
http_status INT, -- 📊 数值型指标
request_id STRING, -- 🔍 高基数特征
PRIMARY KEY(application)
);
🔑 主键设计:选择合适的“数据门牌号”
场景一:无主键设计,高性能日志收集
对于纯粹的日志收集,我常用“无主键+追加模式”的设计:
CREATE TABLE access_logs (
timestamp TIMESTAMP TIME INDEX,
service STRING,
status_code INT,
response_time DOUBLE,
trace_id STRING
) with ('append_mode'='true');
这种设计通常写入速度最快,适合“写入一次,永不更新”的场景,写入速度提升 30%+!
场景二:优先选择低基数列
当需要对数据进行更新或按特定维度查询时,主键就变得很重要了。以下是我总结的黄金法则:
- 只选择低基数列:避免用
request_id
、trace_id
等作为主键; - 控制主键数量:主键值笛卡尔积建议不超过 10 万个;
- 限制主键列数:通常不超过 5 个列;
- 偏好字符串和整数:避免浮点数和时间戳类型作为主键。
一个不错的主键组合示例:
CREATE TABLE service_metrics (
datacenter STRING, -- 低基数:北美、欧洲、亚太等
service_name STRING, -- 低基数:用户服务、支付服务等
cpu_util DOUBLE,
memory_util DOUBLE,
ts TIMESTAMP,
PRIMARY KEY(datacenter, service_name),
TIME INDEX(ts)
);
🔍 查询加速三剑客:索引选择指南
GreptimeDB 提供了三种索引类型,我们可以根据数据特性选择需要的索引:
倒排索引:精准分类查找
适合低基数的有限分类的快速筛选:
CREATE TABLE api_metrics (
`endpoint` STRING, -- 🎯 服务接口名
service_name STRING INVERTED INDEX, -- 🎯 服务名称索引
http_status INT INVERTED INDEX, -- 🎯 状态码索引
latency DOUBLE,
ts TIMESTAMP,
PRIMARY KEY(service_name, `endpoint`),
TIME INDEX(ts)
);
倒排索引支持丰富的比较操作:=
,<
,>
,IN
和 BETWEEN
等。
跳数索引:大海捞针利器
对于很高基数的列的筛选,比如处理千万级用户 ID 的查询:
CREATE TABLE user_events (
user_id STRING SKIPPING INDEX, -- 对用户ID建立跳数索引
event_type STRING,
device STRING,
ts TIMESTAMP,
TIME INDEX(ts)
) with ('append_mode'='true');
跳数索引就非常有效,它占用空间更小,对写入的影响也很小,但它只支持等值查询。
全文索引:日志关键词搜索
CREATE TABLE error_logs (
message STRING FULLTEXT INDEX WITH(analyzer = 'English'),
`level` STRING INVERTED INDEX,
ts TIMESTAMP,
TIME INDEX(ts)
) with ('append_mode'='true');
这使你可以执行类似 SELECT * FROM error_logs WHERE matches(message, 'connection timeout')
的查询。
🧮 宽表与多表设计:如何组织多种指标
宽表优势:一站式数据超市
从我们的实践经验来看,把同时采集的多个指标放在一个宽表中通常是最佳选择:
CREATE TABLE node_metrics (
host STRING,
cpu_user DOUBLE,
cpu_system DOUBLE,
memory_used DOUBLE,
disk_read_bytes DOUBLE,
disk_write_bytes DOUBLE,
net_in_bytes DOUBLE,
net_out_bytes DOUBLE,
ts TIMESTAMP,
PRIMARY KEY(host),
TIME INDEX(ts)
);
宽表设计带来三大好处:
- 数据压缩率更高(节省 30-50% 存储空间);
- 关联指标查询更简单(无需复杂的
JOIN
)。
何时应该分表(多表)
只有在以下场景才考虑使用多表设计:
- 不同指标有完全不同的采集频率(如秒级 CPU 指标与小时级账单数据);
- 不同指标关联的元数据(标签)差异巨大;
- 出于数据访问权限管理需求。
🔄 数据去重与更新策略
在处理指标数据时,有时需要对同一时间点的数据进行更新。GreptimeDB 提供了两种合并模式:
last_row
模式:整行更新
默认情况下,GreptimeDB 使用 last_row
模式,保留最新插入的整行数据。
last_non_null
模式:字段级更新
当你只想更新个别字段时,last_non_null
模式更为合适:
CREATE TABLE device_telemetry (
device_id STRING,
temperature DOUBLE,
humidity DOUBLE,
battery DOUBLE,
ts TIMESTAMP,
PRIMARY KEY(device_id),
TIME INDEX(ts)
) with ('merge_mode'='last_non_null');
这样,你就可以只更新某个字段,不会影响其他字段的值:
INSERT INTO device_telemetry(device_id, temperature, ts)
VALUES ('device1', 25.5, now()); -- 只更新温度,保留其他字段现有值
🌐 分布式表设计:扩展到 PB 级数据
当数据量达到 TB 级别,单机存储不足以支撑时,我们可以考虑分区表设计:
CREATE TABLE global_metrics (
region STRING,
datacenter STRING,
host STRING,
cpu DOUBLE,
memory DOUBLE,
ts TIMESTAMP,
PRIMARY KEY(region, datacenter, host),
TIME INDEX(ts)
) PARTITION ON COLUMNS (region) (
region = 'region1',
region = 'region2',
region = 'region3'
);
选择分区键的关键是找到分布均匀且与查询模式匹配的列。根据我们的经验,地域、业务线或应用名称通常是不错的选择。
🛠️ 实战案例分析
案例一:大规模 API 监控系统
在一个处理每天数十亿API请求的系统中,我们使用以下设计:
CREATE TABLE api_metrics (
region STRING,
service STRING,
`endpoint` STRING,
status_code INT INVERTED INDEX,
p50_latency DOUBLE,
p95_latency DOUBLE,
p99_latency DOUBLE,
error_count INT,
success_count INT,
ts TIMESTAMP,
PRIMARY KEY(region, service, `endpoint`),
TIME INDEX(ts)
) PARTITION ON COLUMNS (region) (
region = 'us-east-1',
region = 'eu-central-1',
region = 'ap-southeast-2'
) with ('merge_mode'='last_non_null');
这个设计的优势是:
- 使用多级低基数主键,便于按服务或端点筛选;
- 将相关指标放在同一行,简化查询和提高压缩率;
- 对状态码建立索引,支持快速筛选错误请求;
- 按地域分区,减少跨区域查询的网络开销。
案例二:IoT 设备监控平台
对于一个监控数十万 IoT 设备的平台,我们采用了这样的设计:
CREATE TABLE device_metrics (
device_type STRING,
`location` STRING,
device_id STRING SKIPPING INDEX,
temperature DOUBLE,
humidity DOUBLE,
battery_level DOUBLE,
signal_strength DOUBLE,
alert_count INT,
ts TIMESTAMP,
PRIMARY KEY(device_type, `location`),
TIME INDEX(ts)
) PARTITION ON COLUMNS (device_type) (
device_type = 'sensor',
device_type = 'actuator',
device_type = 'controller'
);
优势:
- 使用
device_type
和location
作为低基数主键,而非高基数的device_id
; - 对
device_id
建立跳数索引,支持快速查找特定设备; - 将相关传感器数据存储在同一行,提高查询效率;
- 按设备类型分区,便于管理不同类型设备的数据保留策略。
💡 性能优化小贴士
基于我们的实际经验,这里分享六个能显著提升性能的小技巧:
- 从简单开始:先创建简单的无主键表,建立性能基准;
- 避免过度索引:索引会增加写入开销,只为常用查询条件创建索引;
- 定期监控表大小:当单表超过 500GB 时考虑分区;
- 时间分区已内置:GreptimeDB 的数据存储已经按照时间来分区组织,无需额外操作;
- 注意热点分区:确保写入和查询负载均匀分布在各分区;
- 合理设置 TTL:对不同重要程度的数据设置不同的保留期;
- 容量规划:写入满足的情况下,预留 50% 的系统资源给查询请求;
- 采样策略:使用 Flow 流计算任务来对数据做降采样,比如秒级降低为分钟级等。
数据建模是一门艺术,也是一门科学。通过合理的设计,你可以在保证查询性能的同时,大幅降低存储和运维成本。希望这份指南能帮助你在GreptimeDB上构建高效的可观测性数据平台!🎯
如果有任何问题,在评论区留言或加入我们的社区讨论。
作者注:本文基于GreptimeDB v0.13 及以上版本的特性撰写,随着新版本发布可能有所变化。
欢迎阅读数据建模指南官方文档
关于 Greptime
Greptime 格睿科技专注于为可观测、物联网及车联网等领域提供实时、高效的数据存储和分析服务,帮助客户挖掘数据的深层价值。目前基于云原生的时序数据库 GreptimeDB 已经衍生出多款适合不同用户的解决方案,更多信息或 demo 展示请联系下方小助手(微信号:greptime)。
欢迎对开源感兴趣的朋友们参与贡献和讨论,从带有 good first issue 标签的 issue 开始你的开源之旅吧~期待在开源社群里遇见你!添加小助手微信即可加入“技术交流群”与志同道合的朋友们面对面交流哦~
Star us on GitHub Now: https://github.com/GreptimeTeam/greptimedb
Twitter: https://twitter.com/Greptime
Slack: https://greptime.com/slack