3. 日志采集与结构化:从杂乱无章的日志到结构化数据
说实话,我见过太多线上事故了。系统挂了,第一反应就是查日志。结果呢?一堆文本文件散落在各个服务器上,grep 半天啥也找不到。那种感觉,就像在垃圾堆里找一枚针。
所以,日志采集和结构化,是数据质量监控的第一步。没有这一步,后面所有的告警、分析、可视化都是空中楼阁。今天我就聊聊,怎么用 ELK/EFK 栈把这堆「垃圾」变成「金矿」。
3.1 为什么日志必须结构化?
先看一个反面教材。这是我以前接手的一个老系统,日志长这样:
2024-01-15 10:23:45 ERROR user login failed
2024-01-15 10:23:46 WARN timeout detected
2024-01-15 10:23:47 INFO payment success for order 12345
你想想看,这种日志能干嘛?想统计「今天有多少次登录失败」,你得写正则去匹配。想查「某个订单的完整调用链」,你得肉眼去翻。说白了,这就是非结构化数据,机器读不懂,人也读得累。
我习惯把日志结构化成 JSON 格式:
{
"@timestamp": "2024-01-15T10:23:45.123Z",
"level": "ERROR",
"logger": "com.api.auth.LoginService",
"message": "user login failed",
"user_id": "u_10086",
"error_code": "AUTH_001",
"duration_ms": 230,
"trace_id": "abc123def456"
}
结构化之后,Elasticsearch 可以直接索引每个字段。你想查什么,一个 Query 就搞定。我在项目中遇到过,把日志从文本切到 JSON 后,排查问题的速度至少快了 3 倍。
核心原则:日志结构化不是「锦上添花」,而是「雪中送炭」。没有结构化的日志,数据质量监控就是空谈。
3.2 ELK vs EFK:怎么选?
ELK 是 Elasticsearch + Logstash + Kibana。EFK 是把 Logstash 换成了 Fluentd 或 Fluent Bit。我两个都用过,说说我的感受。
| 组件 | 优势 | 劣势 | 我的建议 |
|---|---|---|---|
| Logstash | 插件丰富,数据处理能力强 | 吃内存,Java 栈,启动慢 | 适合复杂的数据清洗场景 |
| Fluentd | 轻量,Ruby 写的,资源占用低 | 部分插件维护一般 | 适合容器化环境 |
| Fluent Bit | 极轻量,C 写的,性能怪兽 | 功能相对精简 | 适合边缘节点或嵌入式场景 |
我个人习惯:新项目优先上 EFK,尤其是用 Fluent Bit 做采集端。为什么?因为现在都是容器化部署,资源敏感。Fluent Bit 的内存占用只有 Logstash 的十分之一,性能却差不多。
避坑指南:我曾经在一个高并发场景下用 Logstash 采集日志,结果 Logstash 自己 OOM 了,导致日志丢失。后来换成 Fluent Bit + 缓冲队列,再也没出过问题。记住,采集端一定要有背压机制。
3.3 搭建 EFK 栈:从零到一
好,我们直接上手。假设你有三台机器:一台跑 Elasticsearch,一台跑 Kibana,一台跑 Fluent Bit。当然,生产环境建议 ES 集群,这里简化演示。
3.3.1 Elasticsearch 部署
ES 是核心存储和检索引擎。我习惯用 Docker 快速拉起:
docker run -d \
--name elasticsearch \
-p 9200:9200 \
-p 9300:9300 \
-e "discovery.type=single-node" \
-e "ES_JAVA_OPTS=-Xms1g -Xmx1g" \
-v es_data:/usr/share/elasticsearch/data \
docker.elastic.co/elasticsearch/elasticsearch:8.11.0
嗯,这里要注意:ES 的 JVM 堆内存不要超过物理内存的 50%。我见过有人给 32G 机器分配了 24G 堆,结果 GC 频繁,集群直接挂了。留一半给操作系统做文件缓存,性能反而更好。
3.3.2 Kibana 部署
Kibana 是可视化层,没啥好说的,配好 ES 地址就行:
docker run -d \
--name kibana \
-p 5601:5601 \
-e "ELASTICSEARCH_HOSTS=http://your-es-ip:9200" \
docker.elastic.co/kibana/kibana:8.11.0
3.3.3 Fluent Bit 配置
这是关键。Fluent Bit 负责采集、解析、转发。我的配置文件长这样:
[SERVICE]
flush 1
log_level info
parsers_file parsers.conf
[INPUT]
name tail
path /var/log/app/*.log
tag app.*
parser json
read_from_head true
[FILTER]
name modify
match app.*
add hostname ${HOSTNAME}
add env production
[OUTPUT]
name es
match *
host your-es-ip
port 9200
index app-logs-%Y.%m.%d
type _doc
generate_id true
这里有个小技巧:用 %Y.%m.%d 做索引后缀,每天自动滚动。我习惯保留 30 天数据,配合 ILM(索引生命周期管理)自动删除旧索引,省心。
注意:Fluent Bit 的 parser 必须和日志格式匹配。如果你的日志是 JSON,就用 parser json。如果是自定义格式,需要自己写 parser 配置文件。我曾经因为 parser 写错了,导致所有日志都进了 _text 字段,排查了半天。
3.4 日志结构化实战:从混乱到清晰
假设你的应用日志还是非结构化的,怎么办?别急,Fluent Bit 的 Filter 插件可以帮你做「在线清洗」。
举个例子,原始日志:
2024-01-15 10:23:45 ERROR [com.api.order] order_id=12345, user_id=u_10086, amount=99.9, message="payment timeout"
我们可以用 lua 或 grep 插件把它拆成结构化字段。我个人习惯用 lua,灵活:
[FILTER]
name lua
match app.*
code function parse_log(tag, timestamp, record)
local msg = record["log"]
local level, logger, rest = msg:match("(%w+) %[(%S+)%] (.+)")
record["level"] = level
record["logger"] = logger
-- 继续解析 rest 字段...
return 1, timestamp, record
end
你看,这样就把杂乱无章的文本,变成了结构化的 JSON。ES 收到后,每个字段都可以独立搜索和聚合。
3.5 数据质量监控的基石:索引模板与 Mapping
日志进了 ES,不等于万事大吉。我见过最坑的事:ES 自动推断字段类型,把数字当成字符串。你想做聚合统计?没门。
所以,我习惯提前定义索引模板:
PUT _index_template/app-logs-template
{
"index_patterns": ["app-logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "5s"
},
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"level": { "type": "keyword" },
"logger": { "type": "keyword" },
"message": { "type": "text" },
"duration_ms": { "type": "integer" },
"amount": { "type": "float" },
"user_id": { "type": "keyword" },
"trace_id": { "type": "keyword" }
}
}
}
}
这样,duration_ms 是整数,amount 是浮点数,level 是关键字。你想算平均响应时间、统计错误率,直接写 ES 聚合查询就行。
数据质量的第一道防线:在日志入口就做好结构化,比事后清洗成本低 10 倍。索引模板 + Mapping 就是你的「数据契约」,谁都不能乱来。
3.6 整体架构图
说了这么多,画个图总结一下。这是 EFK 栈的典型数据流:
3.7 避坑总结
最后,分享几个我踩过的坑:
- 日志丢失:Fluent Bit 默认是异步模式,如果 ES 挂了,日志会丢。我习惯加一个
buffer配置,用磁盘做缓冲,保证数据不丢。 - Mapping 爆炸:ES 默认会动态生成字段。如果日志里有随机 key(比如
user_xxx),Mapping 会无限膨胀。我习惯用dynamic: false或dynamic: strict控制。 - 时区问题:日志里的时间戳不带时区,ES 默认按 UTC 存。我习惯在 Fluent Bit 里统一转成 UTC,Kibana 展示时再按用户时区转换。
- 性能瓶颈:Logstash 的 Grok 解析很慢。高并发场景下,我建议用 Fluent Bit 的
lua或regex解析,性能好一个数量级。
嗯,日志采集和结构化,说白了就是「把垃圾变成黄金」。做好了,后面的数据质量监控才有根基。做不好,你就是在沙子上盖楼。