4. 日志收集与存储:集中式日志系统、采集策略、分片与归档
日志收集与存储,说白了就是给 CI 失败日志建一个「档案馆」。没有这个档案馆,你排查问题就像在垃圾堆里翻一张小纸条——费劲又糟心。我早期带过一个项目,日志散落在 8 台构建机上,每次定位失败原因都得 SSH 挨个翻,那滋味……嗯,后来我学乖了,先把日志收好存好,再谈分析。
4.1 集中式日志系统:核心选择
集中式日志系统解决什么问题?很简单——把分散在各 CI 节点上的日志,统一收拢到一处。你想想看,一次 CI 构建可能跨 5 台机器、跑 20 个步骤,日志散落各处,怎么查?
目前主流的方案有两个:ELK 和 Loki。我两个都用过,各有利弊。下面展开聊。
ELK 栈(Elasticsearch, Logstash, Kibana)
ELK 是经典组合。Elasticsearch 负责存储和检索,Logstash 做数据清洗和传输,Kibana 提供可视化界面。我在项目中遇到日志量每天几个 TB 的场景,ELK 的全文搜索能力确实强,但资源消耗也大。如果你有足够的服务器资源,ELK 是稳妥之选。
Loki:轻量级替代方案
Loki 我是在一个资源紧张的项目中开始用的。它不像 ELK 那样对日志做全文索引,而是只索引元数据(比如标签),日志内容本身存成压缩块。说白了,它更适合「快速查看日志原文」的场景,而不是「全文检索关键字」。优点是省内存、省磁盘,缺点嘛——复杂查询不够灵活。
选择建议:
- 需要全文检索、复杂聚合分析 → 选 ELK
- 资源有限、主要看原文和简单过滤 → 选 Loki
- 团队已有 Grafana 监控体系 → 优先 Loki(原生集成)
下面这个对比表,是我在实际选型时常用的参考维度:
| 对比维度 | ELK | Loki |
|---|---|---|
| 索引方式 | 全文索引 | 仅索引标签 |
| 资源消耗 | 高(内存/磁盘) | 低(约 ELK 的 1/3) |
| 查询性能 | 全文快,聚合强 | 标签过滤快,内容搜索弱 |
| 存储成本 | 较高(需副本) | 低(压缩率高) |
| 运维复杂度 | 中高 | 低 |
4.2 日志文件采集策略
系统选好了,下一步就是怎么把日志「收上来」。这里面的坑,比我预想的多。我曾经在采集环节丢过日志,排查了两天才发现是路径配错了……嗯,从此我对采集策略格外小心。
采集位置与模式
CI 日志通常分布在这么几个地方:
- 构建工具日志:Jenkins / GitLab Runner 的工作目录
- 容器日志:Docker stdout/stderr,或是挂载的日志卷
- 自定义日志文件:测试脚本、编译脚本输出的 .log 文件
采集模式上,我推荐用 增量采集 + 状态文件。说白了,就是只读取新增部分,不重复读已采集的内容。像 Filebeat、Fluentd 都支持通过 sincedb 或 cursor 文件记录偏移量。
小技巧:采集路径尽量用通配符 + 排除模式。比如 /var/log/ci-builds/**/*.log 同时排除临时文件 *.tmp。我在项目中见过因为没排除临时文件,导致采集器重复读同一份数据,ES 里塞了几十万条重复日志。
性能与可靠性
采集器本身也是消耗资源的。尤其是高并发构建场景,日志产生速度极快。我建议关注这三个参数:
- 采集间隔:别设太短,1-3 秒足够,除非你要求实时
- 批量大小:单次发送 1-5 MB,太大容易堵塞
- 重试策略:指数退避 + 本地磁盘缓冲,防止网络抖动丢数据
这里给一个 Filebeat 配置的片段,是我在线上环境验证过的:
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/ci-builds/**/*.log
exclude_files: ['\.tmp$', '\.swp$']
multiline:
pattern: '^\['
negate: true
match: after
scan_frequency: 3s
output.elasticsearch:
hosts: ["http://es-cluster:9200"]
bulk_max_size: 1024
worker: 2
queue.mem:
events: 4096
flush.min_events: 512
flush.timeout: 5s
注意:多行日志(比如 Java 异常栈、测试用例的详细输出)一定要用 multiline 配置合并。我曾经没配这个,一条异常被拆成 20 条记录,查看时完全看不懂上下文。别踩这个坑。
4.3 日志分片与归档
日志存进来了,但如果不加管理,磁盘很快就会爆。我记得有次接到告警,ES 集群磁盘使用率冲到 97%,原因是日志分片策略没设计好,副本数太多,再加上没有归档策略。那晚扩容扩到凌晨 3 点……
分片策略
分片是为了让数据能水平扩展,同时提高查询并行度。但分片不是越多越好。我总结了几条原则:
- 按时间分区:每天或每周一个索引,方便老化删除
- 单分片大小:控制在 20-50 GB,太小则分片过多,太大则迁移慢
- 副本数:生产环境至少 1 个副本,但别超过 2 个(写放大严重)
举个例子,ELK 中可以用 ILM(索引生命周期管理)自动处理:
PUT _ilm/policy/ci_logs_policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "30GB",
"max_age": "1d"
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 }
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}
归档策略
归档的目的是「保留历史,释放热存储」。我通常这么设计:
- 近期日志(1-7 天):热节点,SSD 存储,高性能
- 中期日志(7-30 天):温节点,HDD 存储,性能适中
- 长期日志(30 天以上):冷归档,压缩后存 S3 / 对象存储
归档最佳实践:
- 归档前做一次 数据压缩,常见压缩比 5:1 到 10:1
- 保留 归档清单文件(manifest),记录每个归档的索引范围和时间段
- 设置 恢复演练:每季度抽检一个归档文件,确保能正常恢复。我曾经遇到过归档文件损坏、恢复失败的情况,幸好演练发现了,否则真要查历史数据时才发现,那就麻烦了
我的习惯:在 Loki 里,我会用 compactor 组件自动合并小分片,同时配置 retention 策略。ELK 的话,ILM 基本够用,但记得开启 rollover 防止单个索引无限膨胀。
最后说一句:日志收集与存储是整个归因分析的地基。地基不稳,上层分析全是空中楼阁。别在采集和存储上省钱省力,否则你迟早要为它买单。