日志分析与可视化

日志分析与可视化

写在前面

这是 Nginx 系列的最后一篇,讲日志。日志是排查问题的关键证据,但默认的 nginx access_log 太简单,满足不了生产环境的需要。本篇会讲怎么定制日志格式、怎么用 GoAccess 实时分析、怎么接入 ELK 做全链路分析,以及一个用日志找出恶意爬虫的真实案例。

一、access_log 格式定制

1.1 默认格式

log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

1.2 生产推荐格式

log_format main '$remote_addr - $remote_user [$time_local] '
                '"$request" $status $body_bytes_sent '
                '"$http_referer" "$http_user_agent" '
                '"$http_x_forwarded_for" '
                'rt=$request_time uct=$upstream_connect_time '
                'urt=$upstream_response_time '
                'req_id=$request_id';

关键变量:

1.3 JSON 格式(给 ELK 用)

log_format json escape=json
    '{'
        '"time":"$time_iso8601",'
        '"remote_addr":"$remote_addr",'
        '"request":"$request",'
        '"status":$status,'
        '"body_bytes_sent":$body_bytes_sent,'
        '"request_time":$request_time,'
        '"upstream_response_time":"$upstream_response_time",'
        '"upstream_addr":"$upstream_addr",'
        '"http_user_agent":"$http_user_agent",'
        '"http_referer":"$http_referer",'
        '"request_id":"$request_id"'
    '}';

escape=json 是关键,自动转义 JSON 里的引号、换行等特殊字符。

1.4 多日志分流

server {
    access_log /var/log/nginx/main.log main;

    # 静态资源不记日志
    location /static/ {
        access_log off;
        expires 30d;
    }

    # 健康检查单独记录
    location = /health {
        access_log /var/log/nginx/health.log main;
        return 200 "ok\n";
    }
}

静态资源(图片、CSS、JS)占访问量的 80%+,不记日志能省大量 IO。

二、日志切割

2.1 logrotate 配置

# /etc/logrotate.d/nginx

/var/log/nginx/*.log {
    daily                    # 每天切一次
    rotate 30                # 保留 30 天
    missingok                # 日志丢失不报错
    notifempty               # 空日志不切
    compress                 # 压缩
    delaycompress            # 压缩前一天的不压缩当天的
    sharedscripts            # 多个文件共享脚本
    postrotate
        # 让 Nginx 重新打开日志文件
        if [ -f /var/run/nginx.pid ]; then
            kill -USR1 $(cat /var/run/nginx.pid)
        fi
    endscript
}

USR1 信号让 Nginx 重新打开日志文件(不中断服务)。

2.2 手动测试

logrotate -d /etc/logrotate.d/nginx  # 模拟运行
logrotate -f /etc/logrotate.d/nginx  # 强制运行

2.3 大文件处理

# 按时间找大日志
find /var/log/nginx -size +100M -exec ls -lh {} \;

# 找最近一周的 5xx 错误
find /var/log/nginx -name "*.log.*.gz" -mtime -7 | xargs zcat | grep " 50[234] "

三、GoAccess 实时分析

3.1 安装

# Ubuntu
sudo apt install goaccess

# macOS
brew install goaccess

3.2 终端实时分析

goaccess /var/log/nginx/access.log -c

交互式 TUI,功能:

3.3 HTML 报表

goaccess /var/log/nginx/access.log \
  -o /var/www/html/report.html \
  --log-format=COMBINED \
  --real-time-html

--real-time-html 生成实时更新的 HTML 报表,通过 WebSocket 推送。

3.4 自定义日志格式

goaccess /var/log/nginx/access.log \
  --log-format='%h - %^ [%d:%t %^] "%r" %s %b "%R" "%u" "%^" rt=%T uct=%^ urt=%^ req_id=%^' \
  --date-format=%Y-%m-%d \
  --time-format=%H:%M:%S

四、ELK 完整日志分析

4.1 架构

[Nginx] → Filebeat → Logstash → Elasticsearch → Kibana
                                       ↓
                                  业务查询

4.2 Filebeat 配置

# /etc/filebeat/filebeat.yml
filebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /var/log/nginx/access.log*
    json.keys_under_root: true
    json.add_error_key: true
    fields:
      app: nginx
    fields_under_root: true

processors:
  - add_host_metadata:
      when.not.contains.tags: forwarded
  - add_cloud_metadata: ~

output.logstash:
  hosts: ["logstash:5044"]

4.3 Logstash 配置

# /etc/logstash/conf.d/nginx.conf
input {
  beats {
    port => 5044
  }
}

filter {
  if [app] == "nginx" {
    # 解析 user agent
    useragent {
      source => "http_user_agent"
      target => "ua"
    }

    # 解析 referer URL
    if [http_referer] {
      grok {
        match => { "http_referer" => "%{URIPROTO}://%{URIHOST:referer_host}%{GREEDYDATA:referer_path}" }
      }
    }

    # 解析 request
    grok {
      match => { "request" => "%{WORD:method} %{URIPATH:url}(?:\?%{NOTSPACE:query})? HTTP/%{NUMBER:http_version}" }
    }

    # IP 地理位置
    geoip {
      source => "remote_addr"
      target => "geo"
      database => "/etc/logstash/GeoLite2-City.mmdb"
    }

    # 慢请求标记
    if [request_time] {
      if [request_time] > 1.0 {
        mutate { add_tag => ["slow"] }
      }
    }
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "nginx-%{+YYYY.MM.dd}"
  }
}

4.4 Kibana 看板

常用可视化

{
  "title": "QPS by Status",
  "type": "line",
  "query": "app:nginx",
  "interval": "1m",
  "metrics": [
    { "type": "count", "field": "request" }
  ],
  "buckets": [
    { "type": "date_histogram", "field": "@timestamp", "interval": "1m" },
    { "type": "terms", "field": "status", "size": 5 }
  ]
}

常用看板:

4.5 关键查询(KQL)

# 5xx 错误
status: 5*

# 慢请求(> 1 秒)
request_time > 1

# 某 IP 的所有请求
remote_addr: "1.2.3.4"

# 某 URL 的所有错误
url: "/api/orders" and status: >=500

# 某时间段的异常
@timestamp:[now-1h TO now] and status: 502

# POST 请求中的 5xx
method: "POST" and status: 5*

五、实战:用日志找出恶意爬虫

5.1 故障背景

时间:2024 年 8 月某天 现象:公司产品页面的爬虫抓取量突然暴增 10 倍,带宽费用激增 目标:找出爬虫源 IP,屏蔽

5.2 第一步:看实时数据

goaccess /var/log/nginx/access.log -c

发现某个 /api/products/search 接口的请求量从 1k/小时涨到 50k/小时。

5.3 第二步:看 Top IP

# Kibana 查询
url: "/api/products/search"
| terms field="remote_addr" size=20 order by count desc

Top 10 IP 都是一个段:203.0.113.0/24。

5.4 第三步:看 User-Agent

url: "/api/products/search" and remote_addr: 203.0.113.*
| terms field="http_user_agent" size=10

全是 python-requests/2.31.0——Python 脚本。

5.5 第四步:看请求模式

url: "/api/products/search" and remote_addr: 203.0.113.*
| sort @timestamp asc
| limit 100

发现:

5.6 第五步:看 referer

remote_addr: 203.0.113.* and http_referer: *

没有 referer——普通浏览器都会有 referer,只有脚本和直接访问没有。

5.7 完整爬虫特征

IP 段:203.0.113.0/24
User-Agent:python-requests/2.31.0(完全没伪装)
请求频率:10 req/s(机器人特征)
Referer:空
查询参数:200+ 个不同关键词

5.8 应对方案

短期:Nginx 封禁

location /api/products/search {
    # 封禁爬虫 IP 段
    allow all;
    deny 203.0.113.0/24;

    # 限制请求频率
    limit_req zone=search_limit burst=20 nodelay;

    proxy_pass http://product_service;
}

中期:加验证码

location /api/products/search {
    if ($http_user_agent ~* "python-requests") {
        return 403;
    }

    if ($http_referer = "") {
        # 没有 referer 的请求,302 到验证码页
        return 302 https://company.com/captcha?return=$request_uri;
    }

    proxy_pass http://product_service;
}

长期:商业爬虫防护

接入 Cloudflare / 阿里云 WAF,提供爬虫识别服务(基于行为分析)。

5.9 长期监控

# Kibana 设置告警:某 IP 段请求突增 5 倍
remote_addr: 203.0.113.*
| count
| condition: count > 10000 (last 5m)
| action: Slack 通知

六、用 OpenResty 写自定义日志分析

6.1 安装 OpenResty

wget https://openresty.org/package/ubuntu/openresty_1.21.4.1~jammy_amd64.deb
sudo dpkg -i openresty_1.21.4.1~jammy_amd64.deb
sudo apt install openresty

6.2 实时统计 PV/UV

http {
    # 用 shared dict 计数
    lua_shared_dict pv_count 10m;
    lua_shared_dict uv_set 32m;

    log_by_lua_block {
        local uri = ngx.var.uri
        local ip = ngx.var.remote_addr

        -- PV
        local pv = ngx.shared.pv_count
        pv:incr(uri, 1, 0)

        -- UV
        local uv = ngx.shared.uv_set
        local key = uri .. "|" .. ip
        uv:set(key, 1, 60 * 60 * 24)  -- 24 小时
    }
}

6.3 输出 JSON 给前端

location /api/stats {
    content_by_lua_block {
        local pv = ngx.shared.pv_count
        local uv = ngx.shared.uv_set

        local stats = {}
        for _, k in ipairs(pv:get_keys(0)) do
            local count = pv:get(k) or 0
            stats[k] = count
        end

        ngx.header.content_type = "application/json"
        ngx.say(cjson.encode(stats))
    }
}

七、本篇小结

工具 适用
logrotate 日志切割
GoAccess 终端 / 简单 HTML 报表
ELK 完整日志分析平台
OpenResty 自定义实时分析
关键点 说明
日志格式 必须包含 request_time upstream_response_time request_id
JSON 格式 ELK 用 escape=json 自动转义
静态资源 用 access_log off 减 IO
切割 用 logrotate + USR1 信号

到这里 Nginx 进阶系列 9 篇全部完成。

系列回顾

  1. ✅ 配置文件深度解析
  2. ✅ location 匹配规则
  3. ✅ 反向代理与负载均衡
  4. ✅ HTTPS 配置全攻略
  5. ✅ 缓存策略
  6. ✅ 安全加固
  7. ✅ 性能调优
  8. ✅ 502/504 排查
  9. ✅ 日志分析

下一个系列是 前端构建工具演进,会展开 rsbuild 那篇文章的完整故事。

参考资料