分布式日志收集:ELK vs Loki,海量日志场景下的降采样与存储优化
问题
微服务架构下日志量呈指数级增长,ELK 和 Loki 的核心区别是什么?海量日志场景下如何做降采样和存储优化?
先厘清场景:你需要什么样的日志系统
在讨论选型之前,先想清楚一个问题:你查日志是为了什么?
- 排查问题:知道服务名、traceId,搜到对应日志就行
- 聚合分析:统计某个接口在 5 分钟内的错误率趋势
- 安全审计:需要全文搜索,精确匹配关键词
这三类场景对日志系统的要求完全不同。排查问题只需要快速定位,聚合分析需要索引和聚合能力,安全审计需要全文搜索和保留能力。没有万能的日志系统,只有适合你查询模式的日志系统。
ELK:经典方案,全文搜索能力强
架构与数据流
ELK 的典型数据流如下:
应用日志 → Filebeat(采集) → Logstash(解析/过滤) → Elasticsearch(索引/存储) → Kibana(可视化)
↑
Kafka(可选缓冲层)Logstash 从各节点采集日志,经过解析和格式化后写入 Elasticsearch,Elasticsearch 对日志内容建立倒排索引,Kibana 提供可视化界面。在日均 10TB+ 日志量的场景下,通常会在 Logstash 前面加一层 Kafka 做缓冲——防止 Logstash 或 ES 挂掉时日志丢失,同时允许 Consumer 重启后重放。
核心优势:全文搜索能力极强,支持复杂的聚合查询(如 Terms Aggregation、Date Histogram)。你可以用类似 Google 搜索的方式搜任意关键词,毫秒级返回结果。
核心代价:存储成本高。Elasticsearch 的索引和副本会占用 2-3 倍原始数据量。更重要的是,即使你只查最近 1 小时的日志,ES 也保持全量索引,冷数据不会自动降级。
写入瓶颈:ES 的写入性能受限于 refresh 间隔(默认 1 秒)和 segment merge 的开销。单节点写入吞吐通常在 1-2 万条/秒,超过后需要分片和扩容。但分片多了,查询又会变慢——这是 ES 的经典 trade-off。
生产踩坑:segment merge 引起的写入抖动
我手上一个 ES 集群(7.10 版本,5 节点,每个节点 32C/64GB),日均写入 8TB 日志,白天高峰期突然出现周期性的写入拒绝(429 Too Many Requests)。排查发现是 segment merge 在高峰期触发,占满 IO 导致写入队列积压。解决方案:
- 将
index.merge.scheduler.max_thread_count从默认的Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2))手工调为 1,限制 merge 并发。 - 设置
index.merge.policy.segments_per_tier从 10 改为 20,减少 merge 触发频率(代价是查询时多扫描 segment,性能下降约 10-15%)。 - 把写入高峰期(10:00-12:00、14:00-16:00)的
index.refresh_interval从 1s 改为 30s,降低 refresh 频率。
这三板斧下去,写入拒绝率从 3% 降到 0.1% 以下。
Loki:轻量方案,低成本存储
架构与数据流
Loki 的典型数据流如下:
应用日志 → Promtail(采集/打标签) → Loki Ingester(内存中攒批) → 压缩后写入对象存储(S3/MinIO)
↑
索引(BoltDB/TSDB)Loki 的设计思路完全不同:不索引日志内容,只索引元数据(标签)。日志内容以压缩块形式存储在对象存储(如 S3/MinIO)中。
Ingester 攒批细节:日志到达 Ingester 后不会立即写入存储,而是在内存中按 stream(一组相同的标签组合)攒批,攒够 256KB 或 1 分钟(chunk_target_size=256KB、max_chunk_age=1m)后压缩成一个 chunk 写入对象存储。这套机制让 Loki 的写入吞吐可以线性扩展——来多少 Ingester 接多少流量。
查询流程:先在标签索引中按标签(如 service=user-service、env=prod)过滤,得到匹配的日志文件列表,然后扫描这些文件的内容,返回符合关键词的记录。
存储优势:压缩率 10-20 倍,因为不建全文索引,磁盘占用只有 ELK 的 1/5 到 1/10。日志压缩块存放在对象存储上,存储成本几乎可以忽略。
查询劣势:不支持全文搜索。复杂的文本查询(如多关键词 AND/OR 组合)需要扫描大量日志文件,速度远慢于 ELK。如果你的查询模式是"搜所有包含 ERROR 的日志并按时间排序",Loki 的响应时间可能在秒级甚至分钟级。
生产踩坑:Loki 的日志重复问题
Promtail 的 positions.yaml 文件记录了每个日志文件已读取的偏移量。如果 Promtail 异常重启,该文件可能未持久化,导致重启后重复读取并发送日志,造成 Loki 中同一日志出现多份副本。
解决方案:将 positions.yaml 挂载到持久卷(K8s 下用 emptyDir 就是默认踩坑),并设置 sync_period: 10s 让 Promtail 更频繁地同步偏移量。
降采样策略:按时间价值梯度配置
日志的价值随时间递减。今天的日志是排查工具,30 天前的日志是合规证据。按不同价值配置不同策略:
热数据(近 7 天):全量保留,不采样。所有级别都保留,包括 DEBUG 日志。这是排查问题的黄金窗口。
温数据(7-30 天):按 1/10 采样。只保留 WARN 和 ERROR 级别,INFO 和 DEBUG 日志丢弃。如果需要,可以保留聚合统计(如每分钟的请求量、错误数、P99 延迟),但不保留原始日志。
冷数据(30 天+):只保留 ERROR 级别日志,其余全部丢弃。或者更进一步,只保留 ERROR 日志的聚合统计(如每天的错误分类和数量),不保留原始日志内容。
代码示例:基于日志级别的降采样过滤器
下面是一个 Java 实现的日志采样过滤器,根据日志级别和时间窗口决定是否保留该条日志:
@Component
public class LogSamplingFilter {
private static final Map<Level, Integer> LEVEL_WEIGHT = Map.of(
Level.FATAL, 5,
Level.ERROR, 4,
Level.WARN, 3,
Level.INFO, 2,
Level.DEBUG, 1
);
private final Map<String, Double> samplingRate = new ConcurrentHashMap<>();
/**
* 判断是否保留该条日志
* @param level 日志级别
* @param serviceName 服务名
* @param logTimestamp 日志时间戳(毫秒)
* @return true 保留,false 丢弃
*/
public boolean shouldSample(Level level, String serviceName, long logTimestamp) {
int daysOld = (int) ((System.currentTimeMillis() - logTimestamp) / 86400_000L);
// 热数据:全量保留
if (daysOld <= 7) {
return true;
}
// 温数据:按级别降采样
if (daysOld <= 30) {
return switch (level) {
case FATAL, ERROR -> true;
case WARN -> randomKeep(0.3);
default -> false;
};
}
// 冷数据:只保留 ERROR
return level == Level.FATAL || level == Level.ERROR;
}
/**
* 动态降级:QPS 超过阈值时,自动降低 INFO 日志的采样率
*/
public void adjustOnHighLoad(String serviceName, long currentQps) {
if (currentQps > 10000) {
samplingRate.put(serviceName + ":INFO", 0.01);
} else if (currentQps > 5000) {
samplingRate.put(serviceName + ":INFO", 0.1);
} else {
samplingRate.put(serviceName + ":INFO", 1.0);
}
}
private boolean randomKeep(double rate) {
return ThreadLocalRandom.current().nextDouble() < rate;
}
}这个过滤器的核心思想是:日志采样不是一刀切,而是按时间维度 + 级别维度 + 负载维度做三维决策。热数据不采样,温数据按级别降采样,高负载时自动降级 INFO 日志。这样在保证排查效率的前提下,将存储成本降到最低。
实际生产中的混合方案
大多数公司最终会走混合路线:
- ELK 存 ERROR 级别日志:用于排查和聚合分析,量小所以成本可控。一个 500 节点微服务集群,日均 ERROR 日志大约 500 万条,Elasticsearch 7 节点集群(每节点 1TB SSD)可保留 30 天。
- Loki 存全量日志:用于链路追踪,量大但成本低。同一集群日均全量日志约 200 亿条,Loki 用 3 个 Ingester + MinIO 对象存储,存储成本约为 ELK 方案的 1/8。
查询时,先通过 Loki 的标签过滤找到 traceId,再跳转到 ELK 查该 traceId 下的 ERROR 日志详情。两者互补,不是二选一。
深度对比表
| 维度 | ELK | Loki |
|---|---|---|
| 存储成本 | 2-3 倍原始数据(含副本+索引) | 1/10 ~ 1/20 原始数据压缩 |
| 写入吞吐(单节点) | 1-2 万条/秒,受 refresh/merge 限制 | 5-10 万条/秒,线性扩展 |
| 全文搜索延迟 | 毫秒级(倒排索引) | 秒级~分钟级(扫描压缩块) |
| 聚合查询 | 强大(Terms、Date Histogram、Percentile) | 弱(需配合 Prometheus 或 LogQL 简单聚合) |
| 索引粒度 | 全文索引,每条日志都建索引 | 仅标签索引,内容不索引 |
| 扩容复杂度 | 高(分片数、rebalance、shard 分配) | 低(Ingester 无状态,加节点即可) |
| 运维成本 | 高(需要专职 ES 运维) | 低(无状态 + 对象存储) |
| 对象存储支持 | 不支持原生;需搭配 ILM 冷热分离 | 原生支持(S3/MinIO/GCS) |
| 日志保留策略 | ILM(Index Lifecycle Management) | 按 retention 自动删除 chunks |
| 是否适合链路追踪 | 不适合(全量存成本太高) | 适合(按标签过滤发现 traceId) |
| 典型部署规模 | 7-15 节点,高配机器 | 3-5 节点 + 对象存储 |
日志水位控制与成本优化
不要忽视日志输出端的控制。每个服务应该限流日志输出速率,防止日志打印成为性能瓶颈。
log4j2 的 BurstFilter 配置示例:
<!-- 每秒最多输出 1000 条,超出后丢弃直到下个窗口 -->
<BurstFilter level="INFO" rate="1000" maxBurst="2000"/>日志压缩归档:30 天前的日志打包压缩(gzip 压缩率 10:1),存到对象存储,查询时解压回传,可以节省 90% 的存储成本。Elasticsearch 的 ILM 策略可以自动迁移到 warm 节点(HDD 存储)然后 cold 阶段冻结索引,最后 delete。
Kubernetes 下的日志采集注意事项:
- 容器日志默认写到 stdout/stderr,由 Docker 的 json-file driver 管理,默认 10MB 轮转,容器内保留最后一个文件。如果 logrotate 配置不当,高日志量服务的 Pod 可能因为磁盘写满被驱逐。
- 推荐用 sidecar 模式或 DaemonSet 的 Promtail 采集,日志直接写到对象存储,不在节点上保留。
- 设置
--log-opt max-size=10m --log-opt max-file=3限制容器日志大小。
总结
| 维度 | ELK | Loki |
|---|---|---|
| 存储成本 | 高(2-3 倍原始数据) | 低(1/10 ~ 1/20 压缩) |
| 全文搜索 | 强,毫秒级 | 弱,秒级甚至更慢 |
| 聚合分析 | 强,支持复杂聚合 | 弱,需配合 Prometheus |
| 查询模式 | 关键词搜索、聚合 | 标签过滤 + 内容扫描 |
| 适用场景 | 排查 + 分析 | 排查为主 |
关键建议:日志选型取决于你的查询模式。如果频繁搜索关键词做聚合分析,选 ELK;如果主要把日志当排查工具(知道 traceId 或服务名就能找到日志),选 Loki,成本优势巨大。不要追求单一日志系统覆盖所有场景,混合方案才是最务实的选择。
确保日志系统在生产环境稳定运行,需要关注三个维度:写入端(Logstash/Promtail 的可靠性)、存储端(ES 的 segment merge 抖动、Loki 的 chunk 压缩配置)、查询端(Kibana 面板的查询超时设置、Grafana 的 Loki 数据源查询并发控制)。三个维度都压住了,日志系统才算在生产环境站住脚。
参考:DDIA 第 8 章(Distributed System Troubles)对日志系统的可靠性设计有深入讨论;Grafana Loki 官方文档的架构设计部分值得一读。Elasticsearch 官方博客的 Elasticsearch Indexing Performance 系列对写优化有详细配置说明。