[开源] ip2geo:零依赖的 Go IP 归属地查询库, IPv4 查询比 geoip2-golang 快 14~ 58 倍

haxqer 2026-10-28 19:34 1

GitHub: https://github.com/haxqer/ip2geo


写这个库的起因很简单:服务里用 geoip2-golang 查 IP 归属地,profile 一看,单次 City() 要 1.7µs 左右,大部分时间花在解码 mmdb 的数据结构上。而我其实只需要国家 / 省份 / 城市 / 经纬度 / 时区这几个字段。


于是做了 ip2geo:把 MaxMind GeoIP2 / GeoLite2 City (或 DB-IP )的 .mmdb 一次性转成紧凑的 .geoc 文件,之后直接在它上面查 IPv4 / IPv6 。


性能


Apple M3 单核,500 万个随机 IPv4 ,5 次取中位数:




































reader ns / 次 吞吐 相对 City()
geoip2-golang City() 1768 0.6 M/s 1×
geoip2-golang Country() 1121 0.9 M/s 1.6×
ip2geo city profile 119 8.4 M/s 14.5×
ip2geo country profile 30.5 32.8 M/s 58×

IPv6 在 country profile 下约 42ns ,city profile 约 260ns 。基准代码在仓库 bench/ 目录,可以自己复现。


特点



  • 读取端只依赖标准库,零第三方依赖

  • Lookup 零内存分配,*Reader 并发安全

  • 三档 profile 按需取舍:country ( 3.5 MiB )/ region ( 26 MiB )/ city ( 116 MiB ),以 123 MiB 的 GeoIP2-City 为源

  • 也可以不转换,mmdb.Open 直接读 .mmdb 在内存里建索引(启动约 2 秒),API 风格接近 geoip2-golang

  • 附带 CLI:ip2geo build / ip2geo lookup


不用注册 MaxMind 也能试


Release 里有基于 DB-IP Lite ( CC BY 4.0 )构建好的 .geoc:


curl -LO https://github.com/haxqer/ip2geo/releases/download/data-2026-10/dbip-city-lite.geoc.gz
gunzip dbip-city-lite.geoc.gz
go run github.com/haxqer/ip2geo/cmd/ip2geo@latest lookup -db dbip-city-lite.geoc 8.8.8.8
# 8.8.8.8: US / California / Mountain View (37.4220, -122.0850)

刚发布 v0.1.0 ,MIT 协议。欢迎试用、提 issue ,觉得有用的话给个 star 🙏

最新回复 (7)
  • xiaoshouchen 10-28 21:39
    1楼
    是用空间换时间,把所有数据存在内存中吗?
  • hzz2 10-28 21:49
    2楼
    没压测过,但是感觉也不慢 https://ip.hzzio.top , https://github.com/hezhizheng/goip 可以自己部署
  • haxqer 楼主 10-29 11:00
    3楼
    @xiaoshouchen

    不算空间换时间,其实是反过来的:文件比原版 mmdb 还小。

    拿 123 MiB 的 GeoIP2-City 转出来,city 档是 73.7 MiB ,country 档只有 1.7 MiB 。加载方式跟 geoip2-golang 一样,都是 mmap 整个文件,所以占的内存也更少。

    快主要靠两点:
    1. 查找结构换了。mmdb 用的是按位走的二叉 trie ,一次查询要在内存里随机跳 20 ~ 30 次,缓存命中很差。ip2geo 改成 DXR:先拿 IP 的高 20 位直接定位到一个桶,再在桶里做一次很短的二分,几次访存就够了。
    2. 不在查询时解码。mmdb 每查一次都要反序列化一遍 map 结构( geoip2-golang 走的是反射加接口),ip2geo 在构建时就把记录整理成定长字段,查询时直接读。

    另外可以按需取精度:只要国家就用 country 档,边界数从几百万降到二十多万,查一次大约 30 ns 。
  • haxqer 楼主 10-29 11:01
    4楼
    @hzz2
    感谢分享,看了一下 goip ,部署成服务开箱即用挺方便的👍

    两者定位稍有不同:ip2geo 是一个 Go 库,在进程内直接调用,适合日志批处理、网关里每个请求都要查一次这类对单次耗时敏感的场景。走 HTTP 的话,网络开销本身就是几十微秒到毫秒级,查询快几十倍基本感觉不到。

    如果 goip 底层用的是 geoip2-golang 或 mmdb ,倒是可以试试把 mmdb.Open 换进去当后端,接口差不多,可以直接替换。
  • haxqer 楼主 10-29 11:10
    5楼
    @hzz2
    感谢分享~顺手看了下,goip 底层是 maxminddb-golang v1 ,每次 Lookup 进一个带全部语言 map 的结构体。我用同一份 GeoIP2-City 简单压了一下( M3 ,200 万随机 IPv4 ):

    goip 这种用法:~2.8 µs/次,每次分配 70 次 / ~2 KB ,8 核总吞吐 ~1.5M/s
    ip2geo city 档:~90 ns/次,零分配,8 核 ~48M/s

    不过对 HTTP 服务来说,一次请求本身就几十微秒,这点差距基本体现不出来。ip2geo 更适合进程内大批量查询,比如日志清洗、网关里每个请求都要查一次。另外 ip2geo 目前没有代理/VPN 标记字段,Merged-IP 的那部分数据还替代不了。
  • xiaoshouchen 10-29 11:11
    6楼
    @haxqer 我之前做 CI 的时候,总会把 mmdb 打包到容器里,image 尺寸就大了很多,http 太慢,后面独立一个 rpc 服务似乎也还行。你们都优化到 ns 级别了,着实牛逼
  • haxqer 楼主 10-29 11:30
    7楼
    @xiaoshouchen

    谢谢~ ns 级主要还是靠换了数据结构,加上按需降低精度,不是什么黑科技😂

    你说的镜像体积和 RPC 这两个痛点,正好是我做这个的初衷之一:

    体积:大部分业务其实只需要国家或省份。country 档转出来只有 1.7 MiB ,region 档 13 MiB ,city 档 74 MiB ,都比原始的 mmdb ( 123 MiB )小。只要国家的话,直接 go:embed 进二进制都没压力:

    //go:embed country.geoc
    var geoc []byte
    db, _ := ip2geo.OpenBytes(geoc)

    这样就不用单独打包数据文件了,镜像几乎不会变大。

    RPC:一次 RPC 来回至少几十到几百微秒,进程内查一次是几十纳秒,差了三个数量级。文件小了以后,每个服务自己带一份直接查就行,不用再维护一个独立的 IP 服务。

    如果确实需要城市级别,可以不打进镜像,改成挂载卷或在 init container 里下载 .geoc 。启动时 mmap 打开只要几十毫秒,更新数据也不用重新发版。
* 帖子来源V2EX
返回