2935 字
约 9 分钟
22
S13-A10 · 日志与搜索的边界:什么时候该用 ES 存日志

搜索集群写入延迟从 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. 自测

  1. 日志索引与业务索引在 refresh 频率上的诉求为什么相反?
  2. 关掉 _source 会失去哪三种能力?
  3. dynamic: strict 在日志场景的价值与代价分别是什么?
  4. 为什么说"按天索引"让删除从 O(n) 变成 O(1)?
  5. 日志突增时,为什么应该丢日志而不是让应用阻塞?
  6. 什么信号出现就说明日志写入已经在丢数据?
S13-A10 · 日志与搜索的边界:什么时候该用 ES 存日志
https://www.clxhxhhr.top/posts/6191/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。