مشكلة الذاكرة المشتركة في PHP
PmaControl هي أداة مراقبة مكتوبة بلغة PHP. تقوم برامجه الجانبية بجمع المقاييس من عشرات أو مئات من مثيلات MariaDB / MySQL، ثم تقوم بتخزين النتائج للوحة تحكم الويب.
ليس لدى PHP ذاكرة مشتركة أصلية بين العمليات (على عكس Go مع goroutines أو Java مع سلاسلها). كل عامل PHP هو عملية مستقلة. لمشاركة البيانات بين البرنامج الخفي للمجمع وخادم الويب، يستخدم PmaControl الملفات المحورية — وهو تنفيذ خاص للذاكرة المشتركة عبر نظام الملفات.
تقوم فئة StorageFile (المستوحاة من نمط SharedMemory) بتسلسل البيانات إلى JSON وكتابتها في الملفات الموجودة على القرص. تتم إدارة الوصول المتزامن بواسطة flock() باستخدام LOCK_EX (القفل الحصري).
LOCK_EX يعمل بشكل صحيح
السؤال الأول الذي قمنا بفحصه: هل يضمن flock() مع LOCK_EX الاستبعاد المتبادل حقًا؟ هل هناك خطر الكتابة الصامتة فوق البيانات؟
الجواب واضح: نعم، LOCK_EX يعمل بشكل صحيح. يضمن نواة Linux أن عملية واحدة فقط يمكنها الاحتفاظ بـ LOCK_EX على ملف في أي وقت محدد. تنتظر العمليات الأخرى (الحظر) حتى يتم تحرير القفل.
لقد تحققنا من خلال اختبار الإجهاد:
// Test de concurrence sur flock()
// Lancé avec 50 processus simultanés
$fp = fopen('/tmp/pivot_test.json', 'c+');
if (flock($fp, LOCK_EX)) {
$data = json_decode(fread($fp, filesize('/tmp/pivot_test.json')), true);
$data['counter'] = ($data['counter'] ?? 0) + 1;
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, json_encode($data));
flock($fp, LOCK_UN);
}
fclose($fp);
وبعد 10000 تكرار مع 50 عملية متزامنة، أصبح العداد 500000 بالضبط. لا يوجد فقدان للكتابة، ولا الكتابة الفوقية الصامتة.
المشكلة ليست في التصحيح هذا هو ضبط النفس .
عنق الزجاجة
يستخدم PmaControl الملفات المحورية لتخزين الحالة الفعلية للخوادم الخاضعة للمراقبة. يبدو الهيكل كما يلي:
/var/lib/pmacontrol/pivot/
server_status.json ← statut de TOUS les serveurs
server_42_metrics.json ← métriques détaillées du serveur 42
server_43_metrics.json
...
المشكلة هي ملف server_status.json. كل عامل خفي، بعد جمع المقاييس من الخادم، يقوم بتحديث هذا الملف المركزي بالحالة الجديدة. العملية:
- احصل على
LOCK_EXمنserver_status.json - قراءة المحتوى الكامل (JSON من جميع الخوادم)
- قم بتحرير الإدخال الخاص بالخادم المتأثر
- أعد كتابة الملف بأكمله
- قم بتحرير القفل
مع 10 خوادم وفاصل زمني للتجميع مدته 10 ثوانٍ، فإنه يعمل. مع وجود 100 خادم، يبدأ العمال في حظر بعضهم البعض أثناء انتظار القفل.
قياس ضبط النفس
لقد قمنا بتجهيز StorageFile لقياس وقت الانتظار على flock():
$start = microtime(true);
flock($fp, LOCK_EX);
$wait = microtime(true) - $start;
النتائج بأعداد مختلفة من الخوادم:
| عدد الخوادم | متوسط وقت الانتظار قطيع () | ص99 |
|---|---|---|
| 10 | 0.2 مللي ثانية | 1.1 مللي ثانية |
| 50 | 4.8 مللي ثانية | 28 مللي ثانية |
| 100 | 18 مللي ثانية | 142 مللي ثانية |
| 200 | 67 مللي ثانية | 480 مللي ثانية |
| 500 | 312 مللي ثانية | 1.8 ثانية |
بما يتجاوز 100 خادم، يتجاوز P99 100 مللي ثانية. في 500 خادم، ينتظر بعض العاملين حوالي ثانيتين لكتابة الحالة — خلال فاصل زمني للتجميع مدته 10 ثوانٍ. وهذا يمثل 20% من الوقت الذي تقضيه الميزانية في انتظار القفل.
لماذا ينمو الملف
يحتوي الملف server_status.json على حالة كافة الخوادم. في 100 خادم يبلغ حجمه حوالي 200 كيلو بايت. في 500 خادم، حوالي 1 ميغابايت.
كل تحديث:
- يقرأ 1 ميجابايت من JSON
- تحليل 1 ميجابايت في بنية PHP
- تعديل 2 كيلو بايت (خادم واحد)
- إجراء تسلسل بحجم 1 ميجابايت إلى JSON
- يكتب 1 ميجابايت على القرص
النسبة سخيفة: 2 كيلوبايت من البيانات المفيدة مقابل 4 ميغابايت من الإدخال/الإخراج.
الحل: التقسيم بواسطة server_id
التوصية هي تجزئة الملف المحوري حسب server_id:
/var/lib/pmacontrol/pivot/
status/
server_42.json ← 2 KB, un seul serveur
server_43.json
server_44.json
...
يحتاج كل عامل فقط إلى قفل ملف الخادم الخاص به. لا مزيد من الخلاف العام.
التأثير المُقاس
بعد التقسيم:
| عدد الخوادم | متوسط وقت الانتظار قطيع () | ص99 |
|---|---|---|
| 100 | 0.1 مللي ثانية | 0.5 مللي ثانية |
| 200 | 0.1 مللي ثانية | 0.6 مللي ثانية |
| 500 | 0.2 مللي ثانية | 0.8 مللي ثانية |
يختفي ضبط النفس بشكل شبه كامل. لم يعد وقت الانتظار يعتمد على عدد الخوادم ولكن على عدد العمال الذين يجمعون الخادم نفسه (عادةً 1).
الطرف المقابل
يجب أن تقرأ لوحة المعلومات الآن ملفات N بدلاً من ملف واحد فقط لعرض النظرة العامة. يذهب رمز القراءة من:
// Avant : un seul fichier
$allStatus = json_decode(file_get_contents('pivot/server_status.json'), true);
لديه :
// Après : N fichiers
$allStatus = [];
foreach (glob('pivot/status/server_*.json') as $file) {
$serverId = extractServerId($file);
$allStatus[$serverId] = json_decode(file_get_contents($file), true);
}
إنه المزيد من التعليمات البرمجية، ولكن القراءة غير محظورة بشكل طبيعي (لا حاجة إلى LOCK_EX للقراءة بفضل الكتابة الذرية عبر rename()).
ما وراء نظام الملفات: Redis وmemcached
بالنسبة لعمليات النشر الكبيرة (أكثر من 500 خادم)، يصل أسلوب نظام الملفات إلى حدوده حتى مع التقسيم:
- زمن وصول الإدخال/الإخراج: كل عملية كتابة تلامس القرص (باستثناء ذاكرة التخزين المؤقت للصفحة Linux)
- ضغط Inode: 500 ملف محوري = 500 inode
- لا يوجد TTL: تظل الملفات المحورية للخادم المحذوفة حتى التنظيف اليدوي
الخطوة الطبيعية التالية هي استبدال StorageFile بالواجهة الخلفية Redis أو memcached:
// Interface abstraite
interface StorageBackend {
public function get(string $key): ?array;
public function set(string $key, array $data, int $ttl = 0): void;
}
// Implémentation fichier (actuelle)
class StorageFile implements StorageBackend { ... }
// Implémentation Redis (future)
class StorageRedis implements StorageBackend { ... }
يعمل Redis على التخلص من مشاكل التنافس (العمليات الذرية من جانب الخادم)، وTTL (انتهاء الصلاحية الأصلي)، ومشكلات الأداء (كل ذلك في الذاكرة).
لماذا لا تنتقل مباشرة إلى Redis
تم تصميم PmaControl ليكون سهل التثبيت: لا توجد تبعيات خارجية، أو خادم PHP واحد، أو Redis أو RabbitMQ. يسمح أسلوب الملف المحوري بالتثبيت على الحد الأدنى Debian دون متطلبات مسبقة.
إن إضافة Redis كتبعية إلزامية من شأنه أن يكسر هذه الفلسفة. الحل الذي تم اختياره هو الاحتفاظ بـ StorageFile كواجهة خلفية افتراضية (مع التقسيم) وتقديم StorageRedis كخيار لعمليات النشر الكبيرة.
ملخص التوصيات
| الحجم | توصية | الخلفية |
|---|---|---|
| 1-50 خادم | ملف محوري واحد | ملف التخزين |
| 50-200 خادم | المشاركة بواسطة server_id | ملف التخزين (مجزأ) |
| 200-500 خادم | مشاركة + SSD سريع | ملف التخزين (مجزأ) |
| أكثر من 500 خادم | ريديس/memcached | تخزين ريديس |
الخلاصة
flock() مع LOCK_EX يعمل بشكل جيد - لا توجد كتابة فوقية صامتة. لكن التنافس على ملف محوري مشترك بين جميع العاملين يمثل مشكلة حقيقية تتجاوز 100 خادم.
الحل هو التقسيم بواسطة server_id: يقوم كل عامل بتأمين ملفه الخاص، مما يؤدي إلى القضاء على التنافس العالمي. بالنسبة لعمليات النشر الكبيرة جدًا، يتولى Redis المسؤولية.
نظام الملفات ليس خيارًا سيئًا للذاكرة المشتركة في PHP. عليك فقط أن تعرف متى تصل إلى حدودها.
تعليقات (0)
لا توجد تعليقات حتى الآن.
اترك تعليقا