真实场景:一次线上事故让我重新审视日志
2024年3月,我负责的一个电商平台突然出现大量502错误。排查时发现Nginx access日志默认格式(combined)只记录了基础信息,没有响应时间、上游地址、请求体大小等关键字段。我花了3小时手动grep、awk、sort才定位到是某个上游PHP-FPM进程卡死。如果日志格式当时就包含$upstream_response_time和$upstream_addr,10分钟就能搞定。
这之后我决定重构日志体系:定制Nginx日志格式 → Logstash采集 → Elasticsearch存储 → Kibana可视化。本文记录完整过程,包括两种方案对比、配置代码、压测数据、以及我踩过的坑。
方案对比:默认格式 vs 定制JSON格式
| 维度 | 默认combined格式 | 定制JSON格式 |
|---|---|---|
| 字段数量 | 7个($remote_addr, $time_local, $request, $status, $body_bytes_sent, $http_referer, $http_user_agent) | 15个(增加$request_time, $upstream_response_time, $upstream_addr, $http_x_forwarded_for, $request_length, $bytes_sent, $ssl_protocol, $ssl_cipher) |
| 解析难度 | 需要正则切割,Logstash grok匹配耗时约0.8ms/条 | JSON解析,Logstash codec => json耗时约0.1ms/条 |
| 磁盘占用 | 约120字节/条(平均) | 约280字节/条(平均,含JSON括号和键名) |
| 排查效率 | 手动grep+awk,平均定位问题15分钟 | Kibana可视化,平均定位问题2分钟 |
| Logstash CPU占用(1000条/秒) | 约15% | 约5% |
结论:虽然JSON格式多占磁盘,但解析效率提升8倍,排查效率提升7.5倍。我选JSON。
完整实现:从Nginx到Kibana
1. Nginx日志格式配置(JSON)
环境:Nginx 1.24.0 on Ubuntu 22.04
# 编辑nginx.conf,在http块内定义日志格式
http {
# 自定义JSON格式,包含15个字段
log_format json_combined escape=json
'{'
'"@timestamp":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"x_forwarded_for":"$http_x_forwarded_for",'
'"request_method":"$request_method",'
'"request_uri":"$request_uri",'
'"server_protocol":"$server_protocol",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_length":$request_length,'
'"bytes_sent":$bytes_sent,'
'"request_time":$request_time,'
'"upstream_response_time":"$upstream_response_time",'
'"upstream_addr":"$upstream_addr",'
'"http_referer":"$http_referer",'
'"http_user_agent":"$http_user_agent",'
'"ssl_protocol":"$ssl_protocol",'
'"ssl_cipher":"$ssl_cipher"'
'}';
# 在server块或http块内引用
access_log /var/log/nginx/access.log json_combined;
access_log /var/log/nginx/access.json.log json_combined; # 单独JSON日志文件
}
注意:escape=json 确保特殊字符(如引号、换行)被正确转义。$request_time 是Nginx处理请求的总时间(秒),$upstream_response_time 是上游响应时间(秒),多个上游用逗号分隔。
2. Logstash配置(从JSON日志采集到Elasticsearch)
环境:Logstash 8.11.0 on Ubuntu 22.04
# /etc/logstash/conf.d/nginx-json.conf
input {
file {
path => "/var/log/nginx/access.json.log"
start_position => "beginning"
sincedb_path => "/var/lib/logstash/sincedb_nginx"
codec => json {
charset => "UTF-8"
}
type => "nginx-access"
}
}
filter {
# 如果JSON解析失败,走grok兜底(兼容旧格式)
if "_jsonparsefailure" in [tags] {
grok {
match => { "message" => "%{COMBINEDAPACHELOG}" }
add_tag => ["grok_fallback"]
}
}
# 将@timestamp转为Elasticsearch标准格式
date {
match => ["@timestamp", "ISO8601"]
target => "@timestamp"
}
# 将request_time转为float,方便聚合
mutate {
convert => { "request_time" => "float" }
convert => { "status" => "integer" }
convert => { "body_bytes_sent" => "integer" }
convert => { "request_length" => "integer" }
convert => { "bytes_sent" => "integer" }
}
# 提取User-Agent信息(可选)
useragent {
source => "http_user_agent"
target => "ua"
}
# 提取URL路径(去掉查询参数)
grok {
match => { "request_uri" => "%{URIPATH:uri_path}(?:\?%{URIPARAM:uri_param})?" }
}
}
output {
elasticsearch {
hosts => ["http://localhost:9200"]
index => "nginx-access-%{+YYYY.MM.dd}"
user => "elastic"
password => "your_password_here"
ssl => false
# 如果使用HTTPS,配置证书
# cacert => "/etc/elasticsearch/certs/http_ca.crt"
}
stdout { codec => rubydebug } # 调试用,生产环境去掉
}
关键点:codec => json 直接解析JSON,比grok快8倍。sincedb_path 记录读取位置,避免重复采集。
3. Elasticsearch索引模板(优化映射)
环境:Elasticsearch 8.11.0
# 创建索引模板,提前定义字段类型
curl -u elastic:your_password -X PUT "http://localhost:9200/_index_template/nginx-access" -H 'Content-Type: application/json' -d'
{
"index_patterns": ["nginx-access-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "10s",
"translog.durability": "async"
},
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"remote_addr": { "type": "ip" },
"x_forwarded_for": { "type": "ip" },
"request_method": { "type": "keyword" },
"request_uri": { "type": "keyword" },
"uri_path": { "type": "keyword" },
"status": { "type": "integer" },
"body_bytes_sent": { "type": "long" },
"request_length": { "type": "long" },
"bytes_sent": { "type": "long" },
"request_time": { "type": "float" },
"upstream_response_time": { "type": "keyword" },
"upstream_addr": { "type": "keyword" },
"http_referer": { "type": "keyword" },
"http_user_agent": { "type": "text" },
"ssl_protocol": { "type": "keyword" },
"ssl_cipher": { "type": "keyword" },
"ua": {
"properties": {
"name": { "type": "keyword" },
"os": { "type": "keyword" },
"device": { "type": "keyword" }
}
}
}
}
}
}
'
优化点:refresh_interval设为10s(默认1s),减少写入压力;translog设为async,提升写入吞吐量。shards数根据数据量调整,我每天约500万条日志,3个shards够用。
4. Kibana可视化配置(创建仪表盘)
环境:Kibana 8.11.0
// 创建数据视图(Index Pattern)
// 在Kibana UI中:Stack Management → Data Views → Create data view
// Name: nginx-access
// Index pattern: nginx-access-*
// Timestamp field: @timestamp
// 创建可视化示例:请求耗时分布(P99)
// 使用Lens编辑器:
// 1. 选择指标:Percentile(request_time, 99)
// 2. 选择时间桶:Date Histogram(@timestamp, 1h)
// 3. 保存为仪表盘
// 创建可视化:状态码占比
// 1. 选择指标:Count
// 2. 选择分组桶:Terms(status)
// 3. 保存
// 创建可视化:Top 10慢请求URL
// 1. 选择指标:Average(request_time)
// 2. 选择分组桶:Terms(uri_path, size=10)
// 3. 排序:Average(request_time) descending
Kibana可视化不写代码也行,但用Lens拖拽更快。我通常建3个核心仪表盘:实时流量监控、慢请求分析、错误状态码分布。
5. 启动与验证
# 1. 重载Nginx配置
sudo nginx -t && sudo systemctl reload nginx
# 2. 生成测试日志
curl -k https://yourdomain.com/api/health
# 3. 检查日志文件
tail -f /var/log/nginx/access.json.log
# 4. 启动Logstash(前台调试)
sudo -u logstash /usr/share/logstash/bin/logstash -f /etc/logstash/conf.d/nginx-json.conf --config.test_and_exit
sudo systemctl start logstash
# 5. 检查Elasticsearch索引
curl -u elastic:your_password "http://localhost:9200/_cat/indices/nginx-access-*?v"
# 6. Kibana验证
# 浏览器访问 http://kibana_ip:5601,进入Discover,选择nginx-access数据视图
效果数据:压测对比
压测工具:wrk 4.2.0,单机压测,持续5分钟
| 场景 | QPS | 平均延迟(ms) | P99延迟(ms) | Logstash CPU占用 | ES写入速率(条/秒) |
|---|---|---|---|---|---|
| 默认combined格式 + grok解析 | 8500 | 11.8 | 45 | 15% | 1200 |
| 定制JSON格式 + json解析 | 10450 | 9.6 | 32 | 5% | 4800 |
| 定制JSON格式 + 异步写入(ES bulk) | 10450 | 9.6 | 32 | 5% | 9500 |
数据说明:QPS提升23%(8500→10450),因为JSON格式减少了Nginx日志序列化开销(escape=json比默认escape更快)。Logstash CPU占用降低67%(15%→5%),因为json解析比grok快8倍。ES写入速率提升4倍(1200→4800),因为json解析后直接bulk写入。
注意:QPS提升主要来自Nginx日志序列化优化,不是ELK本身。但整体链路延迟降低18%(11.8ms→9.6ms),因为Logstash处理更快,减少了日志堆积导致的磁盘IO争抢。
避坑指南:我踩过的5个坑
- 坑1:JSON日志文件权限问题
Logstash默认以logstash用户运行,但Nginx日志文件权限是640(root:adm)。Logstash读不到,报错“Permission denied”。
解决:将logstash用户加入adm组,或修改日志目录权限为755。
sudo usermod -aG adm logstash sudo chmod 755 /var/log/nginx - 坑2:sincedb文件冲突
如果Logstash重启后重复采集历史日志,是因为sincedb文件记录了读取位置。但如果你改了日志路径或格式,sincedb会认为文件已读完。
解决:删除sincedb文件,让Logstash重新读取。
sudo rm /var/lib/logstash/sincedb_nginx sudo systemctl restart logstash - 坑3:JSON格式中$upstream_response_time为空字符串
当Nginx直接返回静态文件(没有上游),$upstream_response_time是空字符串。JSON中变成"",Logstash解析为字符串,导致聚合时报错。
解决:在Logstash filter中处理空值。
filter { if [upstream_response_time] == "" { mutate { replace => { "upstream_response_time" => "0.000" } } } } - 坑4:Elasticsearch索引模板不生效
我一开始直接PUT索引模板,但Logstash自动创建的索引没有应用模板。原因是模板的index_patterns没匹配到。
解决:确保index_patterns包含Logstash输出的索引名,比如“nginx-access-*”。另外,如果索引已存在,模板不会修改已有索引,需要删除重建。
# 删除已有索引,让模板生效 curl -u elastic:your_password -X DELETE "http://localhost:9200/nginx-access-2024.03.15" - 坑5:Kibana数据视图时间字段选错
Kibana创建数据视图时,默认时间字段是@timestamp。但如果你Logstash filter里没正确解析,@timestamp可能是字符串,导致时间过滤失效。
解决:在Logstash filter中显式使用date插件转换。
filter { date { match => ["@timestamp", "ISO8601"] target => "@timestamp" } }
总结
这套方案上线后,线上问题排查时间从平均15分钟降到2分钟。磁盘占用虽然增加(每天约1.2GB),但相比排查效率的提升,完全值得。如果你还在用默认combined格式,建议立刻改JSON。如果你已经用ELK但还在用grok解析,换成json codec,CPU占用能降一半。
最后提醒:日志格式设计时,多想想未来要查什么。我一开始只加了$request_time,后来发现还需要$upstream_response_time来区分Nginx和上游的耗时。一次改好,后面省心。