Nginx1.31.7编译原生GeoIP模块并写入JSON访问日志

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 装的。翻了一遍脚本先确认了两件事:

  1. RealIP 模块本来就在编译参数里(--with-http_realip_module 和 --with-stream_realip_module 都有),不用额外补;
  2. 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。所以要改两处:

  1. 脚本顶部 ENABLE_GEOIP="yes"(升级流程的依赖安装也走这个开关);
  2. 在 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

验证顺序:

  1. 编译完先 ./objs/nginx -V 确认 geoip 参数在;
  2. nginx -t 重点看有没有 unknown directive "geoip_city"、数据库打不开、动态库加载失败这三类报错;
  3. 走真实访问路径(CDN → WAF → Nginx)发起请求再查日志,确认 client_ip 是真实访客 IP、GeoIP 字段有值。不要从内网直连源站测,那样验不出头信任逻辑;
  4. 拿新日志和旧 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 解析和字段映射,尤其是经纬度的数值类型。

八、参考资料

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇