移动用户反馈页面卡顿,电信用户访问相同接口却很流畅,这类问题在运维群里并不少见。排查时最尴尬的是,Nginx 默认的 access.log 里只有客户端 IP、请求时间、状态码和 User-Agent,根本没有运营商字段。一个地址是 223.104.x.x 的请求,到底来自移动 4G 还是联通宽带,光看 IP 段有时能看出来,但遇到动态 IP、跨省调度或小运营商线路就很容易误判。想让日志具备线路分析能力,关键是把 IP 归属地查询下沉到 Nginx 写入日志之前,把运营商、自治系统号和地理位置作为自定义字段输出。

默认日志为什么区分不了运营商
Nginx 默认的 combined 日志格式长这样:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
这里面几个变量分别代表客户端地址、请求行、状态码和响应体大小。它们全部来自 HTTP 请求本身或 TCP 连接信息,Nginx 不会主动去查询 IP 属于哪家运营商。部分运维会尝试用 geo 指令或 map 写死 IP 段,但运营商地址段经常调整,维护起来非常痛苦。更关键的是,不同运营商之间的跨网流量往往集中在骨干网互联点,电信访问移动源站、移动用户回联通源站这类请求天然容易延迟升高。如果日志里没有 ASN 字段,就无法把问题聚合到具体线路。
ASN 是自治系统号,运营商和大型云厂商都会拥有一个或多个 ASN。比如中国电信骨干通常对应 AS4134,联通骨干对应 AS4837,移动骨干对应 AS9808。把这些编号记进日志,比写入运营商中文名更稳定,也更方便程序筛选。
用 GeoIP2 模块给日志增加线路字段
推荐使用 ngx_http_geoip2_module,它基于 libmaxminddb 读取 mmdb 格式 IP 数据库。先编译模块:
./configure --with-compat --add-dynamic-module=/usr/src/ngx_http_geoip2_module make && make install
安装完成后,在 nginx.conf 里加载模块并声明 GeoIP2 数据库位置。GeoLite2-City 提供国家、省份和城市,GeoLite2-ASN 提供自治系统号与运营商名称。两个数据库可以分别用 geoip2 块加载。
http {
geoip2 /etc/nginx/GeoIP/GeoLite2-City.mmdb {
$geoip2_data_country_code country iso_code;
$geoip2_data_city_name city names zh-CN;
}
geoip2 /etc/nginx/GeoIP/GeoLite2-ASN.mmdb {
$geoip2_data_asn_code source $remote_addr;
$geoip2_data_asn_name source $remote_addr;
}
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$geoip2_data_country_code $geoip2_data_city_name '
'$geoip2_data_asn_code $geoip2_data_asn_name';
}
这里的 $geoip2_data_asn_code 会直接输出 ASN 编号,例如 4134。日志示例可能变成:
203.0.113.15 - - [12/Nov/2025:10:23:45 +0800] "GET /api/index HTTP/1.1" 200 3456 "-" "Mozilla/5.0" "-" CN Beijing 4134 CHINANET
需要说明,GeoLite2 是免费数据库,精度足够做线路级分析。如果业务对精度要求更高,可以换 GeoIP2 商业版或国内 ip2region。后者的优势是单文件、查询快,但需要额外写一个小脚本或接入 Nginx 第三方模块来生成映射。
日志字段不要一次性加太多,否则会显著增加磁盘写入。一般建议只加国家、城市、ASN 三个字段,运营商中文名可以事后通过 ASN 表关联,不必写在每行日志里。对于高 QPS 场景,还可以考虑抽样记录响应时间字段,避免日志膨胀。
如何按线路统计响应时间和错误率
有了携带 ASN 字段的日志后,第一件事就是做分组聚合。下面这段 awk 会按 ASN 统计平均响应时间和 5xx 错误数量,假设日志最后一列是 ASN,倒数第 3 列是状态码,倒数第 4 列是 $request_time。
awk '{asn=$NF; status=$(NF-3); rt=$(NF-4)}
{sum[asn]+=rt; cnt[asn]++; if(status>=500) err[asn]++}
END {for(a in sum) printf "%s avg_rt=%.3f count=%d err5xx=%d\n", a, sum[a]/cnt[a], cnt[a], err[a]}' access.log | sort -k2 -nr
输出示例会类似这样:
4134 avg_rt=0.212 count=83542 err5xx=31 4837 avg_rt=0.198 count=79031 err5xx=28 9808 avg_rt=0.386 count=92117 err5xx=143
从结果可以明显看到,移动 AS9808 的平均响应时间比电信和联通高出一大截,5xx 错误也更多。这通常意味着移动用户访问当前源站存在跨网绕路。进一步可以按省份过滤,检查是不是某个省份移动宽带集中报障。把 $geoip2_data_city_name 或省份字段加入分组,就能定位到更小区域。
如果平均响应时间不能反映尾部延迟,可以再对 $request_time 做分位数统计。Nginx 日志只保留每请求一个值,借助 sort -n 和 awk 也能粗略计算 p95、p99,帮助判断是否只是个别长尾请求拉高平均值。对于排查运营商线路来说,p95 通常比平均值更有参考价值。
根据线路差异做后端调度优化
统计出差异之后,不能停在报表上。Nginx 的 map 指令可以根据 ASN 把流量分配到不同的后端。比如移动用户走移动源站,电信用户走电信源站,减少跨网回源。
map $geoip2_data_asn_code $backend {
default default_backend;
4134 telecom_backend;
4837 unicom_backend;
9808 mobile_backend;
}
upstream telecom_backend {
server 10.0.1.10:8080;
keepalive 32;
}
upstream unicom_backend {
server 10.0.2.10:8080;
keepalive 32;
}
upstream mobile_backend {
server 10.0.3.10:8080;
keepalive 32;
}
server {
location / {
proxy_pass http://$backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
这种多源站方案适合源站已经做了三线部署的场景。如果只有单源站,也可以利用缓存节点:让移动用户优先回源到移动线路的缓存服务器,或者在 Nginx 层面对移动线路启用更长的缓存时间。线路级别的路由判断还可以交给 DNS 智能解析,但 Nginx 日志仍然要保留 ASN,否则调度效果无法量化。
另外要注意 IP 库更新。ASN 分配会变化,运营商会调整线路,如果半年不更新 GeoIP 库,部分 IP 会被归到 unknown。推荐使用定时任务每周拉取最新数据库,并通过 nginx -s reload 重新加载。日志分析脚本也可以按天跑,输出到监控平台,一旦某条线路错误率突增就能告警。
最后还有一点:运营商信息属于网络元数据,用于运维排障和调度优化一般没有问题,但在对外展示或存储时仍然需要遵守内部数据安全规范,必要时只保留 ASN 和城市,去掉精确 IP。