Doris 实战与架构

分析型数据库 / 源码阅读 / LESSON 26

Doris 可观测数据最佳实践——日志、Trace、指标的一体化落地手册

从采集、建模、分区分桶、索引、导入、冷热分层、物化视图、资源隔离和排障建立 Doris 可观测数据落地手册。

阅读时间
70 分钟
学习路径
Doris 实战与架构
内容来源
Doris 深度笔记

预计阅读时间: 70 分钟 前置阅读: doc-17, doc-18, doc-20, doc-22, doc-23 下一次阅读: 选择一个真实业务系统, 用本文的 POC 清单落地一条日志、Trace、指标闭环。


1. 这章补的不是语法, 而是落地方法

doc-22 已经讲过 Doris 为什么适合日志、Trace、指标和半结构化分析。这一章继续往生产实践走: 当你真的要把可观测平台建在 Doris 上, 应该如何设计采集链路、表模型、写入、索引、保留周期、看板加速、资源隔离和排障流程。

可观测数据的核心矛盾是:

写入必须持续
  查询必须及时
    成本必须可控
      字段还会不断变化

如果只把 Elasticsearch 或 ClickHouse 里的表结构照搬到 Doris, 大概率会踩三个坑:

  1. 把日志、Trace、指标塞进一张万能宽表, 导致排序、索引和保留策略互相牵制。
  2. 给所有文本和 JSON 字段都建倒排索引, 造成写入和 Compaction 成本失控。
  3. 只验证一条关键词查询, 没验证持续导入、Dashboard 并发、冷数据查询和故障恢复。

本文的目标是把 Doris 可观测数据平台拆成可执行的工程决策。

参考:


2. 总体架构: 先分清三条数据线

Doris 可观测数据最佳实践——日志、Trace、指标的一体化落地手册 图 01

最稳的结构不是一张大表, 而是三类事实表:

数据主用途推荐主表核心查询
日志 Log文本检索、错误定位、审计obs_log_detail时间窗口 + 服务 + 级别 + message MATCH
链路 Tracetrace_id 点查、接口耗时、错误路径obs_trace_spantrace_id 点查, service/operation 聚合
指标 MetricSLO、容量、RED/USE 看板obs_metric_rollup_1m时间序列聚合和下钻
事件 Event发布、告警、变更、审计obs_event_detail时间窗口 + 类型 + 实体

每条线都共享三个基础字段:

  • tenant_id: 多租户隔离和资源归因。
  • event_time: 分区裁剪和保留周期。
  • service_name / resource_service: 服务过滤和聚合。

但它们不能共享完全相同的 Key、索引和分桶策略。日志要照顾文本搜索, Trace 要照顾 trace_id, 指标要照顾时间聚合和标签基数。


3. 写入链路: 不要让 Doris 直接承受所有毛刺

推荐写入路径:

应用 SDK / Agent
  → Collector / Fluent Bit / Vector
    → Kafka
      → Flink 清洗、补字段、限流、分流
        → Doris Routine Load 或 Flink Connector

什么时候可以跳过 Kafka/Flink?

情况建议
小团队、自用系统、每天几十 GBCollector/Vector 直接 Stream Load 到 Doris
日志量稳定, 只做简单字段映射Kafka + Routine Load
需要清洗、脱敏、维表补充、标签归一Kafka + Flink + Doris
需要多下游同时消费Kafka 作为缓冲和重放层
有严格 exactly-once 与主键更新Flink CDC / Flink Connector, 并用 Label 或 checkpoint 对齐幂等

生产上更关注五个指标:

  1. 端到端可见延迟 P95。
  2. Kafka 消费延迟。
  3. Doris load 成功率和失败原因。
  4. 单次 load 的行数、字节数和触达分区数。
  5. Rowset/Version 增长和 Compaction 是否跟得上。

经验值:

  • 不要一条日志一次 Stream Load。
  • 每次 load 尽量只触达少量时间分区。
  • 日志和 Trace 优先走 append-only, 不要无意义地做 Unique Key。
  • 批量太小会堆版本, 批量太大会拖高失败重试成本。
  • Label 要由 source、topic、partition、offset range 或 checkpoint 组成, 不要随机生成。

4. 日志表: 先决定检索路径, 再决定索引

日志主表一般使用 Duplicate Key, 因为日志是追加事实。

CREATE TABLE obs_log_detail
(
    event_time DATETIME NOT NULL,
    tenant_id BIGINT NOT NULL,
    env VARCHAR(24),
    service_name VARCHAR(96),
    instance_id VARCHAR(128),
    severity VARCHAR(16),
    trace_id VARCHAR(64),
    span_id VARCHAR(64),
    request_id VARCHAR(96),
    message STRING,
    body_json VARIANT,
    resource_attrs VARIANT,
    INDEX idx_message (message) USING INVERTED PROPERTIES("parser" = "unicode"),
    INDEX idx_trace_id (trace_id) USING INVERTED,
    INDEX idx_request_id (request_id) USING INVERTED
)
DUPLICATE KEY(event_time, tenant_id, service_name)
AUTO PARTITION BY RANGE(date_trunc(event_time, 'day')) ()
DISTRIBUTED BY HASH(tenant_id) BUCKETS 32
PROPERTIES (
    "replication_num" = "3"
);

字段策略:

字段类型做法
高频过滤独立列, 例如 tenant_id, env, service_name, severity
高频点查独立列 + 倒排或 Bloom, 例如 trace_id, request_id
长文本STRING + 倒排索引, 按语言选择 parser
动态属性VARIANT, 热字段再提升为列
原始日志可以保留 raw_log, 但不要让所有查询都扫它

不要盲目加索引:

  • message 适合倒排, 但要压测写入成本。
  • service_name 如果常在 WHERE 中出现, 排序前缀和 Bitmap 往往比全文索引更自然。
  • trace_id 如果主要等值点查, 倒排或 Bloom 都要用真实查询对比。
  • 低选择性字段, 例如只有 INFO/WARN/ERRORseverity, 单独倒排意义不大。
  • 高频变动 JSON 子字段要先统计 TopN, 再决定是否列化或建索引。

5. Trace 表: 一张表很难同时满足点查和聚合

Trace Span 有两类典型访问模式:

  1. 输入 trace_id, 还原整条链路。
  2. 按服务、接口、状态码、耗时做聚合。

基础 Span 表:

CREATE TABLE obs_trace_span
(
    start_time DATETIME NOT NULL,
    tenant_id BIGINT NOT NULL,
    env VARCHAR(24),
    trace_id VARCHAR(64) NOT NULL,
    span_id VARCHAR(64) NOT NULL,
    parent_span_id VARCHAR(64),
    service_name VARCHAR(96),
    operation_name VARCHAR(160),
    span_kind VARCHAR(32),
    status_code VARCHAR(32),
    duration_ms BIGINT,
    resource_attrs VARIANT,
    span_attrs VARIANT,
    INDEX idx_trace_id (trace_id) USING INVERTED,
    INDEX idx_operation (operation_name) USING INVERTED
)
DUPLICATE KEY(start_time, tenant_id, service_name)
AUTO PARTITION BY RANGE(date_trunc(start_time, 'day')) ()
DISTRIBUTED BY HASH(tenant_id) BUCKETS 32;

如果链路还原是核心路径, 建议再补一张 Trace 点查表:

CREATE TABLE obs_trace_by_id
(
    trace_id VARCHAR(64) NOT NULL,
    start_time DATETIME NOT NULL,
    tenant_id BIGINT NOT NULL,
    service_name VARCHAR(96),
    operation_name VARCHAR(160),
    span_id VARCHAR(64) NOT NULL,
    parent_span_id VARCHAR(64),
    duration_ms BIGINT,
    status_code VARCHAR(32),
    span_attrs VARIANT
)
DUPLICATE KEY(trace_id, start_time, span_id)
AUTO PARTITION BY RANGE(date_trunc(start_time, 'day')) ()
DISTRIBUTED BY HASH(trace_id) BUCKETS 64;

这不是重复造表, 而是承认两种访问模式的物理路径不同:

  • obs_trace_span: 服务和接口聚合更友好。
  • obs_trace_by_id: trace_id 点查和链路还原更稳定。

专家要问:

  1. trace_id 查询是否必须秒级返回?
  2. 单条 Trace 的 Span 数是否可能上千?
  3. 是否要从日志点击 trace_id 跳转到链路?
  4. 采样策略在哪里执行?
  5. 错误 Span 是否需要单独加速?

6. 指标表: 原始点、分钟聚合和看板表要分层

指标不建议只存一张原始宽表。常见分层:

raw metric samples
  → 1m rollup
    → 5m / 1h rollup
      → SLO dashboard table

分钟聚合表示例:

CREATE TABLE obs_metric_rollup_1m
(
    ts_minute DATETIME NOT NULL,
    tenant_id BIGINT NOT NULL,
    env VARCHAR(24),
    service_name VARCHAR(96),
    metric_name VARCHAR(160) NOT NULL,
    tags_hash BIGINT NOT NULL,
    value_sum DOUBLE SUM,
    value_count BIGINT SUM,
    value_max DOUBLE MAX,
    value_min DOUBLE MIN
)
AGGREGATE KEY(ts_minute, tenant_id, service_name, metric_name, tags_hash)
AUTO PARTITION BY RANGE(date_trunc(ts_minute, 'day')) ()
DISTRIBUTED BY HASH(tenant_id) BUCKETS 32;

高基数标签是指标系统最大的成本陷阱:

标签建议
service, env, region, cluster独立列
http.route, status_code, method高频维度可列化
pod, container, host看容量场景决定是否列化
user_id, request_id, session_id不进入指标标签, 放日志或 Trace
长尾 tags归一后放 VARIANT 或外部维表

如果要做 RED/USE/SLO 看板, 不要让 Grafana 每次都扫明细:

  • RED: Rate, Errors, Duration。
  • USE: Utilization, Saturation, Errors。
  • SLO: 可用性、错误预算、延迟分位。

这些都适合用物化视图或独立汇总表承接。


7. 分区、分桶和排序: 让 90% 查询先被时间裁掉

可观测查询几乎都带时间窗口。表设计的第一目标是让时间裁剪稳定生效。

推荐:

场景分区粒度分桶建议排序前缀
高频日志, 每天 TB 级小时或天tenant_id 或随机分桶event_time, tenant_id, service_name
中等日志, 每天百 GBtenant_idevent_time, tenant_id, service_name
Trace 点查trace_idtrace_id, start_time 或单独点查表
指标聚合tenant_idservice_namets_minute, tenant_id, metric_name
审计事件天或月tenant_idevent_time, tenant_id, event_type

常见反例:

  • service_name 分区: 服务变多后分区不可控。
  • trace_id 分区: 分区数爆炸, 时间裁剪失效。
  • Bucket 数远大于数据量: 小 Tablet 增多, 导入和元数据成本上升。
  • 所有表统一 128 buckets: 没有根据 BE 数、分区数据量和查询并行度调整。

POC 时必须记录:

SHOW PARTITIONS FROM obs_log_detail;
SHOW TABLETS FROM obs_log_detail;
EXPLAIN SELECT ...;

确认计划里真的裁掉了无关分区, 而不是把一周、一个月的数据全扫掉。


8. 半结构化字段: VARIANT 负责长尾, 列负责热路径

OpenTelemetry 和日志字段天然会漂移。正确策略是“热字段列化, 长尾字段保留”。

字段晋升流程:

先进入 VARIANT
  → 统计 7-14 天查询频率和过滤选择性
    → 高频稳定字段提升为物理列
      → 必要时补索引或物化视图

例子:

字段初始位置晋升条件
http.status_codebody_json / span_attrs错误分析每天使用
k8s.namespace.nameresource_attrs多集群看板高频过滤
exception.typebody_json排障入口字段
db.statement长文本或 VARIANT需要检索, 且脱敏合规
user.id不建议进入指标审计或日志点查另行设计

两个底线:

  1. 不把所有 JSON 字段都建索引。
  2. 不把每个偶发字段都加成物理列。

专家级平台要有字段治理表:

字段来源查询频率基数是否列化是否索引保留周期

这张治理表比一开始追求完美 schema 更重要。


9. 冷热分层和保留周期: 可观测平台的成本生命线

可观测数据的价值随时间下降。保留周期要按使用目的设计:

层级周期示例存储查询期望
3-7 天Doris 内表, 关键索引完整秒级检索和看板
30-90 天Doris 内表, 精简索引或低副本分钟级分析可接受
180 天以上Iceberg/Hive/S3 归档审计和合规, 慢查询可接受

实践建议:

  • 热表保留完整倒排索引和高副本。
  • 温表可减少索引数量, 或只保留核心字段索引。
  • 冷数据进入对象存储或湖表, 通过 External Catalog 查询。
  • 冷数据查询若会进入常态运营, 再做 Data Cache、内表回灌或物化汇总。
  • 不要把“偶尔审计一次”的冷数据留在热表里消耗 SSD 和 Compaction。

删除策略优先级:

分区 TTL 删除
  > 分区迁移/归档
    > 条件 DELETE

对于日志和 Trace, 分区级生命周期最干净; 条件删除容易制造删除标记和额外 Compaction。


10. 看板加速: 不要让 Dashboard 每 10 秒扫明细

可观测看板往往有固定查询:

  • 最近 15 分钟错误数。
  • 服务 P95/P99 延迟。
  • 接口 TOP N。
  • 错误类型趋势。
  • 集群资源利用率。

这些适合用异步物化视图或汇总表。

示例:

CREATE MATERIALIZED VIEW mv_service_error_1m
BUILD IMMEDIATE
REFRESH AUTO
AS
SELECT
    date_trunc(start_time, 'minute') AS ts_minute,
    tenant_id,
    env,
    service_name,
    operation_name,
    count(*) AS span_count,
    sum(CASE WHEN status_code != 'OK' THEN 1 ELSE 0 END) AS error_count,
    avg(duration_ms) AS avg_duration_ms
FROM obs_trace_span
GROUP BY
    date_trunc(start_time, 'minute'),
    tenant_id,
    env,
    service_name,
    operation_name;

看板加速的关键不是“能不能建 MV”, 而是回答:

  1. 刷新频率是否匹配看板刷新?
  2. 刷新任务是否和在线查询隔离?
  3. 延迟指标是否允许近似或预聚合?
  4. 字段变更是否会破坏 MV?
  5. 失败后看板如何降级?

不要把物化视图刷新放进同一个高优先级资源池。否则异常刷新会抢走排障时最需要的查询资源。


11. 查询模板: 让用户先走正确路径

错误日志检索

SELECT event_time, service_name, severity, trace_id, message
FROM obs_log_detail
WHERE event_time >= now() - INTERVAL 30 MINUTE
  AND tenant_id = 1001
  AND env = 'prod'
  AND service_name = 'checkout'
  AND severity IN ('ERROR', 'FATAL')
  AND message MATCH 'timeout'
ORDER BY event_time DESC
LIMIT 200;

从日志跳 Trace

SELECT start_time, service_name, operation_name, span_id, parent_span_id, duration_ms, status_code
FROM obs_trace_by_id
WHERE trace_id = '7f0b8a...'
ORDER BY start_time ASC;

服务错误率

SELECT
    service_name,
    count(*) AS spans,
    sum(CASE WHEN status_code != 'OK' THEN 1 ELSE 0 END) AS errors,
    sum(CASE WHEN status_code != 'OK' THEN 1 ELSE 0 END) / count(*) AS error_rate
FROM obs_trace_span
WHERE start_time >= now() - INTERVAL 15 MINUTE
  AND tenant_id = 1001
GROUP BY service_name
ORDER BY error_rate DESC
LIMIT 20;

延迟趋势

SELECT
    ts_minute,
    service_name,
    sum(value_sum) / nullif(sum(value_count), 0) AS avg_latency_ms
FROM obs_metric_rollup_1m
WHERE ts_minute >= now() - INTERVAL 6 HOUR
  AND tenant_id = 1001
  AND metric_name = 'http.server.duration'
GROUP BY ts_minute, service_name
ORDER BY ts_minute ASC;

查询入口要给用户默认时间窗口和 LIMIT。无限时间范围的日志搜索, 即使 Doris 能跑, 也会把平台体验拖坏。


12. 资源隔离: 排障查询要比离线分析更重要

可观测平台最怕两件事:

  1. 故障时, 大量排障查询和看板刷新同时涌入。
  2. 平时, 离线分析或报表把在线检索资源占满。

建议至少分三类 Workload:

资源组用途策略
obs_realtime日志检索、Trace 点查、故障排查高优先级, 严格超时, 限制扫描范围
obs_dashboardGrafana/看板刷新中优先级, 稳定并发, 尽量读汇总表
obs_adhoc长周期分析、审计、探索低优先级, 允许排队和更长耗时
obs_etlMV 刷新、回补、冷热迁移独立窗口, 不抢故障排查资源

同时设置查询保护:

  • 默认查询必须带时间条件。
  • 超过扫描阈值的 SQL 需要二次确认或走异步任务。
  • 冷数据查询进入低优先级资源组。
  • 新用户和自动化任务设置更严格的超时。
  • 发生事故时可以临时降级 obs_adhocobs_etl

13. Doris 自身也要被观测

用 Doris 做可观测平台时, Doris 集群本身也必须进入监控。

至少采集:

层级指标或日志
FE查询延迟、队列、连接数、元数据内存、Journal、选主
BECPU、内存、磁盘、Tablet、Compaction、Scan、Load、网络
Load成功率、失败原因、耗时、触达分区、错误行
Query慢 SQL、Profile、扫描行数、返回行数、内存峰值
StorageRowset、Version Count、Compaction backlog、磁盘水位

可以把 Doris audit log 和系统指标也写入 Doris, 但要避免自我放大:

  • Doris 自身日志进入独立租户或独立库。
  • Doris 故障时, 仍要保留外部监控入口。
  • 不要让平台自己的审计日志无限递归写入。
  • audit_log 类查询单独做保留周期和脱敏。

14. 容量估算: 先算写入, 再算索引

容量估算模板:

日原始日志量
  × 压缩后比例
  × 副本数
  × 索引放大
  × 热保留天数
  / 磁盘安全水位

示例:

日原始日志 + Trace3 TB
压缩后比例25%
副本数3
索引放大1.35
热保留14 天
安全水位70%
3 TB * 0.25 * 3 * 1.35 * 14 / 0.7 ≈ 60.75 TB 可用磁盘

还要单独估计:

  • Compaction 临时空间。
  • 回补期间的双写或重放空间。
  • Kafka 保留空间。
  • 冷数据归档空间。
  • 物化视图和汇总表空间。

如果倒排索引覆盖字段很多, 索引放大不是一个固定常数, 必须用真实日志压测。


15. 排障手册: 从现象回到物理路径

关键词查询慢

1. 是否带时间窗口
2. 是否命中分区裁剪
3. message MATCH 是否命中倒排索引
4. service/tenant 过滤是否足够早
5. Profile 里 ScanRows 是否异常
6. 是否扫到了温/冷数据
7. 是否被 Compaction 或大查询抢资源

Trace 点查慢

1. trace_id 是否有索引或独立点查表
2. 查询是否仍带时间范围
3. trace_id 是否分布均匀
4. 单条 Trace Span 数是否异常
5. 是否需要从 trace 表回连日志表

Dashboard 抖动

1. 是否每次刷新都扫明细
2. 是否读了异步物化视图或汇总表
3. MV 刷新是否延迟
4. Dashboard 并发是否被资源组限制
5. 是否存在某个面板 SQL 无时间过滤

导入延迟上升

1. Kafka lag 是否上升
2. Routine Load task 是否暂停
3. Stream Load 是否失败重试
4. 单批是否触达太多分区
5. Version Count 是否升高
6. Compaction backlog 是否升高
7. BE 磁盘或内存是否接近水位

排障时不要只看 SQL 是否“应该很快”。用 EXPLAINProfile 证明:

  • 分区裁剪是否生效。
  • 索引过滤是否生效。
  • 实际扫描行数是多少。
  • 慢在 Scan、Join、Aggregate、Sort 还是 Exchange。
  • 资源组是否排队。

16. POC 验收清单

一个合格的 Doris 可观测 POC 至少覆盖这些场景:

类别验收问题
写入峰值写入、重试、错误数据、回补是否稳定
日志最近 15 分钟关键词检索是否秒级返回
Trace从日志 trace_id 跳链路是否稳定
指标SLO 看板是否读汇总层而不是明细层
字段新增 JSON 字段是否可写入, 热字段如何晋升
成本压缩率、索引放大、热温冷成本是否有实测
运维Doris FE/BE、Load、Compaction、Query 是否可观测
治理租户隔离、权限、脱敏、审计是否明确
故障BE 下线、Kafka 堆积、MV 刷新失败时如何降级

建议 POC 数据不要低于真实峰值的 30%, 否则看不出导入、Compaction 和索引构建的真实压力。对于日志平台, 只拿几百万行做演示很容易误判。


17. 最终判断

Doris 做可观测数据平台的优势不是“能把日志存进去”, 而是能把日志、Trace、指标、事件放到统一 SQL 分析体系里, 同时用列存、倒排索引、物化视图和湖仓分层降低长期成本。

专家级落地的关键是五句话:

  1. 日志、Trace、指标分表, 不做万能大表。
  2. 时间分区和租户过滤是第一层性能边界。
  3. 倒排索引用在高价值检索路径, 不滥用。
  4. Dashboard 读汇总层, 排障查询读明细层。
  5. 热数据服务故障排查, 冷数据服务审计合规。

一句话总结:

Doris 可观测最佳实践不是某一个建表模板, 而是一组围绕写入、检索、聚合、保留、隔离和排障的工程约束。约束设计清楚, Doris 才能同时承担日志搜索、链路分析、指标看板和成本治理。