Nginx1.31.7编译原生GeoIP模块并写入JSON访问日志
记录时间:2026-10-21
环境:RHEL/Kylin 系(离线源);Nginx 1.31.7 源码编译(前缀 /AppHome/nginx、运行用户 <运行用户>);安装脚本 shell-repository/nginx_install/install_nginx.sh;日志格式沿用现网 conf 的 aka_logs(escape=json)
一、背景
想把 GeoIP 解析从 Logstash 挪到 Nginx 侧:1.31.7 起原生 ngx_http_geoip_module 可以直接读 GeoLite2-City.mmdb,把国家、城市、经纬度直接写进 JSON 访问日志,Logstash 少干一份活。这个模块默认不编译,二进制部署需要重编一次。
我自己这套环境是 install_nginx.sh 装的。翻了一遍脚本先确认了两件事:
- RealIP 模块本来就在编译参数里(
--with-http_realip_module和--with-stream_realip_module都有),不用额外补; - GeoIP 留了开关
ENABLE_GEOIP,默认是no(离线源缺 libmaxminddb 时编译不过,所以默认关着)。
所以这次要动的只有一件事:把 GeoIP 模块编进去。
二、现状检查
nginx -v
nginx -V 2>&1 | tr ' ' '\n' | grep -E "geoip|realip"
预期:现有二进制有 realip、没有 geoip。脚本当前编译参数对照:
| 编译参数 | 脚本现状 | 说明 |
|---|---|---|
--prefix=/AppHome/nginx --user=<运行用户> --group=<运行用户> |
已有 | 安装前缀与运行用户 |
--with-http_ssl_module --with-pcre --with-openssl-opt='enable-tls1_3 enable-weak-ssl-ciphers' |
已有 | SSL/TLS |
--with-http_v2_module --with-http_gzip_static_module --with-http_sub_module |
已有 | HTTP/2、静态 gzip、sub |
--with-http_stub_status_module |
已有 | 状态页 |
--with-http_realip_module --with-stream_realip_module |
已有 | RealIP,不用再补 |
--with-stream --with-stream_ssl_module |
已有 | stream 四层 |
--with-http_geoip_module |
ENABLE_GEOIP 开关(默认 no) |
本次要开的 |
--add-module=ngx_brotli |
brotli 模式才加 | 可选 |
三、编译二进制(两条路)
3.1 前置:不用脚本时,手工编译环境需要这些依赖
如果不走 install_nginx.sh,手工编译前先确认构建机和运行机的依赖。RPM 系(Kylin/RHEL/CentOS):
# 基础编译工具链与常规依赖(对应脚本 ensure_pkgs 的 RPM 列表)
rpm -q gcc gcc-c++ make pcre-devel zlib-devel openssl-devel
yum install -y gcc gcc-c++ make pcre-devel zlib-devel openssl-devel
# GeoIP:libmaxminddb 是运行时库(运行机也要装),-devel 提供 configure 要的 maxminddb.h
rpm -q libmaxminddb libmaxminddb-devel
yum install -y libmaxminddb libmaxminddb-devel
# 要 brotli 模块再加
yum install -y brotli brotli-devel unzip
Debian 系对应包名:
apt-get install -y gcc g++ make libpcre3-dev zlib1g-dev libssl-dev
apt-get install -y libmaxminddb libmaxminddb-dev
依赖说明:
| 包 | 用途 | 需要它的机器 |
|---|---|---|
gcc gcc-c++ make |
编译工具链 | 构建机 |
pcre-devel zlib-devel openssl-devel |
正则、压缩、SSL 开发库 | 构建机 |
libmaxminddb |
GeoIP 运行时库 | 构建机 + 运行机 |
libmaxminddb-devel(deb 系 libmaxminddb-dev) |
开发头文件 maxminddb.h,configure 检测用 |
构建机 |
brotli brotli-devel unzip |
brotli 模块(可选) | 构建机 |
各发行版实测(devel 包在不在默认源,差别很大):
| 发行版 | 实测情况 | 处理 |
|---|---|---|
| Rocky Linux 9.5 | 默认源没有 devel 包,在 CRB 仓库里(2026-10-21 实测) | 启用 CRB 再装 |
| Kylin V10 (Halberd) | 本地源(含 ISO 挂载源)没有 devel 包,只有运行时库 libmaxminddb |
自行编译安装,或先关 GeoIP |
| openEuler 22.03 LTS-SP3 | 默认源没有 devel 包(只有 libmaxminddb 和 libmaxminddb-help) |
同上 |
Rocky 9 系先启用 CRB 仓库:
dnf repolist --all | grep -i crb # 确认 crb 仓库存在且处于禁用
dnf config-manager --set-enabled crb # 启用
dnf clean all && dnf makecache # 刷新元数据
dnf install -y libmaxminddb-devel # 此时从 crb 仓库装上
实测记录:Rocky 9.5 启用 CRB 后装上 libmaxminddb-devel-1.5.2-4.el9,脚本 standard 模式一次编译通过,nginx -V 里已带 --with-http_geoip_module。
Kylin / openEuler 没有 devel 包时的出路是自行编译安装 libmaxminddb:标准源码编译(./configure && make && make install 装到 /usr/local),完事 ldconfig。脚本的 maxminddb.h 检查兼容 /usr/local/include 这个位置,装到那里就能过。不想折腾就先 ENABLE_GEOIP="no",等有包了再开。
装完先确认头文件在位,再跑 configure:
test -f /usr/include/maxminddb.h && echo "maxminddb.h 就绪"
缺这两个包时 configure 会报找不到 maxminddb 库,而不是”geoip 参数不认识”。离线源没有这两个包就编译不了 geoip,这也是脚本里 ENABLE_GEOIP 默认关着的原因。编译完顺手验运行时链接:
ldd objs/nginx | grep -i maxminddb
# 输出 not found 说明运行机缺 libmaxminddb,先装库再 ldconfig,别切线上二进制
3.2 全新安装(standard / brotli 模式)
把脚本顶部 ENABLE_GEOIP="no" 改成 "yes",然后:
bash install_nginx.sh standard # 或 brotli
脚本会自动装 libmaxminddb-devel(RPM 系)或 libmaxminddb-dev(deb 系),configure 时自动拼上 --with-http_geoip_module,编译完成后检查 maxminddb.h 存在才继续,缺了会直接报错退出。
注意:全新安装会把现有
/AppHome/nginx整个备份搬走(脚本里mv到.bak.时间戳)再重新装,配置、证书要重新套用。这条路只适合新机器或能接受停机窗口的场景,线上机别直接走这个。
3.3 平滑升级(upgrade 模式,线上推荐)
脚本的 upgrade 流程是:提取现有 nginx -V 的 configure arguments 重新编译 → 备份旧二进制 → 替换 → nginx -t 校验 → USR2/WINCH/QUIT 平滑热切换,任一步失败自动回滚。
但它有个坑:提取的是现有参数,现有参数里没有 geoip。所以要改两处:
- 脚本顶部
ENABLE_GEOIP="yes"(升级流程的依赖安装也走这个开关); - 在
extract_configure_args()函数的末尾(log "将使用以下 configure 参数..."之前)追加一行,把 geoip 参数拼进去:
CURRENT_ARGS="$CURRENT_ARGS --with-http_geoip_module"
然后:
bash install_nginx.sh upgrade
几个脚本里已经处理好的细节,升级时别改坏:
- 替换二进制用
mv而不是cp(cp以写方式打开正在运行的可执行文件会触发 ETXTBSY 静默失败,磁盘还是旧二进制,升级形同没做); - 替换后脚本会断言磁盘二进制版本号,对不上直接退出;
nginx -t失败、新 master 未接管都会自动回滚,回滚路径verify_upgrade末尾有提示。
静态编译后不需要 load_module,也不用单独维护 .so 文件。动态模块(=dynamic)要版本和 ABI 严格匹配,维护成本高,不推荐。
四、准备 GeoLite2 数据库
mkdir -p /usr/share/GeoIP
ls -lh /usr/share/GeoIP/GeoLite2-City.mmdb
test -r /usr/share/GeoIP/GeoLite2-City.mmdb && echo readable
更新库文件时先下载到临时文件、校验后再原子替换(mv 过去),避免 Nginx 读到下了一半的库。离线库的获取过程我之前记过一篇(ELK 目录下的 GeoLite2 下载笔记)。
五、配置
5.1 真实客户端 IP(RealIP)
Nginx 的 geoip_proxy_recursive on 是从右往左跳过可信代理、选最后一个非可信地址,”取 XFF 最左边”这个说法不对。为了匹配 CDN 的整条 IP 列表而手工维护上万个网段不现实。
我的做法是让 WAF 规范化真实 IP:WAF 转发时覆盖(不是透传)X-Real-IP,写成已验证的单个访客 IP,源站只允许 WAF 访问。Nginx 侧:
http {
# 只信任 WAF 出口地址,示例网段必须换成实际的;绝不能写成 0.0.0.0/0
set_real_ip_from 10.20.0.0/24;
real_ip_header X-Real-IP;
real_ip_recursive off;
geoip_city /usr/share/GeoIP/GeoLite2-City.mmdb;
# 其余配置……
}
安全前提:如果访客能绕过 WAF 直连源站、或者 WAF 只是原样转发用户提交的
X-Real-IP,攻击者就能伪造客户端 IP。必须由 WAF 覆盖该头,并用防火墙/安全组把源站限制成只收 WAF 流量。如果 WAF 改不了头、只能收多段X-Forwarded-For,也不要无条件信最左地址,先解决”哪些代理可信”这个前提。
5.2 GeoIP 与 JSON 访问日志(在 aka_logs 基础上加字段)
现网日志格式是 aka_logs(现网 nginx.conf 里定义),我直接在它的定义里追加 GeoIP 字段,其余字段原样保留,下游 Filebeat/ES 的字段映射不受影响。经纬度用 map 兜底:库没返回值时输出 JSON 的 null,避免空字符串拼出非法数值字段。
http {
geoip_city /usr/share/GeoIP/GeoLite2-City.mmdb;
map $geoip_latitude $geoip_latitude_json {
"" null;
default $geoip_latitude;
}
map $geoip_longitude $geoip_longitude_json {
"" null;
default $geoip_longitude;
}
# aka_logs 原字段不动,只在 file_dir 与 http_user_agent 之间追加 GeoIP 字段
log_format aka_logs escape=json
'{"@timestamp":"$time_iso8601",'
'"host":"$hostname",'
'"server_ip":"$server_addr",'
'"request_id":"$request_id",'
'"client_ip":"$remote_addr",'
'"xff":"$http_x_forwarded_for",'
'"domain":"$host",'
'"http_host":"$http_host",'
'"server_name":"$server_name",'
'"url":"$uri",'
'"referer":"$http_referer",'
'"args":"$args",'
'"upstreamtime":"$upstream_response_time",'
'"upstream_status":"$upstream_status",'
'"upstream_connect_time":"$upstream_connect_time",'
'"upstream_header_time":"$upstream_header_time",'
'"upstream_response_length":"$upstream_response_length",'
'"responsetime":"$request_time",'
'"request_method":"$request_method",'
'"status":"$status",'
'"size":"$body_bytes_sent",'
'"request_body":"",'
'"request_length":"$request_length",'
'"protocol":"$server_protocol",'
'"scheme":"$scheme",'
'"ssl_protocol":"$ssl_protocol",'
'"ssl_cipher":"$ssl_cipher",'
'"connection_requests":"$connection_requests",'
'"upstreamhost":"$upstream_addr",'
'"file_dir":"$request_filename",'
'"country_code":"$geoip_city_country_code",'
'"country_name":"$geoip_city_country_name",'
'"region":"$geoip_region_name",'
'"city":"$geoip_city",'
'"latitude":$geoip_latitude_json,'
'"longitude":$geoip_longitude_json,'
'"postal_code":"$geoip_postal_code",'
'"http_user_agent":"$http_user_agent"'
'}';
# server {} 在 http {} 内
}
access_log 继续用 aka_logs(现网路径为例):
access_log /AppLogs/ngx_acc_logs/access.log.json aka_logs;
变量说明:
| 变量 | 含义 |
|---|---|
$geoip_city_country_code |
国家/地区二字码 |
$geoip_city_country_name |
国家/地区名称 |
$geoip_region_name |
省/州/地区名称 |
$geoip_city |
城市名称 |
$geoip_latitude / $geoip_longitude |
经纬度 |
$geoip_postal_code |
邮政编码 |
地理库不一定给每个 IP 都返回城市、邮编或坐标,缺失是正常情况;城市定位本身是 IP 级估算,不当作精确位置。
六、验证
nginx -V 2>&1 | grep -E -- '--with-http_geoip_module|--with-http_realip_module'
nginx -t
nginx -s reload
tail -f /AppLogs/ngx_acc_logs/access.log.json
验证顺序:
- 编译完先
./objs/nginx -V确认 geoip 参数在; nginx -t重点看有没有unknown directive "geoip_city"、数据库打不开、动态库加载失败这三类报错;- 走真实访问路径(CDN → WAF → Nginx)发起请求再查日志,确认
client_ip是真实访客 IP、GeoIP 字段有值。不要从内网直连源站测,那样验不出头信任逻辑; - 拿新日志和旧 Logstash 解析结果对比客户端 IP 与地理位置,一致后再动旧流程。
七、注意事项
ENABLE_GEOIP默认no是有原因的:Kylin V10、openEuler 的本地源没有libmaxminddb-devel(实测),Rocky 9 要启用 CRB 仓库才有。开之前先确认源里装得上这个包,脚本会检查/usr/include/maxminddb.h和/usr/local/include/maxminddb.h,没有会明确报错退出;- upgrade 模式两处都要改(开关 + 参数追加),只开开关不追加参数的话,编译完还是没 geoip,白升一次;
- 别用精简参数覆盖现有编译参数:脚本 upgrade 模式本来就提取原参数重编译,这个原则脚本已落实,手工改的时候别绕回去;
- 运行时链接检查:
ldd objs/nginx | grep maxminddb,如果not found先装运行时库并ldconfig,别切线上二进制; - 这套方案只替代 GeoIP 解析。现有 Logstash 还做字段类型转换、useragent 解析写
ua、字段清理这些活,没验证这些字段不再被 ES 查询/仪表盘/告警依赖之前,不要直接下线 Logstash。Filebeat 直接收 JSON 之前,先验 JSON 解析和字段映射,尤其是经纬度的数值类型。