搜索集群写入延迟从 20 ms 涨到 200 ms,业务方说"搜索变卡",一看:同一个集群里躺着每天几亿条访问日志。日志用 ES 存不是错,错在共用集群、共用写入预算,还按业务索引的方式建。
判断标准只有三个问题:查询需要全文检索到什么程度、保留期多长、写入量相对业务写入是几倍。答案决定成本结构,成本结构决定架构。ELK 与 Loki 的机制差异另有一篇专讲,这里只算账。
1. 日志检索与业务搜索的五个本质差异
| 维度 | 业务搜索 | 日志检索 |
|---|---|---|
| 写入模式 | 低频单条,需立即可见 | 高频批量,容忍秒级 |
| 保留期 | 长期,数据即资产 | 短,按天/周滚动删 |
| schema | 稳定,变更走评审 | 易爆字段,随版本漂移 |
| 查询形态 | 相关性排序、聚合、分页 | 时间窗 + 精确过滤 + 少量词 |
| 一致性 | 写完要能搜到 | 丢几条可接受,宁丢不堵 |
最致命的是第一与第三条:业务要 refresh 快,日志要吞吐;业务字段可控,日志字段一次上线就多几十个。
2. 替代方案与它们的成本面
| 方案 | 索引粒度 | 存储成本面 | 查询能力面 | 运维面 |
|---|---|---|---|---|
| ES 全文索引 | 正文分词全索引 | 膨胀与原文同量级或更高 | 最强,任意词组合 | 分片、ILM、调优都要人 |
| Loki 类标签 | 只索引标签 | 压缩块 + 对象存储,明显更低 | 标签筛完块内扫,聚合弱 | 组件少,标签基数要守 |
| 对象存储 + 引擎 | 文件级(列式/压缩) | 最低,按对象存储计价 | 依赖引擎,适合批量分析 | 延迟高,不适合实时排障 |
ClickHouse |
列存 + 跳数索引 | 中低,压缩率高 | 聚合极强、全文弱 | 需建表与合并管理 |
选型不是比强弱,而是你的查询 90% 落在哪类:排障看单条链路 → 标签 + 时间足够;统计分析 → 列存更划算;任意关键词 → 只有全文索引。
3. ES 存日志的真实开销在哪
PUT /logs-app-2026.11.09
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s",
"index.codec": "best_compression"
}
}
| 成本项 | 机制 | 量级判断 |
|---|---|---|
| 索引膨胀 | 倒排 + 正排 + _source 叠加 |
常为原文数倍,按字段数与分词核对 |
| 副本 | 默认 1 副本即双倍存储 | 日志常降副本,靠可重建兜底 |
refresh 频率 |
每次 refresh 生成新段 | 放宽 refresh_interval 是标准动作 |
| 段合并 | 持续写入产生小段 | 合并吃 IO 与 CPU,写越多越明显 |
| 字段爆炸 | 动态映射逐字段建 mapping | 一次上线多几十字段,mapping 与内存齐涨 |
| 分片数 | 分片即 Lucene 索引 | 过多则开销叠加、堆压力上升 |
第 5 项最常翻车:动态映射开着,日志里带个字段名含 ID 或时间戳的 JSON,一天就能把 mapping 撑到几千字段。
4. _source 与 doc_values:日志场景该关哪个
| 设置 | 关掉省什么 | 关掉失去什么 | 日志场景取舍 |
|---|---|---|---|
_source: false |
原始文档存储(常占大头) | 无法 reindex、取回原文、update |
仅当可从对象存储回溯时关 |
doc_values: false |
正排(排序/聚合) | 不能聚合与排序 | 展示型字段可关 |
index: false |
倒排 | 该字段不能全文检索 | 数值型、ID 型字段可关 |
norms: false / index_options: freqs |
评分与位置信息 | 短语检索与评分 | 日志不排序,安全性高 |
关键判断是是否需要回捞原文。_source 一关,日志只剩可检索字段,想看完整上下文只能去对象存储翻,事故现场这个代价很贵。折中做法:热索引保留 _source,冷索引转移后再优化。
"mappings": {
"dynamic": "strict", // 字段爆炸的第一道闸
"properties": {
"message": { "type": "text", "index_options": "freqs", "norms": false },
"traceId": { "type": "keyword" },
"level": { "type": "keyword" },
"@timestamp": { "type": "date" }
}
}
dynamic: strict 让未知字段写入直接失败——日志场景是故意设计:宁可丢字段也不让 mapping 失控,代价是新字段要先改模板。
5. 按天索引与 ILM 不是可选优化
单一日志索引无限增长,分片只增不减,最终只能重建。按时间切索引让删除变成 O(1) 的删索引。
PUT _ilm/policy/logs-30d
{
"policy": {
"phases": {
"hot": { "actions": { "rollover": { "max_primary_shard_size": "30gb", "max_age": "1d" } } },
"warm": { "min_age": "2d", "actions": { "forcemerge": { "max_num_segments": 1 }, "shrink": { "number_of_shards": 1 } } },
"delete": { "min_age": "30d", "actions": { "delete": {} } }
}
}
}
| 阶段 | 动作 | 收益 | 风险 |
|---|---|---|---|
| hot | rollover(按大小/时长) | 单索引可控 | 阈值太小导致索引爆炸 |
| warm | forcemerge、shrink | 段数降、查询更快、分片变少 | 合并抢 IO |
| delete | 整索引删除 | 回收即时、无删除放大 | 保留期须与合规对齐 |
rollover 用主分片大小当判据比文档数稳。索引数也要监控:几百个会让集群状态更新变慢。
6. 写入侧的限流与背压:先保护谁
日志突增(DEBUG 被打满、重试风暴)会打满写入队列,业务写入一起被拒,
# Filebeat/Logstash 侧:批量与重试必须有上限,否则日志客户端变成压测工具
output.elasticsearch:
bulk_max_size: 2000
worker: 2
backoff.init: 1s
backoff.max: 30s
max_retries: 5 # 超过就丢,绝不无限重试
| 保护点 | 做法 | 保护对象 |
|---|---|---|
| 客户端批量 | bulk_max_size + 有界队列 |
写入队列 |
| 客户端重试 | 有限重试 + 退避 | 避免重试风暴 |
| 服务端限流 | 按客户端/索引限速 | 集群整体 |
| 索引隔离 | 日志索引独立节点 | 业务写入预算 |
| 丢弃策略 | 明确丢什么级别 | 保证 ERROR 先落盘 |
第一原则:日志是可选负载。队列满时该丢日志,不该让业务变慢;让日志客户端阻塞应用是最常见的自伤。
7. 该不该共集群:五个判据
| 判据 | 倾向共集群 | 倾向分集群 |
|---|---|---|
| 日增量相对业务写入 | 日志量小于业务写入一个量级 | 日志量高于业务写入 |
| 运维能力 | 只有一套集群的人力 | 能维护两套并做规划 |
| 故障隔离 | 日志不可用可接受 | 日志写入不得影响搜索 |
最常见的是中间态:同集群不同节点角色,日志索引放专用数据节点,用 allocation 隔开,既省运维又分开 IO 与堆压力。
PUT /logs-app-*/_settings
{ "index.routing.allocation.require.data": "logs" }
8. 查询侧的成本
| 查询形态 | 成本来源 | 缓解 |
|---|---|---|
| 大时间窗 + 通配符 | 逐段扫描,跳过索引 | 缩时间窗、改 keyword |
| 高基数聚合 | 需全局去重结构 | 近似聚合、控 precision_threshold |
| 深翻页 | 每页都收集排序结果 | 改 search_after |
日志查询几乎都是"时间窗 + 精确过滤",正是 ES 擅长的;一旦有人拿它跑"按小时统计 UV",成本跳一个量级,这类需求该去列存。
9. 分级:不是所有日志都值得索引
<!-- 分级落盘:ERROR 全量,INFO 采样,DEBUG 只在排障窗口打开 -->
<appender name="ES" class="...ElasticsearchAppender">
<filter class="ch.qos.logback.core.filter.ThresholdFilter">
<level>WARN</level>
</filter>
</appender>
| 级别 | 索引策略 | 理由 |
|---|---|---|
| ERROR / FATAL | 全量 + 保留最久 | 事故证据,量小 |
| WARN | 全量或高采样 | 趋势判断需分布 |
| INFO | 采样或只留关键字段 | 量最大,全量索引最贵 |
| DEBUG / TRACE | 默认不落,按需临时开 | 打开即压测集群 |
采样必须在客户端做:进了集群,成本就已经发生。
10. 保留期与合规
| 日志类型 | 常见保留期 | 依据 |
|---|---|---|
| 应用错误日志 | 周到月级 | 排障窗口 + 复盘 |
| 访问日志 | 月级 | 流量与容量分析 |
| 审计/安全日志 | 半年以上 | 合规要求,按行业核对 |
| 调试日志 | 小时到天级 | 无长期价值 |
保留期不是运维偏好,是成本与合规的交集。delete 阶段要与合规口径对齐,写错两边都难补救。
11. 可观测
# 关键的四个信号,缺一个就无法归因
elasticsearch_indices_store_size_bytes{index="logs-app-*"} # 增长斜率
elasticsearch_indexing_bulk_rejected_total # 写入拒绝
elasticsearch_cluster_thread_pool_write_queue # 写队列深度
elasticsearch_index_refresh_time_seconds_total{index="logs-app-*"}
判据:斜率突然变陡,先查字段数或日志级别被改过;bulk_rejected 一出现就说明已在丢数据,此时该降采样而不是加节点。
12. 排错表
| 现象 | 最可能原因 | 核对 |
|---|---|---|
| 业务搜索写入延迟上涨 | 日志写入抢占 refresh/merge 资源 | 分索引看写入耗时与段数 |
| mapping 字段数暴涨 | 动态映射 + 结构化 JSON 字段 | GET /index/_mapping 字段计数 |
| 单索引几十个分片 | 未 rollover 或阈值过小 | 索引数、主分片数 |
| 查询超时且 CPU 高 | 大时间窗通配 + 高基数聚合 | 慢日志 slowlog 与 profile |
13. 边界
| 场景 | 判断 | 理由 |
|---|---|---|
| 日增量很小、查询极少 | 不必自建 ES 日志 | 集中日志平台通常更便宜 |
| 已有公司级日志平台 | 接入即可 | 重复建设只多一份运维 |
| 只要"按 traceId 查一条链路" | 标签索引足够 | 不需要全文索引 |
| 长期留存且极少查询 | 对象存储 + 按需加载 | 索引成本远高于扫描 |
| 需复杂聚合做分析 | 列存而非日志索引 | 聚合是列存强项 |
| 共集群且无隔离 | 先做节点隔离 | 不隔离即共享故障域 |
门槛不是能不能存,而是愿不愿意为全文检索持续付三份钱:索引膨胀、分片运维、写入预算。付不起就换标签索引。
14. 自测
- 日志索引与业务索引在
refresh频率上的诉求为什么相反? - 关掉
_source会失去哪三种能力? dynamic: strict在日志场景的价值与代价分别是什么?- 为什么说"按天索引"让删除从 O(n) 变成 O(1)?
- 日志突增时,为什么应该丢日志而不是让应用阻塞?
- 什么信号出现就说明日志写入已经在丢数据?