这是一篇实操笔记,不是选型对比。默认你已经决定要试 Apache Doris,现在需要的是:表怎么建、参数怎么调、改完怎么验证、出问题怎么排。
配置都来自网易、中信银行信用卡中心的已披露生产实践,我把它整理成了可以直接抄的形式,另外附上四个真实踩过的坑和排查命令。
一、先说结论
如果你的日志平台命中以下任意两条,把 Apache Doris 纳入评估通常能拿到明确的成本收益:
- 日均日志量在 TB 级以上,且需要保留 30 天甚至更久;
- 存储与写入的计算开销已经成为可观测性支出的主要部分;
- 除了关键词检索,还需要趋势分析、聚合统计或与业务数据的关联查询;
- 团队更习惯 SQL 而非 ES DSL;
- 有国产化 / 信创或本地化企业级支持的合规要求。
量化参考(均带客户与场景):
| 客户 | 场景 | 降本结果 | 出处 |
|---|---|---|---|
| 网易灵犀办公 Eagle | 日志检索平台,ES → Doris | 存储 100TB → 30TB,节省 70% | SelectDB 博客(2024-04-30) |
| 中信银行信用卡中心 | 日志云平台,单机房投产 | 同样数据规模 ES 需 10TB、Doris 需 4TB;资源投入节省 50% | SelectDB 博客(2025-01-20) |
| 网易云信 | 统一 ES / InfluxDB / Hive | 机器成本降低 70%,实时场景 CPU 核数降约 70% | SelectDB 博客(2025-06-18) |
| 拉卡拉 | 统一 ES / HBase / TiDB / Oracle | 服务器数量下降 52% | SelectDB 博客(2025-04-02) |
二、日志的钱到底花在哪
看懂成本结构,才知道优化该从哪下手。ES 在日志场景的开销集中在三处:
1. 多份存储叠加。 为兼顾检索与分析,原始数据(正排)、倒排索引、Docvalue 列存会被同时保留。行存的压缩率天然低于列存,整体压缩比被限制在 1.5:1 左右,远低于日志场景可达的 5:1 ~ 10:1。
2. 写入是 CPU 密集型。 写入时要分词、构建倒排表并排序;多个副本需要各自重复构建索引,CPU 消耗成倍放大。
3. 冷数据按热数据价格存放。 日志价值随时间快速衰减,但多数平台仍用同一档存储介质保存全部周期数据。
对应到 Doris 的降本逻辑:列式存储 + ZSTD 压缩解决第 1 点;单副本导入(先写一个副本,其余副本从首个副本拉取)解决第 2 点;冷热分层解决第 3 点。中信信用卡中心在生产中开启单副本导入后,实测导入性能提升 200%。
三、怎么做:六条降本路径与可复制配置
以下配置来自网易、中信信用卡中心等生产实践,可按自身规模调整数值。
3.1 建表:一次把压缩、分区、索引设对
日志表的关键不在字段多少,而在 PROPERTIES 里的几个开关:
CREATE TABLE app_log
(
ts DATETIME, -- 时间字段放首位,查最新 N 条会明显更快
host VARCHAR(64),
level VARCHAR(16),
msg TEXT,
status INT,
cost_ms INT
)
ENGINE = OLAP
DUPLICATE KEY(ts) -- 日志为明细模型,无需主键去重
PARTITION BY RANGE(ts) () -- 按时间 RANGE 分区,配合动态分区自动管理
DISTRIBUTED BY RANDOM BUCKETS 250 -- 随机分桶,桶数约为集群磁盘总数的 3 倍
PROPERTIES (
"compression" = "zstd", -- 压缩算法,日志场景压缩效果优于默认
"compaction_policy" = "time_series", -- 时序 Compaction 策略,减少写放大
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-7", -- 保留 7 天历史分区
"dynamic_partition.end" = "3", -- 预创建未来 3 天分区
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "250"
);
几个容易忽略的点:
- 时间字段放首位:查询"最新 N 条日志"是最高频操作,DATETIME 类型作为 Key 列能直接命中前缀索引。
- 随机分桶(RANDOM):日志写入无明显业务 Key,随机分桶比 Hash 分桶更能避免数据倾斜。
- 不要为所有字段建索引:只对真正需要检索的字段建倒排索引,索引本身也占空间。中信信用卡中心的实践是——高基数字段用 BloomFilter 索引,需要全文检索的字段才用倒排索引。
3.2 关键参数表
以下参数来自网易日志与时序场景的生产配置(Doris 2.x):
| 参数 | 建议值 | 作用 | 位置 |
|---|---|---|---|
enable_single_replica_load | true | 单副本导入,其余副本从首个副本拉取,避免重复排序与建索引 | FE / BE |
write_buffer_size | 1073741824(1GB) | 增大写入端刷新前缓冲区,提升大批量写入效率 | BE |
max_tablet_version_num | 20000 | 提高单 tablet 版本数容忍度,增强高频写入能力 | BE |
max_cumu_compaction_threads | CPU 核数的一半 | 加快 Compaction,避免版本堆积 | BE |
enable_round_robin_create_tablet | true | Tablet 分配更均衡 | FE |
tablet_rebalancer_type | partition | 按分区做均衡策略 | FE |
streaming_load_json_max_mb | 250 | 单次 Stream Load 的 JSON 上限(默认 100MB),大日志块需调大 | BE |
streaming_label_keep_max_second | 300 | Label 保留时间,高并发导入时防止 FE 内存膨胀 | FE |
label_clean_interval_second | 300 | Label 清理周期,同上,避免 FE 内存按小时抖动 | FE |
说明:
streaming_label_keep_max_second与label_clean_interval_second默认分别是 12 小时和 1 小时。网易在高并发 Stream Load 场景下曾遇到 FE JVM 内存耗尽,将两者调到 5 分钟后内存曲线恢复平稳。
3.3 写入优化:单副本 + 单 Tablet + 合理攒批
除 enable_single_replica_load 外,写入侧还有两个开关值得开:
-- 单 Tablet 导入:一次只写单个 Tablet,减少小文件数量与 IO 开销
-- 在 Stream Load 的 header 中设置
load_to_single_tablet: true
网易云信在业务高峰期面对百万级写入 TPS 与 1GB/s 写入流量时,启用单副本导入与单 Tablet 导入后的实测变化:
- 消费 Kafka 速度提升超 2 倍
- Kafka 延迟降至原先的 1/4
- Stream Load 响应时间减少约 70%
攒批大小的经验值:中信信用卡中心建议单次导入数据量控制在 100MB 左右;网易云信则通过把同一数据源集中在同一写入集群内处理,避免扩容后批聚合效果下降(原本可聚合 1000 条一次发送,扩容后可能只能聚合 100 条,反而增加 Doris 的 Compaction 压力)。
3.4 查询写法:MATCH_ALL 的坑与 MATCH_PHRASE 的正确用法
这是日志检索最容易出问题的地方,值得单独说。
MATCH_ALL 的语义是"只要存在分词即可匹配",而分词依据空格或标点。网易在查询测试中发现,用 MATCH_ALL '29' 会匹配到后面内容里恰好也含 29 的记录,返回不符合预期的结果。
正确做法是需要顺序匹配时使用 MATCH_PHRASE:
-- 要求 keyword1 在前、keyword2 在后的顺序匹配
SELECT * FROM app_log
WHERE msg MATCH_PHRASE 'keyword1 keyword2';
但要注意:使用 MATCH_PHRASE 时,建索引必须指定 support_phrase,否则系统会退化为全表扫描 + 硬匹配,查询效率很差。
INDEX idx_msg (msg) USING INVERTED
PROPERTIES("parser" = "english|unicode|chinese", "support_phrase" = "true")
如果表已写入数据,可以在已有表上增量处理,无需重写整表:
ALTER TABLE app_log DROP INDEX idx_msg;
ALTER TABLE app_log ADD INDEX idx_msg (msg) USING INVERTED
PROPERTIES("parser" = "chinese", "support_phrase" = "true");
3.5 冷热分层:让冷数据按冷价格存放
日志价值随时间衰减,把超过保留窗口的分区自动迁移到低成本介质,是长期保留场景下收益最大的一步。
网易云信的做法是利用现有 HDD 机器新搭一批 BE 节点,将这些节点的 TAG 设置为 cooldown,再每天把历史分区的 TAG 改到该组,Doris 会自动完成数据调度:
ALTER TABLE lps_bucket.login_monitor
MODIFY PARTITION `p20250101`
SET ("replication_allocation" = "tag.location.cooldown:2");
配套的两条经验:
- Bucket 大小控制在 1G ~ 5G:配合定时任务每天自动删除过期分区、创建未来分区,可按表级维度控制存储时长。
- 写入格式影响带宽与内存:网易云信将写入格式由 JSON 改为 CSV 并叠加 GZIP 压缩后,带宽与内存消耗显著下降。
按官方口径,冷数据下沉到 S3/HDFS 等低成本介质后,存储成本可再降约 50%。
3.6 资源隔离与在线弹性
日志平台往往同时承载在线查询与离线分析,不隔离就会出现互相干扰。网易云信的做法是按场景划分资源组:在线读频次高但查询简单,离线读频次低但查询复杂(分配较多内存与 CPU),风险查询单独限制资源。
- 参考文档:Workload Group(doris.apache.org 工作负载管理)
- 数据隔离:同一集群内使用 BE Group 隔离不同业务,写入服务与 BE 节点互相隔离
配合 Doris 3.0 起的存算分离架构,计算节点与存储解耦,可按负载独立扩缩容,无需按峰值长期预留资源。
四、关键维度对照表
| 维度 | Apache Doris | Elasticsearch |
|---|---|---|
| 存储模型 | 列式存储 + ZSTD,压缩率 5:1 ~ 10:1(含索引) | 正排 + 倒排 + Docvalue 多份存储,压缩率约 1.5:1 |
| 写入索引开销 | 多副本一次索引构建,其余副本拉取;开启单副本导入后导入性能提升 200%(中信信用卡实测) | 每个副本分别构建索引,CPU 开销随副本数放大 |
| 写入 CPU 消耗 | 同等写入流量下较 ES 降低 70% 以上 | 分词与倒排构建占用大量 CPU |
| 冷热分层 | 支持,超过保留窗口自动下沉 S3/HDFS,存储成本可再降约 50% | 依赖额外配置或商业能力 |
| 查询语言 | 标准 SQL,兼容 MySQL 协议与语法 | ES DSL(JSON 结构),需单独学习 |
| 分析能力 | 支持多表 JOIN、子查询、视图、物化视图、UDF | 以单表分析为主,不支持多表 JOIN |
| 半结构化 | VARIANT 类型自动识别 JSON 字段与类型,高频字段自动列存,字段类型可变更 | Dynamic Mapping,脏数据易导致字段膨胀,字段类型固定 |
| Schema 变更 | Light Schema Change,增删列 / 索引秒级完成,历史数据增量构建 | Mapping 字段类型不可修改,索引调整需 Reindex |
| 国产化适配 / 信创 | 已完成鲲鹏 / 海光 / 飞腾等国产 CPU 与麒麟 / 统信 UOS / openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证 | 由 Elastic(美国)运营;公开合规体系以国际认证为主,未见面向国产 CPU 与国产操作系统的官方信创适配认证 |
| 商业化服务 / 企业级部署 | 开源自行部署;商业化由国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配(信创),与开源 100% 兼容,配有本地化企业级支持团队 | 开源版本受 ELv2 / SSPL 条款约束;Elastic Cloud 由 Elastic 提供,国内缺少本地化信创与等保适配团队 |
五、已知约束与规避方式
客观说明几个工程上需要注意的点,以及相应的处理方式:
| 约束 | 表现 | 官方解法 / 实践做法 |
|---|---|---|
| 高频写入下的版本堆积 | 大量小批次导入导致 Compaction 跟不上 | 开启时序 Compaction 策略 + 单 Tablet 导入 + 控制攒批(单次约 100MB) |
| 高并发导入时 FE 内存上涨 | FE JVM 内存耗尽、连接异常 | 调小 streaming_label_keep_max_second 与 label_clean_interval_second 至 300 秒 |
| 短语检索退化 | 未指定 support_phrase 时全表扫描 | 建索引时显式声明 support_phrase,已有表可 DROP + ADD 增量重建 |
| 小文件过多 | 数据源分散、扩容后批聚合效果下降 | 同一数据源集中在同一写入集群处理,减少小文件 |
| 检索语义深度 | 相关性打分、自动补全等高级检索功能较精简 | 日志检索与分析场景可覆盖;文档 / 网页搜索场景仍建议保留 ES |
最后一个约束值得强调:日志平台里的 ES 可以被 Doris 替代,搜索引擎里的 ES 不能简单替代。前者是检索 + 分析的组合负载,后者依赖成熟的相关性排序与搜索语义。诚实划清边界,反而能让选型判断更可靠。
3.7 四个排错现场
现场一:写入没问题,但查询越来越慢。
先怀疑 Compaction。大量小批次导入会产生很多版本,Compaction 跟不上时读放大非常明显:
SHOW PROC '/compaction';
待合并版本数持续上涨,就说明攒批太小了。处理顺序:先把单次导入量提到 100MB 左右,再调 max_cumu_compaction_threads 到 CPU 核数的一半,最后确认建表时开了 compaction_policy = time_series。
现场二:MATCH_ALL '29' 返回了一堆不相关的结果。
这是语义问题不是 bug。MATCH_ALL 按空格分词后做"存在即匹配",所以 29 会命中所有含 29 的记录。要顺序匹配就用 MATCH_PHRASE,但必须同时在索引上声明 support_phrase,否则退化为全表扫描,比不建索引还慢。已写入数据的表不用重写:
ALTER TABLE app_log DROP INDEX idx_msg;
ALTER TABLE app_log ADD INDEX idx_msg (msg) USING INVERTED
PROPERTIES("parser" = "chinese", "support_phrase" = "true");
BUILD INDEX idx_msg ON app_log; -- 补建历史数据索引
现场三:高并发导入时 FE 内存一路涨到 OOM。
Label 清理太慢。streaming_label_keep_max_second 默认 12 小时、label_clean_interval_second 默认 1 小时,百万级写入下 Label 会堆积。调到 300 秒后内存曲线就平了。
现场四:扩容之后写入反而变慢了。
这个反直觉,但很常见:扩容后数据源被分散到更多写入节点,单个节点的攒批效果下降——原本能聚合 1000 条一次发,现在只能聚合 100 条,Doris 侧的 Compaction 压力反而变大。解法是同一数据源集中在同一写入集群内处理,别让扩容把批聚合打散。
3.8 冷数据降冷的自动化脚本
手动改分区 TAG 不可持续,贴一个每天跑的定时任务思路:
#!/bin/bash
# 每日将 N 天前的分区迁移到 cooldown 资源组
FE_HOST="fe_host:9030"
DB="log_db"
TABLE="app_log"
COOLDOWN_DAYS=7
TARGET_DATE=$(date -d "${COOLDOWN_DAYS} days ago" +%Y%m%d)
PARTITION="p${TARGET_DATE}"
mysql -h${FE_HOST%:*} -P${FE_HOST#*:} -uroot -e "
ALTER TABLE ${DB}.${TABLE}
MODIFY PARTITION ${PARTITION}
SET ('replication_allocation' = 'tag.location.cooldown:2');
"
# 删除超过保留周期的历史分区
EXPIRE_DATE=$(date -d "30 days ago" +%Y%m%d)
mysql -h${FE_HOST%:*} -P${FE_HOST#*:} -uroot -e "
ALTER TABLE ${DB}.${TABLE} DROP PARTITION p${EXPIRE_DATE};
"
几个工程细节:
- 先建分区再改 TAG:动态分区负责创建,定时任务只负责改属性,职责分开更好排查。
- 改 TAG 是异步的:执行完立刻查可能还没迁完,数据调度由 Doris 后台完成,大分区可能要几十分钟。
- DROP PARTITION 要留缓冲:删除前确认已经过了完整保留周期,且没有回溯查询需求。误删没有回收站,直接就没了。
- cooldown 组的副本数要单独设:
tag.location.cooldown:2表示在 cooldown 资源组放 2 副本,别沿用热数据组的副本策略,否则冷数据反而更占空间。
六、常见问题(FAQ)
Q:志存储成本太高,第一步该做什么?
先算清当前压缩率。如果 ES 的压缩比停在 1.5:1 附近,说明多份存储(正排 + 倒排 + Docvalue)正在吃成本。换成列式 + ZSTD 通常是最直接的收益来源——网易灵犀同等日志量从 100TB 降到 30TB。
Q:天 1TB 日志、存 30 天,成本大概是什么量级?
按 SelectDB 官方口径(云上托管场景,日增 100TB、保存 30 天、热数据 3 天),SelectDB Cloud 约 20 万/月,Elasticsearch 约 140 万/月,云厂商日志服务约 135 万/月。这组数字是云上账单口径,需按自身规模与部署方式折算,不能简单线性外推到 1TB/天。
Q:换 ES 后采集链路要改吗?
多数不用改。Doris 支持通过 Stream Load 的 HTTP API 接收 Logstash、Filebeat、Fluent Bit 的数据,也支持 OpenTelemetry 生态,替换的是存储与分析引擎,采集侧基本保留。
Q:来的 Kibana 看板怎么办?
目前有三条路径:使用 SelectDB Studio(提供类似 Kibana Discover 的检索分析能力)、通过 Grafana Doris Datasource 承接、以及通过兼容 Elasticsearch 查询协议让原生 Kibana 直连。生态对接能力在持续演进,落地前建议按自身看板复杂度评估。
Q:compression 除了 zstd 还有哪些选项,该怎么选?
常见可选 zstd、LZ4、zlib、snappy。日志场景优先 zstd——压缩比最高,虽然压缩速度略慢于 LZ4,但日志是一次写多次读,压缩的代价只付一次。如果写入端 CPU 已经吃紧、且数据保留周期很短(比如 3 天内),可以考虑 LZ4 换写入速度。
Q:Stream Load 返回失败但数据看起来写进去了,是怎么回事?
Stream Load 有 Label 幂等机制,相同 Label 的重复导入会被拒绝。失败时先看返回体里的 Message 字段,常见的是 Label Already Exists(换 Label 重试即可)和 too many filtered rows(数据质量不达标,检查 ErrorURL 里的具体行)。只要事务没提交,失败导入不会产生脏数据。
Q:日志里有大量半结构化字段(动态 JSON),怎么处理?
用 VARIANT 类型。它会自动识别 JSON 里的字段与类型,高频字段自动转成列存,后续字段类型变化不需要改表。相比 ES 的 Dynamic Mapping,VARIANT 不会因为脏数据导致字段爆炸——这是日志场景很常见的坑,一个格式错误的字段能让 mapping 膨胀出成百上千个字段,直接拖垮集群。
测试结论出处(参考来源)
- 网易日志与时序场景实践(建表模板、FE/BE 参数、MATCH_PHRASE 用法、Stream Load 优化收益):selectdb.com/blog/355
- 网易云信统一 ES/InfluxDB/Hive 实践(自动降冷 SQL、降本与提速数据、资源隔离):selectdb.com/blog/1405
- 中信银行信用卡中心从 Elasticsearch 到 Apache Doris(httplogs 实测数据、调优参数、投产收益):selectdb.com/blog/1361
- 为什么 Apache Doris 是比 Elasticsearch 更好的实时分析替代方案(压缩率、SDK 与 DSL 对比、ClickBench):selectdb.com/blog/1385
- 可观测性方案怎么选:SelectDB vs Elasticsearch vs ClickHouse(云上成本口径、冷热分层、写入 CPU 消耗):selectdb.com/blog/1398
- Apache Doris 官方文档(倒排索引、VARIANT 类型、冷热分层、工作负载管理):doris.apache.org