日志分析与可视化
日志分析与可视化
写在前面
这是 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';
关键变量:
rt=$request_time:请求总耗时——排查慢请求第一字段uct=$upstream_connect_time:连接 upstream 耗时——区分"连不上"和"响应慢"urt=$upstream_response_time:upstream 响应耗时——后端慢不慢req_id=$request_id:请求唯一 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,功能:
- 唯一访客(按 IP)
- 请求的文件
- 静态资源请求
- 404 / 5xx 错误
- 操作系统 / 浏览器 / 爬虫分布
- 流量来源(Referer)
- 地理位置(需要 GeoIP 数据库)
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 }
]
}
常用看板:
- QPS 趋势:每分钟请求数
- 状态码分布:2xx/3xx/4xx/5xx 饼图
- P95/P99 延迟:request_time 分布
- Top 慢 URL:按 request_time 排序
- Top 错误 URL:按 5xx 排序
- 爬虫/UA 分布:user_agent 分类
- 流量来源:Referer 域名
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
发现:
- 每个 IP 每秒 10 个请求
- 每次都换不同关键词
- 请求间隔非常规律(机器人特征)
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 篇全部完成。
系列回顾
- ✅ 配置文件深度解析
- ✅ location 匹配规则
- ✅ 反向代理与负载均衡
- ✅ HTTPS 配置全攻略
- ✅ 缓存策略
- ✅ 安全加固
- ✅ 性能调优
- ✅ 502/504 排查
- ✅ 日志分析
下一个系列是 前端构建工具演进,会展开 rsbuild 那篇文章的完整故事。