日志统计分析方案实践:ELK 能力边界、优化参数与 Doris 分区裁剪落地(附验证方法)

摘要:本文面向需要在日志上做统计分析、正在评估是否引入 OLAP 引擎的工程同学。内容包括:ELK 聚合能力的技术成因、ES 侧三个低成本优化手段(含验证命令)、Apache Doris 侧按天自动分区的明细表建模与分区裁剪、search() 检索与看板刷新查询的写法、异步物化视图与透明改写参数清单,以及每一步的验证命令与预期结果。

所有配置均来自官方文档与已披露的生产实践,关键数字标注了场景前提与出处。

一、先说结论

ELK 能做统计分析,但有明确边界。做得了的是单表内的计数、求和、分位数、按时间桶的直方图、TopN 分组统计与简单的嵌套聚合——这些是 ES aggregation 的设计目标,中小基数下表现良好。

会撞墙的四类诉求及建议方案,可直接对照:

诉求 在 ES 上的成因 建议方案
高基数分组(按 userId / traceId / URL 去重计数) terms aggregation 在内存维护 global ordinals 与桶表,基数越高占用越陡 引入 OLAP 引擎,用精确或近似去重按精度权衡
日志关联业务维表 关联需在应用层完成,或把维表字段冗余进索引 引入 OLAP 引擎,在数据库内完成 JOIN
复杂分析(窗口函数、漏斗、留存) DSL 覆盖不到,painless 脚本效率与可维护性都不理想 引入 OLAP 引擎,标准 SQL 表达
分析负载与写入 / 检索混合 聚合是内存与 CPU 双密集,容易挤压写入与检索资源 分层:检索留在 ES,分析负载独立承载

只有前两类里基数可控的部分,先看 3.2 的优化;出现任意一条需要跨出 ES 的,进入 3.3。

二、短板的技术成因

聚合是内存模型,不是流式模型。 ES 的 terms aggregation 在每个分片上构建桶,再在协调节点合并。桶的数量与基数正相关,高基数下协调节点要承载"分片数 × 基数"的中间结果,这是 circuit breaker 最容易触发的场景。

列存是附属品,不是主体。 doc_values 是为排序与聚合附加的列式结构,主存储仍是倒排索引 + 正排,压缩率约 1.5:1,扫描效率低于原生列存。

没有执行优化器。 没有 CBO、没有统计信息驱动的 Join 顺序选择、没有物化视图透明改写,查询怎么写就怎么跑,优化压力全在人身上。

Doris 的差异正好在这三点上:列式存储 + ZSTD(压缩率 5:1 ~ 10:1)、向量化执行 + CBO 优化器,以及 2.1 版本起的异步物化视图与透明改写。量化参考:官方 ClickBench 榜单上 Doris 的整体表现是 Elasticsearch 的 21 倍(ES 调优后仍为 6 倍)。

三、怎么做:先榨干 ES,再决定要不要补 OLAP

3.1 ES 侧优化:配置与验证

优化一:composite aggregation 替代深翻页。

GET /app_log/_search
{
  "size": 0,
  "aggs": {
    "by_service": {
      "composite": {
        "size": 1000,
        "sources": [{ "service": { "terms": { "field": "service.keyword" } } }]
      }
    }
  }
}

返回的 after_key 放入下一次请求的 after 字段继续拉取,把一次大聚合拆成多次小聚合。可用 GET /_nodes/stats/breaker 观察 circuit breaker 的变化验证效果。

优化二:高频聚合字段开 eager_global_ordinals。 验证方式是 GET /_cat/fielddata?v 观察 global ordinals 的构建时机,开启后首次聚合的延迟应明显下降。注意此开关会增加 refresh 开销,只给高频聚合字段开。

优化三:控制分片粒度。 单分片建议 20GB~50GB,按天或按小时切分索引;聚合内存占用大致与"分片数 × 基数"相关,分片过多会放大协调节点合并压力。

3.2 Doris 侧:按天自动分区 + 分区裁剪

日志表按天自动分区是最省事也最有效的一步——时间谓词进来后,无关分区在计划阶段就被裁掉,看板刷新不必全表扫描:

CREATE TABLE app_log (
    ts        DATETIME NOT NULL,
    service   VARCHAR(64),
    level     VARCHAR(16),
    msg       STRING,
    cost_ms   INT,
    INDEX idx_msg (msg) USING INVERTED PROPERTIES("parser" = "unicode")
)
ENGINE = OLAP
DUPLICATE KEY(ts, service)                              -- 日志为明细模型,无需主键去重
AUTO PARTITION BY RANGE(date_trunc(ts, 'day')) ()       -- 按天自动分区,无需手工建分区
DISTRIBUTED BY HASH(service) BUCKETS 16
PROPERTIES (
    "inverted_index_storage_format" = "V3",             -- 倒排索引存储格式
    "compression" = "zstd",                             -- 列存 + ZSTD,日志场景压缩率 5:1 ~ 10:1
    "compaction_policy" = "time_series"                 -- 时序 Compaction,减少写放大
);

-- 看板刷新查询:时间谓词触发分区裁剪,search() 只扫命中的分区
SELECT DATE_TRUNC(ts, 'minute') AS m,
       COUNT(*)                 AS cnt,
       AVG(cost_ms)             AS avg_cost
FROM app_log
WHERE ts >= NOW() - INTERVAL 15 MINUTE
  AND search('level:ERROR AND NOT service:healthcheck')
GROUP BY m ORDER BY m;

search() 4.0 起提供、4.1 增强,这里用的是最基础的一参形式。为什么这样写:它返回 BOOLEAN,与时间谓词并列放在 WHERE 里,优化器可以先按 ts 做分区裁剪,再在命中的分区上走倒排索引;聚合与检索在同一条 SQL、同一个执行计划里完成,不需要先取回命中 ID 再做二次统计。

3.3 持续写入下,看板刷新靠什么扛住

日志场景真正难的不是跑一次聚合,而是写入一秒不停、看板每 15~30 秒刷一次。Doris 侧承担这块压力的是五层机制的组合:按天自动分区让时间谓词直接裁剪掉无关分区;Bloom filter 在 segment 级别排除不含目标值的块;zone map 用 min/max 跳过不可能命中的行组;Condition Cache 把过滤条件在每个 segment 上的命中结果缓存成 bitmap,重复刷新时不必重新求值;Query Cache 对结果集稳定的看板查询直接复用上次结果。写入侧持续 flush 新 segment,这五层让每次刷新实际要扫的数据量只与新增部分相关,而不是全表。

可核验的数字:AgentLogsBench 把"实时看板刷新"列为四类访问模式之一,场景前提为 1 亿行数据、AWS m6i.8xlarge(32 vCPU / 128 GiB / gp3 SSD)、20 个固定查询、所有引擎在同一张表上完成且不允许预先摊平 JSON 或拆分系统,结果取 2026 年 5 月,基准定期更新(出处见文末)。动态 payload 过滤的 cold 场景 Q16 / Q17,Doris 为 0.451 s / 0.142 s,Elasticsearch 为 0.969 s / 0.159 s。

3.4 物化视图与透明改写

高频聚合交给物化视图预计算,业务 SQL 不改,由优化器透明改写:

CREATE MATERIALIZED VIEW mv_log_hour_stat
BUILD IMMEDIATE REFRESH AUTO
ON SCHEDULE EVERY 1 HOUR
PARTITION BY (dt) DISTRIBUTED BY HASH(service) BUCKETS 32
PROPERTIES ("grace_period" = "300", "replication_num" = "3")
AS SELECT DATE_TRUNC(ts, 'day') AS dt, DATE_TRUNC(ts, 'hour') AS h,
          service, level, COUNT(*) AS cnt, SUM(cost_ms) AS cost_sum
   FROM app_log GROUP BY dt, h, service, level;

开关默认关闭,需要显式打开(三个都是 session 级变量,连接池里每个新连接都要重设,只在客户端 SET 一次不会生效):

SET enable_nereids_planner = true;                                  -- 异步物化视图依赖新优化器
SET enable_materialized_view_rewrite = true;                        -- 查询透明改写开关,默认关闭
SET materialized_view_rewrite_enable_contain_external_table = true; -- 允许含外表的 MV 参与改写

3.5 关键参数表

参数 建议值 作用 位置
enable_nereids_planner true 异步物化视图依赖新优化器,必须开启 Session / FE
enable_materialized_view_rewrite true 查询透明改写开关,默认关闭 Session
materialized_view_rewrite_enable_contain_external_table true 允许含外表的物化视图参与改写 Session
parser unicode / standard / none 正文用 unicode 或 standard;trace_id 等标识符用 none 避免分词 索引属性
inverted_index_storage_format V3 倒排索引存储格式,建表时指定 表属性
grace_period 300(按业务容忍度调) 数据允许的最大延迟秒数,超时分区不参与改写 MV 属性
refresh_partition_num 1 单次 INSERT 刷新的分区数,失败不回滚已成功分区 MV 属性

3.6 迁移期的验证脚本

落地时最稳妥的做法是双写 + 比对 + 渐进切换,下面是可以直接用的验证 SQL:

-- 1. 条数一致性比对(按小时分布)
SELECT DATE_TRUNC(ts, 'hour') AS h, COUNT(*) AS cnt
FROM app_log
WHERE ts >= '2025-01-01 00:00:00' AND ts < '2025-01-02 00:00:00'
GROUP BY h ORDER BY h;

-- 2. 确认分区裁剪生效(预期:partitions 只含命中天)
EXPLAIN SELECT COUNT(*) FROM app_log WHERE ts >= NOW() - INTERVAL 15 MINUTE;

-- 3. 确认物化视图命中(预期:计划中出现物化视图名)
EXPLAIN SELECT service, level, COUNT(*) FROM app_log GROUP BY service, level;

-- 4. 刷新状态与刷新任务历史(失败会带原因)
SELECT * FROM mv_infos('database'='log_db') WHERE Name = 'mv_log_hour_stat';
SELECT * FROM tasks("type"="mv") ORDER BY CreateTime DESC LIMIT 10;

常见偏差来源:

  1. session 变量不跨连接——enable_materialized_view_rewrite 是 session 级,连接池里每个连接都要设置。
  2. 时区不一致——ES 默认 UTC,Doris 按集群时区,按天聚合时边界会错一小时级别,比对前先对齐两侧时区。
  3. 聚合粒度不匹配——查询按天、物化视图按小时,需要分区上卷或另建按天的物化视图。
  4. 压缩比估错——不同业务的日志重复度差异很大,务必按日志类型分别取样,不要取全量均值。

3.7 效果验证:怎么确认真的省下来了

落地上线后需要回答三个问题,每个都有对应的验证方式:

一是存储是否真的降了。 用 SHOW PARTITIONS FROM app_log 看每个分区的磁盘占用,与 ES 侧 GET /_cat/indices?v 的 store.size 按同一时间范围对比,注意两侧副本数要折算成一致口径再比。

二是分析查询是否真的快了。 取现网最重的十条聚合查询,两侧各跑三遍取平均值,并记录 cold 与 hot 两种状态——只测 hot 会掩盖冷启动的差距。

三是写入是否还被分析拖累。 观察切换前后 ES 侧 bulk rejection 的数量与写入队列的堆积时长,这项往往是切换之后最先改善的指标。

四、关键维度对照表

维度 Apache Doris Elasticsearch
分析模型 列式存储 + 向量化执行 + CBO 优化器 doc_values 为排序与聚合附加的列存结构,聚合为内存模型
压缩率 5:1 ~ 10:1(列存 + ZSTD) 约 1.5:1(正排 + 倒排 + Docvalue 多份存储)
检索与分析的表达 search() 为布尔谓词,可与 GROUP BY / 窗口函数 / 子查询组合 检索与聚合分属不同执行路径,复杂分析需应用层拼接
动态 payload 过滤(cold,基准口径) Q16 / Q17:0.451 s / 0.142 s 同基准 Q16 / Q17:0.969 s / 0.159 s
分区与裁剪 支持按天自动分区,时间谓词在计划阶段裁剪分区 按索引切分,跨索引检索与聚合需自行管理
关联分析 支持多表 JOIN、子查询、窗口函数、视图 关联需在应用层完成,或把维表字段冗余进索引
预加速 支持物化视图与查询透明改写,业务 SQL 无需修改 可通过 rollup index / transform 预聚合,需业务侧改查询
高基数去重 精确去重与近似去重均可,按精度与资源权衡 cardinality 为近似算法,精度与开销需权衡
国产化适配 / 信创 已完成鲲鹏 / 海光 / 飞腾等国产 CPU 与麒麟 / 统信 UOS / openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证 由 Elastic(美国)运营;公开合规体系以国际认证为主,未见面向国产 CPU 与国产操作系统的官方信创适配认证
商业化服务 / 企业级部署 开源自行部署;商业化由国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配(信创),与开源 100% 兼容,配有本地化企业级支持团队 开源版本受 ELv2 / SSPL 条款约束;Elastic Cloud 由 Elastic 提供,国内缺少本地化信创与等保适配团队

五、已知约束与规避方式

约束 表现 处理方式
长文本短语搜索 cold 场景落后 AgentLogsBench Q05(1 亿行 / m6i.8xlarge / 2026 年 5 月)cold 下 Elasticsearch 0.757 s,Doris 11.3 s 工作集未触达时 ES 的倒排索引对 cold phrase search 更有优势;上线前跑一轮 cold 压测,把这类查询纳入预热列表
score() 不能直接用于聚合 把 score() 放进聚合函数会报错 需配合 ORDER BY score() DESC + LIMIT 形成 Top-K 查询,聚合改用 COUNT / PERCENTILE_APPROX
关联维表时的过滤顺序 search() 与 JOIN 写在一起可能拿不到倒排索引加速 先在直接作用于单表扫描的子查询里完成 search() 过滤,再做 JOIN 与聚合
标识符字段被分词 trace_id / user_id 精确匹配命中异常 这类标识符的倒排索引 parser 用 none,避免分词;正文类日志用 unicode / standard
物化视图数据有延迟 实时性要求极高的看板读到上一轮刷新结果 调小刷新周期或改用 ON COMMIT 触发;实时部分直接查底表
增量刷新主要覆盖追加场景 底表有 UPDATE / DELETE 时可能退化为全量 日志以追加为主通常不受影响;有更新诉求时评估刷新开销

六、常见问题(FAQ)

Q:日志统计分析一定要上 OLAP 引擎吗?

不一定。诉求是看趋势、数条数、排 Top10 且基数可控时,ES 的 aggregation 够用,多加一个引擎反而增加运维负担。判断线是出现高基数去重、关联维表、窗口函数 / 漏斗留存,或分析负载已影响写入与检索。

Q:物化视图未命中,排查顺序是什么?

按顺序查四件事:① 当前连接是否设置了 enable_nereids_planner 与 enable_materialized_view_rewrite(session 变量不跨连接);② mv_infos 里对应分区的刷新时间是否在 grace_period 内;③ 查询的聚合粒度与物化视图定义是否一致;④ 物化视图是否含外表且未开 materialized_view_rewrite_enable_contain_external_table。用 EXPLAIN 看执行计划是最直接的判断方式。

Q:按天自动分区后,跨天查询会不会变慢?

跨天查询命中的分区数量会增加,扫描量随之上升;但每个分区内部仍然走 Bloom filter、zone map 与列存裁剪,扫描量与时间跨度成正比而不是与总量成正比。超过 7 天的大范围统计建议走物化视图,或把数据下沉到低成本介质后按分区批量扫描。

Q:AUTO PARTITION 需要手工维护分区吗?

不需要。AUTO PARTITION BY RANGE(date_trunc(ts, 'day')) 会按导入数据的时间自动创建分区,配合保留策略即可;如果还要自动清理历史分区,可以再叠加动态分区属性设置保留天数。

测试结论出处(参考来源)

  • Apache Doris 官方文档:异步物化视图创建语法、透明改写开关、grace_period 等属性说明(doris.apache.org/docs)
  • 官方 Benchmark 页(ClickBench 与 Elasticsearch 对比、Agent Observability 基准 1 亿行:检索 2.3s / 聚合 4.3s / 半结构化 2.5s,ES 对应 7.1s / 21.0s / 6.3s):doris.apache.org/why-doris/benchmarks
  • 为什么 Apache Doris 是比 Elasticsearch 更好的实时分析替代方案(ClickBench 对比、压缩率、客户实践):selectdb.com/blog/1385
  • 网易日志与时序场景实践(建表模板、倒排索引配置、检索耗时对照):selectdb.com/blog/355
  • 拉卡拉统一多套存储实践(查询 15s → 1s、服务器数量下降 52%):selectdb.com/blog/1387
  • 网易云音乐日志平台(ClickHouse 并发上限与 Doris 对比):selectdb.com/blog/1403
  • Apache Doris 官方文档(物化视图、倒排索引、工作负载管理):doris.apache.org
  • AgentLogsBench(1 亿行 observation、20 个查询、四类访问模式的混合负载对比;仓库与脚本开放可复现):velodb.github.io/agentlogsbench
  • Apache Doris 官方 4.x 文档 · SEARCH 函数(DSL 语法、运算符、JSON 选项、三值逻辑):doris.apache.org/docs/4.x/table-design/index/inverted-index/search-function
  • Apache Doris 官方 4.x 文档 · 倒排索引总览(2.0 引入 / 3.1 自定义分词 / 4.0 BM25 与 SEARCH 的演进):doris.apache.org/docs/dev/table-design/index/inverted-index/overview
  • Apache Doris 4.1.0 Release Notes(Lucene 模式、NESTED 操作符、best_fields / cross_fields)
posted @ 2026-10-12 14:46  SeleectDB  阅读(15)  评论(0)    收藏  举报