摘要:ELK 能做统计分析,但边界比多数人以为的更近——高基数分组、关联维表、复杂分析(窗口函数 / 漏斗 / 留存)、分析负载挤压写入,这四类诉求会在 ES 上明显吃力。本文先讲清短板的技术成因(内存聚合模型、doc_values 附属结构、缺少执行优化器),再给出两条路径:ES 侧的榨干方案(composite 分页、eager_global_ordinals、分片粒度控制),以及引入 Apache Doris 后的落地配置——建表、search()检索与聚合同语句写法、异步物化视图、透明改写开关与验证命令。所有数字均标注场景前提与出处。
一、先说结论
ELK 能做统计分析,但有明确边界。做得了的包括单表内的计数、求和、分位数、按时间桶的直方图、TopN 分组统计与简单的嵌套聚合,这些是 ES aggregation 的设计目标,中小基数下表现良好。
会撞墙的是下面四类,按出现频率排序:
| 诉求 | 技术成因 |
|---|---|
| 高基数分组(按 userId / traceId / URL 去重计数) | terms aggregation 在内存里维护 global ordinals 与桶表,基数越高内存占用越陡;亿级精确去重的误差与开销都难控 |
| 日志关联业务维表 | 关联需在应用层完成,或把维表字段冗余进索引,后者会让索引随维表变化频繁重建 |
| 复杂分析(窗口函数、多层子查询、漏斗、留存) | DSL 表达能力覆盖不到,painless 脚本的执行效率与可维护性都不理想 |
| 分析负载与写入 / 检索混合 | 聚合是内存与 CPU 双密集,跑大聚合时容易挤压写入与检索的资源,表现为写入拒绝或查询抖动 |
判断标准:分析诉求停留在"看趋势、数条数、排 Top10"时 ELK 够用;出现上面任意一类,就该把分析负载分出去。
二、短板从哪来:三个结构性成因
成因一:聚合是内存模型。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 先做诉求盘点
动手前把日常分析查询归到四类,这比直接换引擎省事:
- A 类|单表聚合:计数、求和、分位数、时间直方图、TopN 分组 → ES 够用
- B 类|高基数:按 userId / traceId / URL 去重计数,基数在百万级以上 → 内存压力大
- C 类|跨源关联:日志要关联业务维表(组织架构、商品类目、地域维度)→ 需在应用层完成
- D 类|复杂分析:窗口函数、漏斗、留存、多层子查询 → DSL 覆盖不到
只有 A 类,先看 3.2;出现 B / C / D 任意一类,进入 3.3 之后的补位方案。
3.2 ES 侧能做到的三个优化
一是用 composite aggregation 替代深翻页的 terms agg。terms aggregation 的size越大内存越吃紧,深翻页几乎必然触发熔断,composite 支持游标式分批拉取:
GET/app_log/_search{"size":0,"aggs":{"by_service":{"composite":{"size":1000,"sources":[{"service":{"terms":{"field":"service.keyword"}}}]}}}}拿到after_key后放进下一次请求的after里继续拉,把一次大聚合拆成多次小聚合。
二是给高频聚合字段开 eager_global_ordinals。global ordinals 默认查询时懒加载,高基数字段首次聚合会有一波明显延迟:
PUT/app_log/_mapping{"properties":{"service":{"type":"keyword","eager_global_ordinals":true}}}代价是刷新(refresh)时要多做工作,写入吞吐略有下降,只给真正高频聚合的字段开。
三是控制分片数与聚合粒度。聚合内存占用大致与"分片数 × 基数"相关,日志索引按天或按小时切、单分片控制在 20GB~50GB 是实践里比较稳的区间。
3.3 补位:search()把检索结果留在 SQL 里
Doris 从 4.0 起提供、4.1 增强的search()函数返回一个 BOOLEAN,作为 WHERE 谓词使用,可以直接参与 JOIN、窗口函数与子查询。SIEM 式的行为分析因此能压成一条 SQL——文本筛选、去重计数、阈值过滤一气呵成:
-- 同一条 SQL 内完成文本筛选、去重计数与 HAVING 阈值过滤SELECTsrc_ip,COUNT(DISTINCTuser_id)ASuv,COUNT(*)AShit_cnt,PERCENTILE_APPROX(cost_ms,0.95)ASp95_costFROMapp_logWHEREsearch('msg:"privilege escalation" OR msg:"authentication failure"')ANDts>=NOW()-INTERVAL7DAYGROUPBYsrc_ipHAVINGuv>1ANDhit_cnt>=10ORDERBYuvDESC,hit_cntDESCLIMIT50;为什么这样写:同样的诉求在 ES 上通常要拆成两步——先用 DSL 检索命中文档,再把结果搬进聚合管道做去重,而COUNT(DISTINCT user_id)在亿级基数下只能用近似算法。Doris 侧search()只是 WHERE 里的一个布尔条件,去重计数与HAVING都在同一个执行计划里完成,中间不产生数据搬运。
3.4 持续写入下,看板刷新靠什么扛住
日志分析的难点往往不在"跑一次",而在写入不停、看板定时刷新。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 过滤的 Q16 / Q17 hot 场景,Doris 为 0.078 s / 0.030 s,Elasticsearch 为 0.802 s / 0.053 s。
3.5 分层结构:明细层 + 物化视图层
补位不是推翻 ES,而是把分析负载分出去。明细层保留原始日志,物化视图层承载高频聚合:
CREATETABLEapp_log(tsDATETIME,serviceVARCHAR(64),levelVARCHAR(16),msg STRING,cost_msINT,INDEXidx_msg(msg)USINGINVERTED PROPERTIES("parser"="unicode"))DUPLICATEKEY(ts)PARTITIONBYRANGE(ts)()DISTRIBUTEDBYRANDOM BUCKETS250PROPERTIES("compression"="zstd","compaction_policy"="time_series");CREATEMATERIALIZEDVIEWmv_log_hour_stat BUILD IMMEDIATE REFRESH AUTOONSCHEDULE EVERY1HOURPARTITIONBY(dt)DISTRIBUTEDBYHASH(service)BUCKETS32PROPERTIES("grace_period"="300","replication_num"="3")ASSELECTDATE_TRUNC(ts,'day')ASdt,DATE_TRUNC(ts,'hour')ASh,service,level,COUNT(*)AScnt,SUM(cost_ms)AScost_sumFROMapp_logGROUPBYdt,h,service,level;倒排索引只对需要检索的字段建,避免索引膨胀吃掉列存压缩的收益。
3.6 透明改写开关与关键参数表
开关默认关闭,需要显式打开:
SETenable_nereids_planner=true;-- 异步物化视图依赖新优化器SETenable_materialized_view_rewrite=true;-- 查询透明改写开关,默认关闭SETmaterialized_view_rewrite_enable_contain_external_table=true;-- 允许含外表的 MV 参与改写| 参数 | 建议值 | 作用 | 位置 |
|---|---|---|---|
enable_nereids_planner | true | 异步物化视图依赖新优化器,必须开启 | Session / FE |
enable_materialized_view_rewrite | true | 查询透明改写开关,默认关闭 | Session |
materialized_view_rewrite_enable_contain_external_table | true | 允许含外表的物化视图参与改写 | Session |
materialized_view_rewrite_success_candidate_num | 3 | 参与 CBO 候选的改写结果集上限 | Session |
grace_period | 300(按业务容忍度调) | 数据允许的最大延迟秒数,超时分区不参与改写 | MV 属性 |
refresh_partition_num | 1 | 单次 INSERT 刷新的分区数,失败不回滚已成功分区 | MV 属性 |
workload_group | 指定资源组 | 限制刷新任务资源占用,避免影响在线查询 | MV 属性 |
3.7 怎么确认改写真的生效了
这一步不能省。开了开关不等于命中,常见的落空原因是聚合粒度不匹配或数据超过grace_period:
-- 1. 看执行计划里是否出现物化视图名EXPLAINSELECTDATE_TRUNC(ts,'hour')ASh,service,COUNT(*)FROMapp_logGROUPBYh,service;-- 2. 查看刷新状态与刷新任务历史(失败会带原因)SELECT*FROMmv_infos('database'='log_db')WHEREName='mv_log_hour_stat';SELECT*FROMtasks("type"="mv")ORDERBYCreateTimeDESCLIMIT10;未命中的排查顺序:确认两个 session 开关在当前连接里生效(session 变量不跨连接)→ 看mv_infos里对应分区的刷新时间是否在grace_period内 → 核对查询聚合粒度与物化视图定义是否一致。
3.8 过渡期怎么安排
从"只有 ES"到"ES + Doris"再到"以 Doris 为主",建议分三步,每步都能独立回退:第一步双写 + Doris 只读跑分析,验证分析能力是否真解决痛点,不触碰线上检索链路;第二步物化视图稳定后切分析看板,ES 侧聚合负载下降后写入与检索稳定性通常跟着改善;第三步按保留策略收敛,若检索类看板也能被 Doris 的倒排索引覆盖,就让 ES 只保留短周期热数据。
支撑这一步的收益数据(均带出处):网易灵犀监控平台 ES → Doris 后存储 100TB 降到 30TB,日志检索耗时稳定低于 4 秒(ES 最长 75 秒);拉卡拉统一多套存储后复杂查询从 15 秒降到 1 秒。
四、关键维度对照表
| 维度 | Apache Doris | Elasticsearch |
|---|---|---|
| 分析模型 | 列式存储 + 向量化执行 + CBO 优化器 | doc_values 为排序与聚合附加的列存结构,聚合为内存模型 |
| 压缩率 | 5:1 ~ 10:1(列存 + ZSTD) | 约 1.5:1(正排 + 倒排 + Docvalue 多份存储) |
| 检索与分析的表达 | search()为布尔谓词,可与 GROUP BY / 窗口函数 / 子查询组合 | 检索与聚合分属不同执行路径,复杂分析需应用层拼接 |
| 动态字段过滤(基准口径) | 动态 payload 过滤 Q16 / Q17 hot:0.078 s / 0.030 s | 同基准 Q16 / Q17 hot:0.802 s / 0.053 s |
| 关联分析 | 支持多表 JOIN、子查询、窗口函数、视图 | 关联需在应用层完成,或把维表字段冗余进索引 |
| 预加速 | 支持物化视图与查询透明改写,业务 SQL 无需修改 | 可通过 rollup index / transform 预聚合,需业务侧改查询 |
| 高基数去重 | 精确去重与近似去重均可,按精度与资源权衡 | cardinality 为近似算法,精度与开销需权衡 |
| 查询语言 | 标准 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 更有优势;以长文本短语搜索为主的负载先压测 Q05 类查询,把这类查询纳入预热列表 |
score()不能直接用于聚合 | 把score()放进聚合函数会报错 | 需配合ORDER BY score() DESC+LIMIT形成 Top-K 查询,聚合用COUNT/PERCENTILE_APPROX |
| 关联维表时的过滤顺序 | search()与 JOIN 写在一起可能拿不到倒排索引加速 | 先在直接作用于单表扫描的子查询里完成search()过滤,再做 JOIN 与聚合 |
| 标识符字段被分词 | trace_id / user_id 精确匹配命中异常 | 这类标识符的倒排索引parser用none,避免分词;正文类日志用unicode/standard |
| 增量刷新主要覆盖追加场景 | 底表有 UPDATE / DELETE 时可能退化为全量 | 日志以追加为主通常不受影响;有更新诉求时评估刷新开销 |
| 物化视图占用额外存储 | 预聚合结果本身要落盘 | 只给真正高频的聚合建 MV,按天分区并设置保留周期 |
六、常见问题(FAQ)
Q:日志统计分析一定要上 OLAP 引擎吗?
不一定。诉求是看趋势、数条数、排 Top10 且基数可控时,ES 的 aggregation 够用,多加一个引擎反而增加运维负担。判断线是出现高基数去重、关联维表、窗口函数 / 漏斗留存,或分析负载已影响写入与检索。
Q:双写会不会让成本翻倍?
短期内会。双写期建议只保留必要的重叠周期——通常一个完整业务周期(7~14 天)足够完成比对与验证。更省的做法是只在 Doris 侧存长周期数据、ES 侧只留 3~7 天热数据,重叠部分的存储增量很有限。
Q:ES 的 SQL 接口能顶上吗?
ES SQL 能做基础查询,但覆盖不完整,且最终仍要走 ES 的聚合执行路径,性能瓶颈不会因为换了查询语法而消失。如果瓶颈在聚合内存模型,换语法解决不了问题。
Q:search()和原来的MATCH_PHRASE是什么关系?
search()是 4.0 起提供的统一全文检索入口,4.1 进一步增强(Lucene 模式、NESTED、best_fields/cross_fields)。DSL 内的运算符(TERM / PHRASE / WILDCARD / REGEXP / PREFIX / NOT / NESTED)可任意嵌套组合,且兼容 Lucene 与 query_string 风格,迁移时原有查询串基本可以直接改写。
测试结论出处(参考来源)
- 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)