PmaControl logo PmaControl
  • 首页
  • PmaControl
    • AI智能代理 13个本地代理
    • 定价方案 Community、Cloud、On-Premise、Premium
    • 文档 指南、API、架构
    • 客户 28+企业
    • 常见问题 25个问题 / 7个类别
    数据库
    • MariaDB 30 篇文章
    • MySQL 10 篇文章
    • Galera Cluster 6 篇文章
    • MaxScale 3 篇文章
    • ProxySQL 2 篇文章
    • Amazon Aurora MySQL 0 篇文章
    • Azure Database 0 篇文章
    • ClickHouse 0 篇文章
    • GCP CloudSQL 0 篇文章
    • Percona Server 0 篇文章
    • SingleStore 0 篇文章
    • TiDB 0 篇文章
    • Vitess 0 篇文章
    解决方案
    • 全天候支持 MariaDB & MySQL紧急支持
    • Observabilité SQL 监控、告警、拓扑
    • Haute disponibilité 复制、故障转移、Galera
    • Disaster Recovery 备份、恢复、RPO/RTO
    • Sécurité & conformité 审计、GDPR、SOC2
    • Migration & upgrade 零停机、pt-osc、gh-ost
  • 定价方案
  • 资源
    • 文档 技术指南与API
    • 常见问题 25个常见问题
    • 客户评价 客户反馈与案例
    • 博客 文章与洞察
    • 路线图 即将推出的功能
    专业领域
    • Observabilité SQL 监控、告警、Dot3拓扑
    • Haute disponibilité 复制、故障转移、Galera
    • Sécurité & conformité 审计、GDPR、SOC2、ISO 27001
    • Disaster Recovery 备份、恢复、RPO/RTO
    • Performance & optimisation Digests、EXPLAIN、调优
    • Migration & upgrade 零停机、pt-osc
    快速链接
    • GitHub Wiki 26页 — 安装、引擎、插件
    • 源代码 GitHub官方仓库
    • 全天候支持 MariaDB & MySQL紧急支持
    • 预约演示 30分钟 — 真实架构
  • 全天候支持
  • 预约演示
预约演示
🇫🇷 FR Français 🇬🇧 EN English 🇵🇱 PL Polski 🇷🇺 RU Русский 🇨🇳 ZH 中文
← 返回博客

PmaControl 中的 GeoIP:无需每次请求加载 mmdb 文件的 IPv4 和 IPv6 解析

发布于 2026年4月15日 作者 Aurélien LEQUOY
pmacontrol geoip ipv6 mariadb performance architecture
分享 X LinkedIn Facebook Email PDF
PmaControl 中的 GeoIP:无需每次请求加载 mmdb 文件的 IPv4 和 IPv6 解析

问题

当您监控 100+ 台分布在多个数据中心和国家的 MariaDB / MySQL 服务器时,每次显示主页面都会遇到一个问题:这台服务器在哪里?

经典答案:打开 MaxMind 的 GeoLite2 .mmdb 文件,对每个 IP 进行 lookup,然后显示国旗。看起来简单……但是:

  • 每次 HTTP 请求都打开一个 70 MB 的 .mmdb 文件开销很大
  • 需要解析 100 台服务器,就是每页 100 次文件 lookup
  • 该文件是为顺序读取优化的二叉树,不适合并发突发访问
  • 在 PHP-FPM 中,每个 worker 独立加载该文件

在 PmaControl 中,server/main 页面每秒通过 AJAX 刷新。每秒打开 .mmdb 文件 100 次是不合理的。

解决方案:全部放入 MariaDB

思路很简单:将 GeoLite2 的全部 ranges 导入一个 MariaDB 表,然后进行经典的 SQL 查询。一个带索引的 SELECT 比在磁盘上遍历二叉树要快得多。

表结构

CREATE TABLE data_geoip (
  id INT AUTO_INCREMENT PRIMARY KEY,
  network_start VARBINARY(16) NOT NULL,
  network_end   VARBINARY(16) NOT NULL,
  country_iso   CHAR(2) NOT NULL DEFAULT '',
  country_name  VARCHAR(100) NOT NULL DEFAULT '',
  INDEX idx_network_start (network_start)
) ENGINE=InnoDB;

选择 VARBINARY(16) 至关重要:

  • 4 字节足以存储 IPv4(32 位)
  • 16 字节用于 IPv6(128 位)
  • VARBINARY(16) 统一存储两种格式
  • INET6_ATON() 将任何 IP(v4 或 v6)转换为可比较的二进制格式

Lookup 查询

SELECT country_iso FROM data_geoip
WHERE network_start <= INET6_ATON('89.30.104.134')
  AND network_end   >= INET6_ATON('89.30.104.134')
LIMIT 1;

结果:FR(法国)。执行时间:< 1 ms。

同样的查询也适用于 IPv6:

SELECT country_iso FROM data_geoip
WHERE network_start <= INET6_ATON('2001:4860:4860::8888')
  AND network_end   >= INET6_ATON('2001:4860:4860::8888')
LIMIT 1;

结果:US(美国——这是 Google 的公共 DNS)。

导入:遍历 IP 空间

IPv4:从 0.0.0.0 到 255.255.255.255

IPv4 空间有 2^32 = 43 亿地址。我们不逐个遍历——而是使用 MaxMind reader 的 getWithPrefixLen(),它为每个地址返回完整的 CIDR:

$ip = 0;
while ($ip <= 4294967295) {
    [$record, $prefixLen] = $reader->getWithPrefixLen(long2ip($ip));

    // 计算网络结束位置
    $networkSize = 1 << (32 - $prefixLen);
    $networkEnd = $ip + $networkSize - 1;

    if ($record && !empty($record['country']['iso_code'])) {
        // INSERT 到 data_geoip
    }

    // 跳过整个 CIDR 块
    $ip = $networkEnd + 1;
}

结果:约 650,000 个 ranges 在几秒内导入。每个 range 覆盖一个完整的 CIDR 块(例如:89.30.104.0/22 -> 1024 个地址对应一行记录)。

IPv6:2000::/3 空间

IPv6 空间有 2^128 个地址——不可能像 IPv4 那样遍历。但公共可路由地址位于 2000::/3(Global Unicast)块中,GeoIP 分配通常为 /32 到 /48。

原理相同:按返回的前缀长度前进。区别在于:我们处理的是 16 字节的二进制地址。

$current = inet_pton('2000::');
$end6    = inet_pton('3fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff');

while ($current <= $end6) {
    [$record, $prefixLen] = $reader->getWithPrefixLen(inet_ntop($current));
    $endBin = binNetworkEnd($current, $prefixLen);

    if ($record && !empty($record['country']['iso_code'])) {
        // INSERT,使用 UNHEX(bin2hex(...))
    }

    $current = binIncrement($endBin); // 128 位算术 +1
}

128 位二进制算术用纯 PHP 实现(不需要 GMP):

// 将 IPv6 地址加 1
function binIncrement(string $bin): string|false
{
    $bytes = unpack('C16', $bin);
    for ($i = 15; $i >= 0; $i--) {
        $bytes[$i]++;
        if ($bytes[$i] <= 255) return pack('C16', ...$bytes);
        $bytes[$i] = 0; // 进位
    }
    return false; // 溢出
}

生产环境的结果

数据量

表 IPv4 IPv6 总计
data_geoip(国家) ~650K ~180K ~830K ranges
data_geoip_city(城市) ~3.7M ~1.2M ~4.9M ranges

性能

操作 时间
导入国家(IPv4 + IPv6) ~30 秒
导入城市(IPv4 + IPv6) ~5 分钟
查询 1 个 IP < 1 ms
查询 100 个 IP(server/main 页面) 总计 ~15 ms
AJAX 刷新(每秒一次) 可忽略不计

显示效果

在 PmaControl 的主页面上,每台服务器在 IP 旁边显示其国旗 emoji:

🇫🇷 89.30.104.134:3306    PIXID-MDB-MASTER1
🇩🇪 136.243.1.1:3306       Hetzner-Slave
🇯🇵 210.171.224.1:3306     NTT-Tokyo
🇺🇸 8.8.8.8:3306           Google-Test

私有 IP(10.x、172.16-31.x、192.168.x、127.x)没有 GeoLite2 记录——国旗直接不显示。

更新

MaxMind 每周更新 GeoLite2。刷新方法:

# 下载新的 .mmdb 到 data/
# 然后重新运行导入:
php App/Webroot/index.php server loadGeoip       # 国家(~30 秒)
php App/Webroot/index.php server loadGeoipCity   # 城市(~5 分钟)

导入先执行 TRUNCATE 然后重新插入所有数据。不需要 diff 或迁移——它是一个可丢弃的缓存。

城市表:更进一步

data_geoip_city 表增加了地区、城市、GPS 坐标和时区:

SELECT country_iso, region_name, city, latitude, longitude, time_zone
FROM data_geoip_city
WHERE network_start <= INET6_ATON('136.243.1.1')
  AND network_end   >= INET6_ATON('136.243.1.1')
LIMIT 1;

结果:DE | Saxony | Falkenstein | 50.4779 | 12.3713 | Europe/Berlin

这为服务器地图、跨数据中心延迟检测或简单地在界面中提供更丰富的显示打开了大门。

为什么不直接用 LEFT JOIN?

可能有人想直接在服务器查询中使用 LEFT JOIN data_geoip g ON g.network_start <= INET6_ATON(s.ip) AND g.network_end >= INET6_ATON(s.ip)。问题是:对于 650K 个 ranges,这个范围连接对优化器来说开销很大。我们更倾向于 N 个单独的 lookup(每个唯一 IP 一次),这些查询由于索引而是即时的。

总结

通过将 GeoLite2 数据导入 MariaDB,我们消除了每次页面渲染时对 .mmdb 文件的依赖。Lookup 变成了一个索引化的 SELECT,执行时间 < 1 ms,同时兼容 IPv4 和 IPv6,更新也只是每周一次简单的 TRUNCATE + INSERT。

源代码可在 GitHub 上获取——欢迎贡献。

分享 X LinkedIn Facebook Email PDF
← 返回博客

评论 (0)

暂无评论。

发表评论

PmaControl
+33 6 63 28 27 47 contact@pmacontrol.com
法律声明 GitHub 联系我们
不要等到故障发生才了解您的架构。 © 2014-2026 PmaControl — 68Koncept