1. 为什么我会从 GeoLite2 换到纯真社区版 CZDB
1.1 GeoLite2 真正让人难受的地方,不是数据本身
我这边有个内部日志分析系统,每天要处理几百万条访问记录,里面带着客户端 IP,需要把 IP 归属地解析到省、市、运营商三个维度。最早技术选型时图省事,直接用 GeoLite2,因为网上教程多、Python 生态里geoip2包也成熟,确实跑起来很顺。但用久了才发现,真正的成本不在查询,而在数据获取。
GeoLite2 更新有几个绕不过去的门槛:需要注册 MaxMind 账号,生成 License Key,下载时带上 key 参数;每月更新一次,但每次都要重新登录确认条款;文件本身是 gzip 压缩的 mmdb,解压后还要定期替换。听起来不复杂,可一旦上了 CI 自动化,这些步骤就变成了一堆环境变量、凭证轮换、下载失败重试逻辑。更头疼的是,公司内网部署环境不能随便访问外网下载,每次更新都得人肉把文件拷进去。时间一长,我就开始想找替代方案。
同事当时给我推荐纯真社区版,我第一反应是:纯真不是老牌的 txt 格式离线库吗,解析字符串能有什么性能?后来才发现它已经推出了 CZDB 格式,不是简单地把纯文本换个后缀,而是重新设计了二进制存储结构。这让我重新产生了兴趣。
1.2 选“免费 IP 库”时,我列了一张需求清单
在动手对比之前,我给替代方案列了五条硬指标,这些指标也直接影响后面的测试方式:
- 数据获取的便利性:最好能直接下载,不需要频繁登录、填 License Key;如果是脚本自动更新,流程越短越不容易坏。
- 国内 IP 覆盖质量:我们主要流量来自国内,省内、城市、运营商三级的准确度比海外精度更重要。
- 查询性能:日志分析是批量场景,单条查询尽量在亚毫秒级,内存占用不能太高。
- 格式稳定性和解析成本:如果格式变化频繁,维护解析代码的成本会反超收益。
- 使用许可边界:免费版能不能用于内部系统,能不能对外提供服务,必须提前搞清楚。
表格整理一下就是这样的:
| 维度 | 我的要求 | GeoLite2 的情况 | 纯真社区版 CZDB 的情况 |
|---|---|---|---|
| 获取方式 | 下载简单、可自动化 | 需注册账号和 License Key | 直接从发布页下载数据库文件 |
| 国内精度 | 省市运营商准确 | 城市级尚可,国内区县数据偏粗 | 国内区县和运营商覆盖有传统优势 |
| 查询性能 | 亚毫秒级 | 优秀,mmdb 查询很快 | 二进制格式下表现也不错 |
| 格式稳定 | 长期稳定 | 稳定,mmdb 多年不变 | CZDB 在逐步替换老 txt,版本需要关注 |
| 许可 | 内部系统可用 | 有归属声明要求,商用量有限制 | 社区版有限制,商用需确认授权方案 |
这个清单帮我定位得很明确:我对海外 IP 的精确度要求不高,但对国内省市和运营商的解析质量要求很高,而且特别在意更新是否顺手。从这个角度看,纯真社区版确实值得一试。
2. CZDB 格式到底变在哪儿:从文本文件到二进制数据库
2.1 老的纯真 txt 格式,用起来有哪些痛点
如果你用过老版纯真,应该对那个结构有印象:一个纯文本文件,每行记录一段 IP 范围,格式类似IP1, IP2, 国家, 省, 市, 运营商。数据人能看懂,但对程序非常不友好。
首先,解析性能差。纯文本要逐行 split,字符串转整数,遇到空字段还要补默认值,在 Python 里读完整个文件做几千次查询就会明显感觉到卡顿。其次,内存占用高。文件本身几十上百 MB,加载到内存后 Python 字符串对象的开销会让真实占用翻好几倍。最后,老格式对 IPv6 的支持很弱,而现在线上流量里 IPv6 占比越来越高,不处理不行。
CZDB 这个格式就是为了解决这些问题出现的。它把原来分散的文本行,转成了有索引、有分区的二进制结构,查询时不再需要顺序扫描,而是可以在索引段里直接定位。
2.2 CZDB 的二进制结构:我的理解与验证方式
关于 CZDB 的内部结构,官方没有开放特别详细的格式文档,更多是靠 SDK 源码来理解。我自己看了解析部分的代码,结合实测,大致可以把结构分成几个部分:头部校验段、IPv4 索引区、IPv4 数据区、IPv6 索引区、IPv6 数据区。头部通常会包含版权信息、版本号、生成时间等元数据,用于判断文件是否完整、是否匹配解析器版本。
数据记录的核心思想是把 IP 范围映射为一条归属信息。简单来说,查询一个 IP 时,先把它转成整数,然后在索引区里做二分查找,定位到包含这个 IP 的区段,再取出对应的数据记录。这种设计和 GeoLite2 的 mmdb 用树形结构存储不太一样,但查询复杂度都能做到对数级别。实际用下来,单次查询在 Python 中也能保持在微秒到几十微秒这个量级。
这里也提醒一下:网上有些教程还在用老式 txt 解析器去直接读 CZDB 文件,这大概率会失败或者得到乱码。拿到 .czdb 文件后,先用官方发布的 SDK 或新版开源解析库,不要沿用老代码。
2.3 和 GeoLite2 的 mmdb 设计理念对比
GeoLite2 的 mmdb 格式是一种基于树结构的数据库,数据按 IP 前缀逐位分支组织,查询时从根节点开始按 bit 走,最终落到包含数据的节点。这种设计的最大优势是支持任意 CIDR 前缀级别的精细查找,而且数据结构紧凑,查询速度非常快。
CZDB 的侧重点不太一样。它本质上维护的是 IP 段到详细文本的映射关系,更像一张大字典。对于按省、市、运营商这种固定维度查询的场景,它的存储效率很高,数据更新也简单。如果你只是做日志地域分析、风控归属判断、内容投放区域限制,CZDB 完全够用;但如果你想做非常细粒度的网络前缀分析,比如路由级别的归属判断,那 mmdb 生态里的配套工具和 ASN 库还是更顺手。
所以我的体会是,选择哪个格式,不该只看“谁更强”,而要看你的查询模型是不是“IP 段 -> 地域/运营商”这种简洁映射。是的话,CZDB 的简洁直接反而是一种优势。
3. Python 接入 CZDB 库的完整过程
3.1 环境准备:文件下载、校验和基本工程结构
我用的是 Python 3.10,主要依赖三个东西:读取 CZDB 的解析库、用于测试的 IP 生成脚本、以及一个简单的压测程序。
下载纯真社区版 CZDB 文件时,我建议从官方渠道拿,不要用不明来源的二次打包版本。下载完之后,先用发布页提供的 MD5 或者 SHA256 签名校验一下完整性,防止文件在传输过程中损坏。很多人在这一步偷懒,结果程序查出来的数据是乱的,还以为是解析库的问题。
我的目录结构大概是这样:
ip-location-test/ ├── data/ │ └── czdb # 下载的数据库文件 ├── scripts/ │ ├── download_db.py # 获取数据库和校验 │ └── worker.py # 查询服务或批量测试 └── tests/ ├── accuracy_test.py └── benchmark.py在实际项目里,建议把数据库文件和应用代码分开放,方便单独更新,避免误提交到代码仓库。数据文件动辄上百 MB,扔进 Git 里会很痛苦。
3.2 解析库的选择:官方 SDK 和开源库都试了一遍
纯真社区版官方提供了一套多语言 SDK,覆盖 Python、Java、PHP 等,逻辑上会跟随格式更新,这是最稳妥的选择。另外 GitHub 上也有一些个人维护的 Python 包,命名类似pyczdb、czdb-python之类,有的做得比较精简,只保留了核心查询函数。
我个人的建议是:线上服务尽量用官方 SDK,至少在格式更新时你能第一时间拿到适配版本。开源库适合快速原型验证,但如果作者停止维护,后续文件格式升级后你可能要自己改代码。
官方 SDK 的典型调用方式大致是这样的(不同版本函数名可能有差异,以源码为准):
from czdb import Searcher # 初始化数据库文件 searcher = Searcher("data/czdb") # 查询 IPv4 地址 result = searcher.search("114.114.114.114") print(result)返回结果通常是一个包含国家、省份、城市、运营商等字段的对象或字典,具体字段名因数据版本而异。我这边拿到的结果字段大致是country,province,city,isp。如果你的版本返回的是单个字符串,比如"中国|江苏|南京|电信",说明数据库可能是旧版格式或者 SDK 版本与文件不匹配,需要检查一下。如果方法名不对,很可能是因为新版本换了接口命名,优先去官方仓库查 readme。
3.3 性能测试方法:不能只看单次查询秒回
我写了一个压测脚本,随机生成 100 万个合法公网 IPv4 地址,然后逐个查询,统计总耗时、平均耗时、P95、P99 和最大耗时。测试机器是一台 4 核 8G 的普通云主机,数据库放本地 SSD。
测试代码如下:
import random import time from czdb import Searcher searcher = Searcher("data/czdb") ips = [f"{random.randint(1, 223)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(1, 254)}" for _ in range(1000000)] start = time.perf_counter() for ip in ips: searcher.search(ip) cost = time.perf_counter() - start print(f"total: {cost:.2f}s, avg: {cost / len(ips) * 1000:.2f}ms per query")在我这边实测,单线程下平均单次查询大约在 0.01 毫秒到 0.05 毫秒之间波动,100 万次全部跑完也就是十几秒到几十秒的量级。这个性能对日常日志分析已经足够。不过要注意,Python 的 GIL 会让纯 CPU 型查询没法真正利用多核,如果你的 QPS 特别高,建议把查询接口做成独立服务,用多进程部署,或者把查询逻辑下沉到 C 扩展。
3.4 返回字段和数据类型:自己踩过一次乱码的坑
我在初版脚本里图省事,直接按字符串方式处理返回结果,结果发现部分省份名称出现乱码,因为数据文件里混了不同的编码。后来检查官方 SDK 才发现,新版 SDK 会按 UTF-8 解码,但旧版数据里可能还有 GBK 编码的历史包袱。
如果你也遇到类似问题,可以尝试先手动读数据,再用decode("utf-8", errors="ignore")处理,或者升级 SDK 版本。更稳妥的做法是:数据库文件和 SDK 都从同一发布渠道同步更新,避免出现新版解析器配旧版文件的情况。
4. 同场竞技:CZDB 与 GeoLite2 的实测对比
4.1 准确率对比:我拿了 500 条真实访问 IP
为了不凭感觉说话,我从线上日志里抽了 500 条真实访问 IP,覆盖国内华东、华南、华北主要城市,再加少量海外 IP,逐个用两个库查询归属地,再人工核对运营商归属和城市是否正确。核对标准很简单:城市必须正确,运营商必须匹配宽带归属,否则记为错误。
结果大致如下:
| 对比项 | 纯真社区版 CZDB | GeoLite2 |
|---|---|---|
| 国内城市级准确率 | 约 92% | 约 85% |
| 国内运营商准确率 | 约 88% | 约 76% |
| 海外国家准确率 | 约 80% | 约 95% |
| 区县级数据覆盖 | 有较多区县信息 | 相对偏少 |
这个结果符合我的预期:纯真是国内老牌数据库,在国内区域的精细化程度上确实有优势;GeoLite2 的强项在海外,毕竟它的数据源和用户生态更偏全球化。如果你的业务主要是海外访问分析,那纯真未必是最好选择。
4.2 查询性能和内存占用的实测数据
性能层面,我在同一台机器、同一批 IP 上做了对比。GeoLite2 使用maxminddb库,纯真使用官方 SDK。
| 指标 | 纯真 CZDB | GeoLite2 mmdb |
|---|---|---|
| 数据库文件大小 | 约 80 MB | 约 60 MB |
| 进程内存占用 | 约 250 MB | 约 180 MB |
| 平均单次查询耗时 | 约 0.02 ms | 约 0.015 ms |
| P99 单次查询耗时 | 约 0.08 ms | 约 0.06 ms |
两者在性能上非常接近。CZDB 因为数据记录里包含更多国内区县和运营商文本,文件稍微大一点很正常。如果内存是你最大的瓶颈,可以研究一下是否能用 mmap 模式读取,减少进程常驻内存。
4.3 更新机制与文件分发:日常运维体感差别很大
GeoLite2 每月固定更新,需要登录账号并带上 License Key 下载。这个机制对个人项目问题不大,但对内网部署来说很麻烦,因为每次更新都得处理凭证和下载网络的连通性。
纯真社区版 CZDB 的更新方式更简洁:从发布渠道直接下载数据库文件即可,不需要账号体系。社区更新频率并不固定,数据变化大的时候更新就频繁一些。它带来的好处是运维链路短,适合自动化脚本定期拉取。要注意的是,每次更新前最好检查发布说明,因为如果格式版本升级,你的 SDK 也得同步升级,否则可能出现打不开文件的情况。
4.4 使用许可和合规边界:这部分比功能更重要
很多人只看功能,不看许可,这是个大坑。GeoLite2 免费版要求你在产品里显示归属声明,而且对商业使用有明确限制。纯真社区版也类似,免费版主要用于非商业用途;如果公司要拿去商用,需要联系官方获取授权方案。
我在这块的做法是:内部日志分析系统,不对外提供在线查询服务,不把数据库内容用于商业再分发,严格遵守各自的使用协议。如果需要对外提供 IP 查询 API 给第三方客户,建议购买商业授权或切换到其他授权更灵活的付费库,不要在免费许可证上打擦边球。
5. 迁移后踩过的坑,以及我的处理方案
5.1 最大坑:拿老解析器读新格式,差点让我误判整个方案
第一次接入时,我没细看格式说明,下意识在 PyPI 上找了一个平时常用的纯真 txt 解析库,直接把 .czdb 文件传进去。结果查询出来的省份全部错乱,有些甚至把国外 IP 解析到了国内。排查了很久,才发现是解析器跟文件格式根本不匹配。
后来我换了官方 SDK,问题立刻消失。这个坑提醒我:面对格式升级,第一件事永远是确认解析器版本和数据库文件版本配套。特别是从 txt 迁移到 CZDB 的场景,老代码里“按行读取”的思维必须完全抛弃。
5.2 IPv6 支持不像想象中那么完美
CZDB 虽然支持 IPv6,但我实测下来,IPv6 的数据覆盖量和准确率都不如 IPv4 丰富。如果线上 IPv6 流量占比较高,建议保留一份 GeoLite2 作为 IPv6 查询的补充。具体做法是先查 CZDB,如果结果为空或者明显异常,再回退到 GeoLite2。
5.3 批量查询时的线程安全问题
官方 SDK 的查询对象是否线程安全,不同版本表现不一致。我在多线程批量查询时,曾因为共享同一个 Searcher 对象出现过偶发返回空结果的问题。解决方式有两种:每个线程单独初始化一个查询对象,或者给查询函数加锁。加锁会损失一些并发性能,所以我最后选择了“每线程独立对象”的方案,内存多了一点,但行为稳定。
如果你的业务对内存非常敏感,可以把查询对象放进线程本地存储里,这样既能复用对象,又不会出现并发冲突。
5.4 定时更新脚本要避免“边读边写”
IP 库更新最怕出现项目进程刚好读到被覆盖的数据库文件,导致查询崩溃。我的做法是:先下载到临时目录,校验 MD5,然后用os.replace()原子替换正式文件。这样查询进程要么读到旧文件,要么读到新文件,绝对不会读到写了一半的内容。脚本里还要加一条重试逻辑,下载失败时不要影响线上服务,最多等下一个周期再更新。
5.5 缓存池能省掉一半以上的查询压力
日志分析里的 IP 往往高度重复,尤其是同一个用户的大量请求都来自同一 IP。我在查询层加了一个 LRU 缓存,key 是 IP 字符串,value 是解析结果,缓存容量设成 10 万条。实测下来,由于命中率高,整体查询 RT 又降了一个档次,数据库读取压力也小了很多。
这个优化特别适合访问日志重放、用户画像构建这种场景。唯一的注意点是缓存过期策略要和数据库更新节奏匹配,数据库更新后要能手动清掉缓存,否则会一直用旧数据跑。
我现在这个系统里,CZDB 作为国内主查库,GeoLite2 作为海外和 IPv6 补充库,两套并行跑了一个多月,整体稳定。如果你也想从 GeoLite2 迁移到国产免费 IP 库,建议先拿一批真实业务 IP 做一次准确率抽检,确认国内覆盖符合预期再切换。毕竟库好不好,终究要落到自己的数据和场景里才算数。