المشكلة
عند مراقبة أكثر من 100 خادم MariaDB / MySQL منتشرة عبر مراكز بيانات وبلدان متعددة، يظهر سؤال واحد في كل مرة تقوم فيها بعرض الصفحة الرئيسية: أين يوجد هذا الخادم؟
الإجابة الكلاسيكية: افتح ملف MaxMind GeoLite2 .mmdb، وقم بالبحث عن كل عنوان IP، واعرض علم الدولة. بسيطة...إلا أن:
- فتح ملف
.mmdbبحجم 70 ميجابايت مع كل طلب HTTP أمر مكلف - مع وجود 100 خادم يجب حلها، يعني ذلك 100 عملية بحث عن الملفات في كل صفحة
- الملف عبارة عن شجرة ثنائية محسنة للقراءة المتسلسلة، وليس للتقسيم المتوازي
- في PHP-FPM، يقوم كل عامل بإعادة تحميل الملف بشكل مستقل
في PmaControl، يتم تحديث صفحة server/main كل ثانية باستخدام AJAX. فتح .mmdb 100 مرة في الثانية هو هراء.
الحل: ضع كل شيء في MariaDB
الفكرة بسيطة: قم باستيراد جميع نطاقات GeoLite2 إلى جدول 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) إلى ثنائي قابل للمقارنة
طلب البحث
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 مللي ثانية.
يعمل نفس الاستعلام مع 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 (الولايات المتحدة — هذا هو DNS العام لـ Google).
الاستيراد: تكرار مساحة IP
IPv4: من 0.0.0.0 إلى 255.255.255.255
مساحة IPv4 هي 2^32 = 4.3 مليار عنوان. نحن لا نستعرضها واحدًا تلو الآخر — بل نستخدم getWithPrefixLen() من قارئ MaxMind الذي يُرجع CIDR الكامل لكل عنوان:
$ip = 0;
while ($ip <= 4294967295) {
[$record, $prefixLen] = $reader->getWithPrefixLen(long2ip($ip));
// Calculer la fin du réseau
$networkSize = 1 << (32 - $prefixLen);
$networkEnd = $ip + $networkSize - 1;
if ($record && !empty($record['country']['iso_code'])) {
// INSERT dans data_geoip
}
// Sauter tout le bloc CIDR
$ip = $networkEnd + 1;
}
النتيجة: ~650,000 نطاق تم استيرادها في ثوانٍ. يغطي كل نطاق كتلة CIDR بأكملها (على سبيل المثال: 89.30.104.0/22 → 1024 عنوانًا في سطر واحد).
IPv6: مساحة 2000::/3
مساحة IPv6 هي 2^128 عنوان - من المستحيل التنقل مثل IPv4. لكن العناوين العامة القابلة للتوجيه موجودة في كتلة 2000::/3 (البث الموحد العالمي)، وتكون تخصيصات 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 avec UNHEX(bin2hex(...))
}
$current = binIncrement($endBin); // +1 en arithmétique 128 bits
}
يتم تنفيذ الحساب الثنائي 128 بت بلغة PHP خالصة (لا يتطلب GMP):
// Incrémenter une adresse IPv6 de 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; // carry
}
return false; // overflow
}
النتيجة في الإنتاج
مجلدات
| الجدول | IPv4 | IPv6 | المجموع |
|---|---|---|---|
data_geoip (بلد) |
~650 ألف | ~180 ألف | نطاقات ~ 830 ألف |
data_geoip_city (مدينة) |
~3.7 م | ~1.2 م | ~4.9 مليون صف |
الأداء
| عملية | الوقت |
|---|---|
| بلد الاستيراد (IPv4 + IPv6) | ~30 ثانية |
| استيراد المدينة (IPv4 + IPv6) | ~5 دقائق |
| بحث 1 IP | <1 مللي ثانية |
| بحث عن 100 IP (خادم الصفحة/الرئيسي) | ~15 مللي ثانية إجمالي |
| تحديث AJAX (1x/ثانية) | لا يذكر |
العرض
في الصفحة الرئيسية لـ PmaControl، يعرض كل خادم علامة الرموز التعبيرية الخاصة به بجوار عنوان IP:
🇫🇷 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 كل أسبوع. للتحديث:
# Télécharger le nouveau .mmdb dans data/
# Puis relancer l'import :
php App/Webroot/index.php server loadGeoip # country (~30s)
php App/Webroot/index.php server loadGeoipCity # city (~5min)
يقوم الاستيراد بإجراء TRUNCATE ثم يعيد إدراج كل شيء. ليست هناك حاجة للاختلاف أو الترحيل — إنها ذاكرة تخزين مؤقت يمكن التخلص منها.
مدينة الطاولة: اذهب إلى أبعد من ذلك
يضيف الجدول 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 data_geoip g ON g.network_start <= INET6_ATON(s.ip) AND g.network_end >= INET6_ATON(s.ip) مباشرة في طلب الخادم. المشكلة: مع نطاقات تبلغ 650 ألفًا، يكون هذا الانضمام إلى النطاق مكلفًا بالنسبة للمُحسِّن. نحن نفضل عمليات البحث الفردية N (1 لكل عنوان IP فريد) والتي تكون فورية بفضل الفهرس.
الخلاصة
من خلال استيراد بيانات GeoLite2 إلى MariaDB، فإننا نزيل الاعتماد على ملف .mmdb في كل مرة يتم فيها عرض الصفحة. يصبح البحث SELECT مفهرسًا عند < 1 مللي ثانية، ومتوافقًا مع IPv4 وIPv6، ويكون التحديث أسبوعيًا بسيطًا TRUNCATE + INSERT.
كود المصدر متاح على GitHub - نرحب بالمساهمات.
تعليقات (0)
لا توجد تعليقات حتى الآن.
اترك تعليقا