这是一篇实操笔记。不讲选型道理,只讲:ELK 在哪撞墙、撞墙之后 Apache Doris 侧怎么配、配完怎么验证、出问题怎么排。
默认你已经会用 ES 的 aggregation,遇到了高基数、关联维表或者漏斗留存这类需求,正在找个能落地的补位方案。配置来自官方文档与已披露的生产实践,我整理成可以直接抄的形式,另外附上四个真实踩过的坑。
一、先说结论
ELK 能做统计分析,但边界比多数人以为的要近。做得了的:单表内的计数、求和、分位数、时间直方图、TopN 分组、简单嵌套聚合。
撞墙的四类,我在生产里全遇到过:
| 诉求 | 现场表现 |
|---|---|
| 高基数分组 | terms agg 内存与基数正相关,circuit breaker 随机熔断,加机器边际收益衰减 |
| 日志关联维表 | 关联逻辑只能放应用层,或把维表字段冗余进索引,维表一改索引就得重建 |
| 窗口函数 / 漏斗 / 留存 | DSL 写不出来,painless 能写但慢且没人愿意维护 |
| 分析与写入混跑 | 大聚合一跑,写入队列堆积,最后报 bulk rejection |
判断线:出现任意一类,就该把分析负载分出去。
二、撞墙的三个结构性原因
聚合是内存模型。 分片各自建桶、协调节点合并,内存占用大致是"分片数 × 基数",这是熔断的主战场。
列存是附属结构。 doc_values 只是为排序与聚合附加的列式结构,压缩率约 1.5:1,扫描效率低于原生列存。
没有执行优化器。 没有 CBO、没有统计信息驱动的 Join 顺序选择、没有物化视图透明改写,优化压力全在人身上。
Doris 对这三点分别是:列式存储 + ZSTD(压缩率 5:1 ~ 10:1)、向量化执行 + CBO 优化器、2.1 版本起的异步物化视图与透明改写。量化参考:官方 ClickBench 榜单上 Doris 的整体表现是 Elasticsearch 的 21 倍(ES 调优后仍为 6 倍)。
三、怎么做
3.1 ES 侧先试三个补救
成本都很低,撞墙后建议先试:composite aggregation 替代深翻页 (terms agg 的 size 越大内存越吃紧,composite 用 after_key / after 游标分批拉,把一次大聚合拆成多次小聚合);高频聚合字段开 eager_global_ordinals (global ordinals 默认懒加载,预构建能消掉首次聚合的那波延迟,代价是 refresh 变慢,只给高频字段开);控制分片粒度(单分片 20GB~50GB,按天或按小时切)。
这三个做完还撞墙,就别在 ES 上耗了。
3.2 VARIANT 嵌套数组:先搜后聚合
Agent Trace 这类半结构化日志,嵌套数组里的字段是分析的关键维度。4.1 起的 NESTED 算子可以直接在 VARIANT 嵌套 JSON 数组内部搜索,不用 ETL 预处理或拆表,搜完直接聚合:
sql
-- steps 是 VARIANT 列里的嵌套数组,4.1 起支持 NESTED
CREATE TABLE app_log (
ts DATETIME NOT NULL,
trace_id VARCHAR(64),
service VARCHAR(64),
cost_ms INT,
steps VARIANT,
INDEX idx_steps (steps) USING INVERTED PROPERTIES("parser" = "unicode")
)
ENGINE = OLAP
DUPLICATE KEY(ts, trace_id)
AUTO PARTITION BY RANGE(date_trunc(ts, 'day')) ()
DISTRIBUTED BY HASH(trace_id) BUCKETS 16
PROPERTIES ("inverted_index_storage_format" = "V3");
-- 在嵌套数组内搜索,再对命中的行做分组、去重与分位统计
SELECT service,
COUNT(*) AS fail_cnt,
COUNT(DISTINCT trace_id) AS trace_cnt,
PERCENTILE_APPROX(cost_ms, 0.95) AS p95_cost
FROM app_log
WHERE search('NESTED(steps, status:error AND tool:code_exec)')
AND ts >= NOW() - INTERVAL 2 HOUR
GROUP BY service
HAVING fail_cnt > 5
ORDER BY fail_cnt DESC;
为什么这样写:search() 返回 BOOLEAN,是 WHERE 里的一个普通谓词,所以 NESTED 搜完之后 GROUP BY、COUNT(DISTINCT)、HAVING 全部接得上,一条 SQL 走完"定位 + 统计"。索引直接建在 VARIANT 列上,不用为了检索把数组摊平成辅助表。
3.3 看板刷新怎么扛住并发写入
写入不停、看板每 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 月,基准定期更新(出处见文末)。JSON Path 查询集(Q07 / Q10 / Q12 / Q16 / Q17 / Q18 / Q20)上,ES / OpenSearch 的平均延迟约为 Doris 的 2.4 倍;动态 payload 过滤 Q16 / Q17 hot,Doris 0.078 s / 0.030 s,Elasticsearch 0.802 s / 0.053 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, COUNT(*) AS cnt, SUM(cost_ms) AS cost_sum
FROM app_log GROUP BY dt, h, service;
最爽的一点是查询不用改------开了透明改写,老 SQL 自动走物化视图。开关默认关闭,必须显式打开:
ini
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 |
inverted_index_storage_format |
V3 |
倒排索引存储格式,建表时指定 | 表属性 |
parser |
unicode / standard / none |
正文用 unicode 或 standard;trace_id 等标识符用 none 避免分词 | 索引属性 |
grace_period |
300 |
数据允许的最大延迟秒数,超时分区不参与改写 | MV 属性 |
workload_group |
指定资源组 | 限制刷新任务资源占用,避免影响在线查询 | MV 属性 |
3.6 怎么确认改写真的生效了
这一步不能省。 开了开关不等于命中:
sql
-- 看执行计划里是否出现物化视图名
EXPLAIN SELECT DATE_TRUNC(ts, 'hour') AS h, service, COUNT(*)
FROM app_log GROUP BY h, service;
-- 刷新状态与刷新任务历史(失败会带原因,最快的定位方式)
SELECT * FROM mv_infos('database'='log_db') WHERE Name = 'mv_log_hour_stat';
SELECT * FROM tasks("type"="mv") ORDER BY CreateTime DESC LIMIT 10;
3.7 四个排错现场
现场一:开了 enable_materialized_view_rewrite 但完全没命中。 九成是 session 变量没生效。这是 session 级的,连接池里每个新连接都要重设,在客户端 SET 一次没用。正确做法是放进连接池初始化 SQL,或在 FE 的 custom_config 里配全局默认:
sql
SHOW VARIABLES LIKE '%materialized_view_rewrite%';
SHOW VARIABLES LIKE '%nereids_planner%';
现场二:EXPLAIN 里还是扫底表,但物化视图明明是新鲜的。 查 grace_period------超过这个时间没刷新的分区不参与改写,查询会回落到明细表。刷新失败或刷新周期大于 grace_period 时,命中率会掉到 0。
现场三:聚合粒度对不上。 查询按天聚合、物化视图按小时建,命中不了。两个解法:用分区上卷,或者另建一个按天的物化视图。别指望优化器帮你做跨粒度推导。
现场四:物化视图刷新把在线查询拖慢了。 刷新任务本身要吃资源,给物化视图指定独立的 workload_group:
sql
ALTER MATERIALIZED VIEW mv_log_hour_stat SET ("workload_group" = "mv_refresh_group");
另外 refresh_partition_num 控制单次 INSERT 刷新的分区数,默认 1。调大能加快刷新,但单次失败的影响面也变大,日志场景一般保持默认。
3.8 检索改写:从 DSL 到 SQL 的成本
真正花时间的是把现网 DSL 翻成 SQL,我的做法是先翻最核心的十几条,别一次性全量迁。search() 的 DSL 兼容 Lucene 与 query_string 风格,运算符 TERM / PHRASE / WILDCARD / REGEXP / PREFIX / NOT / NESTED 可任意嵌套组合,AND / OR / NOT 与括号的语义跟原查询串基本一致,所以大部分改动集中在字段名和分页写法上。翻之前先把要检索的字段想清楚:正文类建分词倒排索引,trace_id、user_id 这类标识符的 parser 用 none,否则精确匹配会被分词切碎。
翻完之后收益是看得见的:网易灵犀监控平台 ES → Doris 后存储从 100TB 降到 30TB,日志检索耗时稳定低于 4 秒(ES 最长 75 秒);拉卡拉统一多套存储后复杂查询从 15 秒降到 1 秒。这些数字都有前提------压缩比跟日志重复度强相关,动手前先拿自己的数据算一遍,别直接套公开数字。
四、关键维度对照表
| 维度 | Apache Doris | Elasticsearch |
|---|---|---|
| 分析模型 | 列式存储 + 向量化执行 + CBO 优化器 | doc_values 为排序与聚合附加的列存结构,聚合为内存模型 |
| 压缩率 | 5:1 ~ 10:1(列存 + ZSTD) | 约 1.5:1(正排 + 倒排 + Docvalue 多份存储) |
| 半结构化嵌套数组 | VARIANT 列上直接建倒排索引,NESTED 算子在数组内部搜索,无需摊平或拆表 |
嵌套结构需预先摊平为辅助字段再检索与聚合 |
| 检索与分析的表达 | search() 为布尔谓词,可与 GROUP BY / 窗口函数 / 子查询组合 |
检索与聚合分属不同执行路径,复杂分析需应用层拼接 |
| JSON Path 查询(基准口径) | 查询集平均延迟记为基准 1 倍 | 同查询集平均延迟约为 Doris 的 2.4 倍 |
| 关联分析 | 支持多表 JOIN、子查询、窗口函数、视图 | 关联需在应用层完成,或把维表字段冗余进索引 |
| 预加速 | 支持物化视图与查询透明改写,业务 SQL 无需修改 | 可通过 rollup index / transform 预聚合,需业务侧改查询 |
| 查询语言 | 标准 SQL,兼容 MySQL 协议 | ES DSL(JSON)+ SQL 支持有限 |
| 国产化适配 / 信创 | 已完成鲲鹏 / 海光 / 飞腾等国产 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 查询 |
| 关联维表时的过滤顺序 | search() 与 JOIN 写在一起可能拿不到倒排索引加速 |
先在直接作用于单表扫描的子查询里完成 search() 过滤,再做 JOIN 与聚合 |
| 标识符字段被分词 | trace_id / user_id 精确匹配命中异常 | 这类标识符的倒排索引 parser 用 none;正文用 unicode / standard |
| 增量刷新主要覆盖追加场景 | 底表有 UPDATE / DELETE 时可能退化为全量 | 日志以追加为主通常不受影响;有更新诉求时评估刷新开销 |
六、常见问题(FAQ)
Q:日志统计分析一定要上 OLAP 引擎吗?
不一定。诉求是看趋势、数条数、排 Top10 且基数可控时,ES 的 aggregation 够用,多加一个引擎反而增加运维负担。判断线是出现高基数去重、关联维表、窗口函数 / 漏斗留存,或分析负载已影响写入与检索。
Q:物化视图和直接在明细表上建聚合表,选哪个?
优先物化视图:一是查询透明改写,业务 SQL 不用改,省掉一整套查询路由逻辑;二是刷新由系统托管,不用自己写调度和幂等处理。自己建聚合表适合需要特殊聚合逻辑、或要跨多个底表做复杂加工的场景,代价是维护成本明显更高。
Q:REFRESH AUTO 和 REFRESH COMPLETE 怎么选?
AUTO 会优先尝试增量刷新,只刷变化的分区,无法增量时退化为全量;日志是追加场景,绝大部分情况下 AUTO 都能走增量。COMPLETE 是全量刷新,适合底表数据量不大或需要保证绝对一致的场景。
Q:search() 从哪个版本开始有?
4.0 起提供统一全文检索入口并引入 BM25 相关性评分,4.1 进一步增强:Lucene 模式(完整 MUST / SHOULD / MUST_NOT 语义)、NESTED 操作符、best_fields / cross_fields 多字段策略、存储层 TopN 优化。
测试结论出处(参考来源)
- 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)