Nginx日志格式与ELK采集分析实战
发布日期: 2026/07/19 阅读总量: 0
Nginx日志格式与ELK采集分析实战

真实场景:一次线上事故让我重新审视日志

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解析850011.84515%1200
定制JSON格式 + json解析104509.6325%4800
定制JSON格式 + 异步写入(ES bulk)104509.6325%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和上游的耗时。一次改好,后面省心。