تطبيق PHP الكلاسيكي مترابط التحميل الثقيل (ما لم تكن بالطبع تكتب على الإطارات المصغرة) والموت الذي لا مفر منه للعملية بعد كل طلب ... مثل هذا التطبيق ثقيل وبطيء ، ولكن يمكننا إعطائه حياة ثانية عن طريق التهجين. لتسريع - نحن شيطان وتحسين تسرب الذاكرة لتحقيق أداء أفضل - سوف نقدم خادم تطبيق Golang RoadRunner PHP الخاص بنا لإضافة مرونة - تبسيط كود PHP ، توسيع المكدس ومشاركة المسؤولية بين الخادم والتطبيق. في جوهرها ، سوف نجعل تطبيقنا يعمل كما لو كنا نكتبه بلغة جافا أو بلغة أخرى.
بفضل التهجين ، توقف التطبيق البطيء سابقًا عن معاناة 502 من الأخطاء تحت التحميل ، انخفض متوسط وقت الاستجابة للطلبات ، وزادت الإنتاجية ، وأصبح النشر والتجميع أسهل بسبب توحيد التطبيق والتخلص من الارتباطات غير الضرورية في شكل nginx + php-fpm.
أنطون تيتوف (
Lachezis ) هو CTO والمؤسس المشارك لـ SpiralScout LLC مع 12 عامًا من الخبرة النشطة في مجال التطوير التجاري في PHP. خلال السنوات القليلة الماضية ، كان يعمل بنشاط على تطبيق Golang على حزمة تطوير الشركة. تحدث انطون عن مثال واحد في
PHP Russia 2019 .
دورة حياة تطبيق PHP
من الناحية التخطيطية ، يبدو جهاز التطبيق المجرد مع إطار عمل معين هكذا.

عندما نرسل طلبًا لعملية ما ، يحدث ذلك:
- تهيئة المشروع ؛
- تحميل المكتبات المشتركة ، والأطر ، و ORMs ؛
- تحميل المكتبات المطلوبة لمشروع معين ؛
- التوجيه.
- طلب التوجيه إلى وحدة تحكم محددة ؛
- توليد استجابة.
هذا هو مبدأ تشغيل تطبيق
أحادي الخيوط كلاسيكي مع نقطة دخول واحدة ، والتي بعد كل تنفيذ يتم إتلافها بالكامل أو مسح حالتها. يتم إلغاء تحميل جميع الأكواد من الذاكرة أو مسح العامل أو إعادة ضبط حالته.
كسول التحميل
تتمثل الطريقة القياسية والسهلة للإسراع في تنفيذ نظام
Lazy-loading أو مكتبات عند الطلب.

مع كسول التحميل نطلب فقط رمز ضروري.
عند الوصول إلى وحدة تحكم معينة ، سيتم تحميل المكتبات الضرورية فقط في الذاكرة ومعالجتها ثم إلغاء تحميلها. يتيح لك ذلك
تقليل متوسط وقت استجابة المشروع وتسهيل عملية العمل على الخادم بشكل كبير. في جميع الأطر التي نستخدمها حاليًا ، يتم تطبيق مبدأ التحميل البطيء.
مخبأ الحسابات المتكررة
الطريقة أكثر تعقيدًا وتستخدم بنشاط ، على سبيل المثال ، في إطار Symfony ومحركات القوالب وأنظمة ORM والتوجيه. هذا ليس التخزين المؤقت مثل memcached أو Redis لبيانات المستخدم.
يسخن هذا النظام
أجزاء من الكود مسبقًا . بناءً على الطلب الأول ، يقوم النظام بإنشاء رمز أو ملف ذاكرة تخزين مؤقت ، وبناءً على الطلبات اللاحقة ، لن يتم تنفيذ هذه العمليات الحسابية الضرورية ، على سبيل المثال ، لتجميع قالب.

التخزين المؤقت
يسرع التطبيق بشكل كبير ، ولكن في نفس الوقت
يعقده . على سبيل المثال ، هناك مشاكل في إبطال ذاكرة التخزين المؤقت وتحديث التطبيق. لا تخلط بين ذاكرة التخزين المؤقت للمستخدم وذاكرة التخزين المؤقت للتطبيق - في واحدة ، تتغير البيانات مع مرور الوقت ، في الآخر فقط عند تحديث الكود.
طلب المعالجة
عند تلقي طلب من خادم PHP-FPM خارجي ، سوف تتطابق نقطة إدخال الطلب والتهيئة.
اتضح أن طلب العميل هو حالة عمليتنا.
الطريقة الوحيدة لتغيير هذه الحالة هي تدمير العامل بالكامل والبدء من جديد بطلب جديد.

هذا هو نموذج كلاسيكي الخيوط مع مزاياها.
- يموت جميع العمال في نهاية الطلب.
- تسرب الذاكرة ، حالة السباق ، حالة توقف تام ليست متأصلة في PHP. لا يمكنك أن تقلق بشأن ذلك.
- الرمز بسيط: نكتب ونعالج الطلب ونموت وننتقل.
من ناحية أخرى ، لكل طلب ، نقوم بتحميل الإطار بالكامل ، وجميع المكتبات ، ونجري بعض الحسابات ، ونعيد ترجمة القوالب. مع كل طلب في دائرة ننتج الكثير من التلاعب والعمل غير الضروري.
كيف يعمل على الخادم
على الأرجح ، ستعمل مجموعة من nginx و PHP. سيعمل Nginx كوكيل عكسي: امنح المستخدمين جزءًا من الإحصائيات ، وقم بتفويض جزء من الطلبات إلى مدير عمليات PHP PHP-FPM أدناه. بالفعل يقوم المدير برفع عامل منفصل للطلب ومعالجته. بعد ذلك ، يتم تدمير العامل أو تطهيره. بعد ذلك ، يتم إنشاء عامل جديد للطلب التالي.

مثل هذا النموذج يعمل بشكل مستقر - التطبيق يكاد يكون من المستحيل قتل. ولكن في ظل الأحمال الثقيلة ، يؤثر مقدار العمل لتهيئة وتدمير العمال على أداء النظام ، لأنه حتى بالنسبة لطلب GET البسيط ، غالبًا ما يتعين علينا سحب مجموعة من التبعيات وإعادة رفع اتصال قاعدة البيانات.
تسريع التطبيق
كيفية تسريع التطبيق الكلاسيكي بعد إدخال ذاكرة التخزين المؤقت وتحميل كسول؟ ما هي الخيارات الأخرى هناك؟
أنتقل إلى اللغة نفسها .
- استخدم OPCache. أعتقد أن لا أحد يقوم بتشغيل PHP على الإنتاج دون تمكين OPCache؟
- انتظر RFC: التحميل المسبق . يسمح لك بتحميل مجموعة من الملفات مسبقًا إلى جهاز افتراضي.
- JIT - يسرع بشكل خطير التطبيق على المهام المرتبطة وحدة المعالجة المركزية. لسوء الحظ ، مع المهام المتعلقة قواعد البيانات ، فإنه لن يساعد كثيرا.
استخدام البدائل . على سبيل المثال ، الجهاز الظاهري HHVM من الفيسبوك. ينفذ التعليمات البرمجية في بيئة أكثر الأمثل. لسوء الحظ ، HHVM غير متوافق تمامًا مع بناء جملة PHP. كبديل ، يعد برنامج التحويل البرمجي kPHP من VK أو PeachPie ، والذي يحول التعليمات البرمجية بالكامل إلى .NET C # ، بديلاً.
أعد الكتابة بالكامل إلى لغة أخرى. هذا خيار جذري - تخلص تمامًا من تحميل الكود بين الطلبات.
يمكنك
تخزين حالة التطبيق بالكامل
في الذاكرة ، واستخدام هذه الذاكرة بنشاط في العمل ، ونسيان مفهوم العامل المحتضر ومسح التطبيق بالكامل بين الطلبات.
ولتحقيق ذلك ، نقوم بنقل نقطة الدخول ، التي اعتادت أن تكون مع نقطة التهيئة ، في عمق التطبيق.
نقل نقطة الدخول - شيطنة
هذا يخلق حلقة لا نهائية في التطبيق: الطلب الوارد - تشغيله من خلال الإطار - يولد استجابة للمستخدم. يعد هذا توفيرًا خطيرًا - حيث يتم إجراء جميع عمليات التمهيد ، ويتم إجراء كل تهيئة للإطار مرة واحدة فقط ، ثم تتم معالجة العديد من الطلبات بواسطة التطبيق.

نحن تكييف التطبيق
ومن المثير للاهتمام ، يمكننا التركيز على تحسين هذا الجزء فقط من التطبيق الذي سيتم تشغيله
في وقت التشغيل : وحدات التحكم ، والمنطق التجاري. في هذه الحالة ، يمكنك التخلي عن طراز كسل التحميل. سنأخذ جزءًا من مشروع bootstrapping إلى البداية - في وقت التهيئة. العمليات الحسابية الأولية: يؤدي التوجيه ، والقوالب ، والإعدادات ، وأنظمة ORM إلى تضخيم التهيئة ، ولكنها في المستقبل ستوفر وقت المعالجة لطلب واحد محدد.

لا أوصي بتجميع القوالب عند تنزيل عامل ، ولكن تنزيله ، على سبيل المثال ، كل التكوينات مفيدة.
مقارنة النماذج
قارن بين الشياطين والنماذج الكلاسيكية.

النموذج الشيطاني من اللحظة التي تم فيها إنشاء العملية حتى اللحظة التي يتم فيها رد الاستجابة للمستخدم يستغرق وقتًا أطول. تم تحسين التطبيق الكلاسيكي من أجل الإنشاء السريع والتجهيز والتدمير.
ومع ذلك ، فإن جميع الطلبات اللاحقة بعد الاحماء رمز أسرع بكثير. الإطار والتطبيق والحاوية موجود بالفعل في الذاكرة ومستعد لقبول الطلبات والاستجابة بسرعة.
مشاكل النموذج طويل العمر
على الرغم من المزايا ، يحتوي النموذج على مجموعة من القيود.
تسرب الذاكرة. يكمن التطبيق في الذاكرة لفترة طويلة جدًا ، وإذا كنت تستخدم "منحنيات" المكتبة أو التبعيات الخاطئة أو الحالات العامة - ستبدأ الذاكرة في التسرب. في مرحلة ما ، سيظهر خطأ فادح سينهي طلب المستخدم.
يتم حل المشكلة بطريقتين.
- اكتب رمزًا دقيقًا واستخدم المكتبات التي أثبتت جدواها.
- مراقبة بنشاط العمال. إذا كنت تشك في أن الذاكرة تتسرب داخل العملية ، فقم بتغييرها بشكل استباقي إلى تمثيلي مع حد أدنى ، أي ببساطة إلى نسخة جديدة لم تتمكن بعد من تجميع ذاكرة غير نظيفة.
تسرب البيانات . على سبيل المثال ، إذا قمنا خلال طلب وارد بحفظ المستخدم الحالي للنظام في بعض المتغيرات العامة وننسى إعادة تعيين هذا المتغير بعد الطلب ، فهناك احتمال أن يتمكن المستخدم التالي للنظام من الوصول بطريق الخطأ إلى البيانات التي لا يجب أن يراها.
يتم حل المشكلة على مستوى بنية التطبيق.
- لا تقم بتخزين مستخدم نشط في سياق عمومي. يتم تجاهل جميع البيانات الخاصة بسياق الطلب ومسحها قبل الطلب التالي.
- التعامل مع بيانات الجلسة بعناية. جلسات في PHP - مع النهج الكلاسيكي ، وهذا هو كائن عالمي. لفه بشكل صحيح بحيث يتم إعادة تعيينه بناءً على طلب لاحق.
إدارة الموارد .
- مراقبة الاتصالات إلى قاعدة البيانات. إذا تم تعليق التطبيق في الذاكرة لمدة شهر أو شهرين ، فسيتم إغلاق الاتصال المفتوح على الأرجح خلال هذا الوقت: ستتم إعادة تثبيت قاعدة البيانات أو إعادة تشغيلها أو سيعيد جدار الحماية إعادة تعيين الاتصال. على مستوى الرمز ، فكّر في إعادة الاتصال ، أو بعد كل طلب ، إعادة تعيين الاتصال وإعادة زيادته عند الطلب التالي.
- تجنب قفل ملف طويل الأجل. إذا كان العامل الخاص بك يكتب بعض المعلومات إلى ملف ، فلا توجد مشكلة. ولكن إذا كان هذا الملف مفتوحًا وقفلًا عليه ، فلن تتمكن أي عملية أخرى في نظامك من الوصول إليه حتى يتم إصدار القفل.
استكشف النموذج طويل العمر
النظر في نموذج عامل طويل العمر - شيطنة تطبيق - واستكشاف طرق لتنفيذه.
نهج غير مانع
نحن نستخدم PHP غير المتزامن - نقوم بتحميل التطبيق مرة واحدة في الذاكرة ومعالجة طلبات HTTP الواردة داخل التطبيق. الآن
التطبيق والخادم هي عملية واحدة . عندما يصل الطلب ، نقوم بإنشاء coroutine منفصل أو في حلقة الحدث نعطي وعدًا ونعالجه ونعطيه للمستخدم.

ميزة لا يمكن إنكارها من النهج هو الحد الأقصى للأداء. من الممكن أيضًا استخدام أدوات مثيرة للاهتمام ، على سبيل المثال ،
تكوين WebSocket مباشرة على التطبيق الخاص بك .
ومع ذلك ، فإن النهج
يزيد بشكل كبير
من تعقيد التنمية . من الضروري تثبيت ELDO ، تذكر أنه لن يتم دعم جميع برامج تشغيل قاعدة البيانات ، وأن مكتبة PDO مستبعدة.
لحل المشاكل في حالة شيطنة مع نهج غير حظر ، يمكنك استخدام أدوات معروفة:
ReactPHP ،
amphp و
Swoole - تطور مثير للاهتمام في شكل امتداد C. تعمل هذه الأدوات بسرعة ، ولديها مجتمع جيد وتوثيق جيد.
حجب النهج
نحن لا نرفع coroutines داخل التطبيق ، ولكن نفعل ذلك من الخارج.

نحن فقط
نلتقط بعض عمليات التطبيق ، كما تفعل PHP-FPM. بدلاً من إرسال هذه الطلبات في شكل حالة عملية ، نقوم بتسليمها من الخارج في شكل بروتوكول أو مراسلة.
نكتب نفس
الكود المفرد المترابط الذي نعرفه ، ونستخدم جميع المكتبات نفسها ونفس PDO. كل العمل الشاق من العمل مع مآخذ التوصيل ، HTTP ، وغيرها من الأدوات يتم
خارج تطبيق PHP .
من السلبيات: يجب أن
نراقب الذاكرة ونتذكر أن
التواصل بين عمليتين مختلفتين ليس مجانيًا ، لكننا بحاجة إلى نقل البيانات. هذا سيخلق النفقات العامة طفيفة.
لحل المشكلة ، يوجد بالفعل أداة
PHP-RM مكتوبة بلغة PHP. في مكتبة ReactPHP ،
يتكامل مع عدة أطر . ومع ذلك ، فإن PHP-PM
بطيئة للغاية
، فهي تسرب الذاكرة على مستوى الخادم وتحت الحمل لا تظهر نموًا كبيرًا مثل PHP-FRM.
نكتب خادم التطبيق لدينا
لقد كتبنا
خادم التطبيق لدينا ، والذي يشبه PHP-RM ، ولكن هناك المزيد من الوظائف. ماذا نريد من الخادم؟
مع الجمع بين الأطر القائمة. نود أن يكون هناك تكامل مرن مع جميع الأطر تقريبًا في السوق. لا أشعر بأنني أكتب أداة تعمل فقط في حالة معينة.
عمليات مختلفة للخادم والتطبيق . إمكانية إعادة التشغيل السريع ، بحيث عند التطوير محليًا ، اضغط على F5 وانظر الرمز المحدث الجديد ، وكذلك تكون قادرًا على توسيعها بشكل فردي.
سرعة عالية والاستقرار . لا يزال ، نحن نكتب خادم HTTP.
سهولة التمدد . لا نريد استخدام الخادم كخادم HTTP فقط ، ولكن أيضًا للسيناريوهات الفردية مثل خادم قائمة الانتظار أو خادم gRPC.
العمل خارج الصندوق كلما كان ذلك ممكنًا: Windows ، Linux ، ARM CPU.
القدرة على كتابة
ملحقات متعددة الخيوط سريعة جدا محددة لتطبيقنا.
كما فهمت بالفعل ، سوف نكتب باللغة Golang.
خادم رود رنر
لإنشاء خادم PHP ، تحتاج إلى حل 4 مشاكل رئيسية:
- إقامة اتصال بين عمليات Golang و PHP.
- إدارة العملية: إنشاء وتدمير ومراقبة العمال.
- تحقيق التوازن بين المهام - توزيع المهام بكفاءة على العمال. نظرًا لأننا نطبق نظامًا يحظر العامل الفردي لمهمة واردة معينة محددة ، فمن المهم إنشاء نظام من شأنه أن يقول بسرعة إن العملية قد انتهت من العمل ومستعدة لقبول المهمة التالية.
- مكدس HTTP - إرسال بيانات طلب HTTP إلى العامل. إنها مهمة بسيطة لكتابة نقطة واردة يقوم المستخدم بإرسال طلب إليها ، والتي يتم تمريرها إلى PHP وإعادتها.
متغيرات التفاعل بين العمليات
أولاً ، دعنا نحل مشكلة التواصل بين عمليات Golang و PHP. لدينا عدة طرق.
التضمين: تضمين مترجم PHP مباشرة في Golang. هذا ممكن ، لكنه يتطلب تجميع PHP مخصص ، تكوين معقد ، وعملية مشتركة للخادم و PHP. كما هو الحال في
go-php ، على سبيل المثال ، حيث يتم دمج مترجم PHP في Golang.
الذاكرة المشتركة - استخدام مساحة الذاكرة المشتركة ، حيث تشترك العمليات في هذه المساحة . يستغرق العمل المضني. عند تبادل البيانات ، يجب عليك مزامنة الحالة يدويًا ومقدار الأخطاء التي قد تحدث كبيرًا جدًا. تعتمد الذاكرة المشتركة أيضًا على نظام التشغيل.
كتابة بروتوكول النقل الخاص بك - Goridge
لقد سلكنا طريقًا بسيطًا يُستخدم في جميع الحلول تقريبًا على أنظمة Linux - استخدمنا بروتوكول النقل. هو
مكتوب على قمة الأنابيب القياسية و UNIX / TCP SOCKETS .
لديه القدرة على نقل البيانات في كلا الاتجاهين ، واكتشاف الأخطاء ، وكذلك وضع علامات على الطلبات ووضع الرؤوس عليها. فارق بسيط بالنسبة لنا هو القدرة على تنفيذ البروتوكول دون تبعية على جانب PHP و Golang - دون امتداد C بلغة خالصة.
كما هو الحال مع أي بروتوكول ، الأساس هو حزمة بيانات. في حالتنا ، تحتوي الحزمة على رأس ثابت يبلغ 17 بايت.

يتم تخصيص البايت الأول لتحديد نوع الحزمة. يمكن أن يكون هذا دفقًا أو علامة تشير إلى نوع تسلسل البيانات. ثم نقوم بتعبئة حجم البيانات مرتين في Little Endian و Big Endian. نحن نستخدم هذا الإرث للكشف عن أخطاء الإرسال. إذا رأينا أن حجم البيانات المحزومة في طلبين مختلفين لا يتطابق ، فعلى الأرجح حدث خطأ في نقل البيانات. ثم يتم نقل البيانات.

في الإصدار الثالث من الحزمة ، سنتخلص من هذا الإرث ، ونقدم مقاربة أكثر كلاسيكية مع اختباري ، ونضيف أيضًا القدرة على استخدام هذا البروتوكول مع عمليات PHP غير المتزامنة.
لتنفيذ البروتوكول في Golang و PHP ، استخدمنا الأدوات القياسية.
في Golang: ترميز / مكتبات ثنائية ومكتبات io و net للعمل مع الأنابيب القياسية ومآخذ UNIX / TCP.
في PHP: الوظيفة المألوفة للعمل مع حزمة / فك حزم البيانات الثنائية وتيارات التمديدات والمقابس للأنابيب والمقابس.
نشأت
آثار جانبية مثيرة للاهتمام أثناء التنفيذ. لقد قمنا بدمجها مع مكتبة Golang net / rpc القياسية ، والتي تسمح لنا بالاتصال برمز الخدمة من Golang مباشرة في التطبيق.
نكتب الخدمة:
مع كمية صغيرة من التعليمات البرمجية ، نسميها من التطبيق:
<?php use Spiral\Goridge; require "vendor/autoload.php"; $rpc = new Goridge\RPC( new Goridge\SocketRelay("127.0.0.1", 6001) ); echo $rpc->call("App.Hi", "Antony");
مدير عمليات PHP
الجزء التالي من الخادم هو إدارة عمال PHP.

العامل هو عملية PHP نلاحظها باستمرار من Golang. نحن نجمع سجل أخطاءه في ملف STDERR ، ونتواصل مع العامل عبر بروتوكول نقل Goridge ، ونجمع إحصائيات حول استهلاك الذاكرة ، وتنفيذ المهام ، والحظر.
التنفيذ بسيط - هذه هي الوظيفة القياسية لنظام التشغيل / exec ، وقت التشغيل ، المزامنة ، الذرية. لخلق العمال نستخدم
مصنع العمال .

لماذا مصنع العمال؟ لأننا نريد التواصل على حد سواء على الأنابيب القياسية وعلى المقابس. في هذه الحالة ، تكون عملية التهيئة مختلفة قليلاً. عند إنشاء عامل يتواصل عن طريق الأنابيب ، يمكننا إنشاءه على الفور وإرسال البيانات مباشرة. في حالة المقابس ، تحتاج إلى إنشاء عامل ، والانتظار حتى يصل إلى النظام ، وإجراء مصافحة PID ، وبعد ذلك فقط يستمر العمل.
موازن المهام
الجزء الثالث من الخادم هو الأكثر أهمية بالنسبة للأداء.
للتنفيذ ، نستخدم وظيفة Golang القياسية -
قناة مخزنة مؤقتًا . على وجه الخصوص ، نقوم بإنشاء العديد من العمال ووضعهم في هذه القناة كومة LIFO.

عند استلام المهام من المستخدم ، نرسل طلبًا إلى مكدس LIFO ونطلب إصدار أول عامل حر. إذا كان العامل لا يمكن تخصيصه لفترة معينة من الوقت ، فإن المستخدم يتلقى خطأ من النوع "خطأ المهلة". إذا تم تخصيص العامل - يحصل من المكدس ، يتم حظره ، وبعد ذلك يتلقى المهمة من المستخدم.

بعد معالجة المهمة ، يتم إرجاع الاستجابة للمستخدم ، ويكون العامل في نهاية المكدس. إنه مستعد لأداء المهمة التالية مرة أخرى.

في حالة حدوث خطأ ، سيتلقى المستخدم خطأ ، حيث سيتم تدمير العامل. نطلب من Worker Pool and Worker Factory إنشاء عملية مماثلة واستبدالها في الحزمة. يسمح هذا للنظام بالعمل حتى في حالة حدوث أخطاء قاتلة ببساطة عن طريق إعادة إنشاء العمال عن طريق القياس باستخدام PHP-FPM.

نتيجة لذلك ، اتضح أنه تم تطبيق نظام صغير يعمل بسرعة كبيرة -
200 نانو ثانية لتخصيص العمال . إنها قادرة على العمل حتى في حالة وجود أخطاء قاتلة. كل عامل في وقت واحد يعالج مهمة واحدة فقط ، والتي تتيح لنا استخدام
نهج الحجب الكلاسيكي .
رصد استباقي
جزء منفصل من كل من مدير العملية وموازن المهام هو نظام المراقبة الاستباقي.

هذا هو النظام الذي يقوم مرة واحدة في استطلاعات الرأي برصد العمال ومراقبة المؤشرات: إنه يبحث في مقدار الذاكرة التي يستهلكونها ، ومقدارها ، وما إذا كانوا في وضع الخمول. بالإضافة إلى التتبع ، يراقب النظام تسرب الذاكرة. إذا تجاوز العامل حدًا معينًا ، فسنراه ونقوم بإزالته بعناية من النظام قبل حدوث تسرب قاتل.
كومة HTTP
الجزء الأخير والبسيط.
كيف يتم تنفيذها:- يثير نقطة HTTP على جانب Golang ؛
- نتلقى طلب
- تحويل إلى تنسيق PSR-7 ؛
- إرسال الطلب إلى أول عامل حر ؛
- فك الطلب في كائن PSR-7 ؛
- نحن نعالج
- نحن نولد الجواب.
للتنفيذ ، استخدمنا
مكتبة Golang NET / HTTP القياسية . هذه مكتبة مشهورة تحتوي على العديد من الملحقات. قادرة على العمل على حد سواء HTTPS وعبر بروتوكول HTTP / 2.
على جانب PHP ، استخدمنا معيار PSR-7
. إنه
إطار مستقل مع العديد من الامتدادات و Middlewares. تعد PSR-7
ثابتة في التصميم ، والتي تتوافق بشكل جيد مع مفهوم التطبيقات طويلة العمر وتتجنب أخطاء الاستعلام العالمية.
يتشابه كلا الهيكلين في كل من Golang و PSR-7 ، مما يوفر وقتًا كبيرًا لتعيين طلب من لغة إلى أخرى.
لبدء الخادم يتطلب
الحد الأدنى من الربط :
http: address: 0.0.0.0:8080 workers: command: "php psr-worker.php" pool: numWorkers: 4
علاوة على ذلك ، من الإصدار 1.3.0 يمكن حذف الجزء الأخير من التكوين.
قم بتنزيل الملف الثنائي للخادم ، ضعه في حاوية Docker أو في مجلد المشروع. بدلاً من ذلك ، على مستوى العالم ، نكتب ملف تكوين صغيرًا يصف الجزء الذي سنستمع إليه ، ونوع العامل هو نقطة الدخول ، وكم هو مطلوب.
على جانب PHP ، نكتب حلقة أساسية تستقبل طلب PSR-7 ، ونقوم بمعالجتها ، وتُرجع استجابة أو خطأ إلى الخادم.
while ($req = $psr7->acceptRequest()) { try { $resp = new \Zend\Diactoros\Response(); $resp->getBody()->write("hello world"); $psr7->respond($resp); } catch (\Throwable $e) { $psr7->getWorker()->error((string)$e); } }
الجمعية. لتنفيذ الخادم ، اخترنا بنية مع نهج المكون. هذا يجعل من الممكن تجميع الخادم لتلبية احتياجات المشروع ، إضافة أو إزالة القطع الفردية حسب متطلبات التطبيق.
func main() { rr.Container.Register(env.ID, &env.Service{}) rr.Container.Register(rpc.ID, &rpc.Service{}) rr.Container.Register(http.ID, &http.Service{}) rr.Container.Register(static.ID, &static.Service{}) rr.Container.Register(limit.ID, &limit.Service{}
استخدام الحالات
النظر في خيارات لاستخدام الخادم وتعديل الهيكل. للبدء ، فكر في خط الأنابيب الكلاسيكي - عمل الخادم مع الطلبات.
نمطية
يستقبل الخادم الطلب إلى نقطة HTTP ويمرره عبر مجموعة من البرامج الوسيطة ، المكتوبة بلغة Golang. يتم تحويل الطلب الوارد إلى مهمة يفهمها العامل. يعطي الخادم المهمة للعامل ويعيدها مرة أخرى.

في الوقت نفسه ، يقوم العامل ، باستخدام بروتوكول Goridge ، بالاتصال بالخادم ومراقبة حالته ونقل البيانات إليه.
الوسيطة على Golang: إذن
هذا هو أول شيء فعله. في طلبنا ، كتبنا الوسيطة
لتخويل المستخدم عن طريق رمز JWT . تتم كتابة الوسيطة بالطريقة نفسها لأي ترخيص آخر. تطبيق بسيط للغاية وبسيط هو كتابة معدل محدد أو قواطع دوائر.
التفويض سريع . إذا كان الطلب غير صالح - فقط لا ترسله إلى تطبيق PHP ولا تضيع الموارد في معالجة المهام عديمة الفائدة.
مراقبة
حالة الاستخدام الثاني. يمكننا دمج نظام المراقبة مباشرة في Golang Middleware. على سبيل المثال ، بروميثيوس ، لجمع إحصائيات عن سرعة نقاط الاستجابة ، وعدد الأخطاء.

يمكنك أيضًا
الجمع بين المراقبة والمقاييس الخاصة بالتطبيق (متوفرة وفقًا لمعايير 1.4.5). على سبيل المثال ، يمكننا إرسال عدد الطلبات إلى قاعدة البيانات أو عدد الطلبات المحددة التي تمت معالجتها إلى خادم Golang ، ثم إلى Prometheus.
تتبع الموزعة وقطع الأشجار
نكتب الوسيطة مع مدير العملية. على وجه الخصوص ، يمكننا الاتصال بنظام الوقت الفعلي لمراقبة السجلات
وجمع جميع السجلات في قاعدة بيانات مركزية واحدة ، وهو أمر مفيد عند كتابة التطبيقات الموزعة.

يمكننا أيضًا
تمييز الطلبات ، ومنحهم معرّفًا محددًا وتمرير هذا المعرف إلى جميع الخدمات المتلقية للمعلومات أو أنظمة الاتصال بينهم. نتيجة لذلك ، يمكننا إنشاء
تتبع موزّع ونرى كيف تسير سجلات التطبيق.
سجل سجل الاستعلام الخاص بك
هذه وحدة صغيرة تقوم بتسجيل جميع الطلبات الواردة وتخزينها في قاعدة بيانات خارجية. تسمح لك الوحدة النمطية بتقديم طلبات إعادة التشغيل في المشروع وتطبيق نظام اختبار تلقائي أو نظام اختبار تحميل أو مجرد التحقق من تشغيل واجهة برمجة التطبيقات.

كيف ننفذ الوحدة؟
نعالج جزءًا من طلبات Golang . نكتب الوسيطة في Golang ويمكننا إرسال جزء من الطلبات إلى Handler ، وهو مكتوب أيضًا في Golang. إذا كانت نقطة ما في التطبيق مثيرة للقلق من حيث الأداء ، فنحن نعيد كتابتها إلى Golang وسحب المكدس من لغة إلى أخرى.
نكتب خادم WebSocket . أصبح تطبيق خادم WebSocket أو خادم إعلام الدفع مهمة تافهة.
- خدمة Golang على مستوى الخادم.
- للتواصل نستخدم Goridge.
- طبقة خدمة رفيعة في PHP.
- نحن ننفذ خادم الإعلام.
نتلقى طلبًا ونرفع اتصال WebSocket. إذا احتاج التطبيق إلى إرسال نوع من الإشعارات إلى المستخدم ، فإنه يطلق هذه الرسالة عبر بروتوكول RPC إلى خادم WebSocket.
إدارة بيئة PHP الخاصة بك. عند إنشاء Worker Pool ، فإن RoadRunner تتحكم بشكل كامل في حالة متغيرات البيئة وتسمح لك بتغييرها كما تريد. إذا كنا نكتب تطبيقًا كبيرًا موزعًا ، فيمكننا استخدام مصدر واحد لبيانات التكوين وتوصيله كنظام لتكوين البيئة. إذا قمنا برفع مجموعة من الخدمات ، فكل هذه الخدمات سوف تطرق إلى نظام واحد ، وتكوينها ثم تعمل. هذا يمكن تبسيط النشر إلى حد كبير ، وكذلك التخلص من ملفات .env.

ومن المثير للاهتمام ، أن متغيرات env المتاحة داخل العامل ليست عالمية داخل النظام. هذا يحسن قليلا سلامة الحاويات.
دمج مكتبة Golang في PHP
استخدمنا هذا الخيار على الموقع الرسمي لـ
RoadRunner . هذا هو دمج قاعدة بيانات كاملة تقريبا
مع البحث عن النص الكامل BleveSearch داخل الخادم.

قمنا بفهرسة صفحات الوثائق: وضعناها في Bolt DB ، وبعد ذلك أجرينا بحثًا عن النص الكامل بدون قاعدة بيانات حقيقية مثل MySQL ، وبدون مجموعة بحث مثل Elasticsearch. كانت النتيجة عبارة عن مشروع صغير حيث توجد بعض الوظائف في PHP ، ولكن البحث موجود في Golang.
تنفيذ وظائف لامدا
يمكنك أن تذهب أبعد
من ذلك وتتخلص تمامًا من طبقة HTTP. في هذه الحالة ، يعد تنفيذ Lambda ، على سبيل المثال ، مهمة بسيطة.

للتنفيذ ، نستخدم
وقت التشغيل القياسي
AWS لوظيفة Lambda. نكتب رابطًا صغيرًا ، ونقطع خوادم HTTP تمامًا ونرسل البيانات بتنسيق ثنائي إلى العمال. لدينا أيضًا إمكانية الوصول إلى إعدادات البيئة ، والتي تتيح لنا كتابة الوظائف التي تم تكوينها مباشرةً من لوحة مسؤول Amazon.
يكون العمال في الذاكرة طوال فترة العملية ، وتبقى وظيفة Lambda بعد الطلب الأولي في الذاكرة لمدة 15 دقيقة. في هذا الوقت ، لا يتم تحميل الكود ويستجيب بسرعة. في الاختبارات الاصطناعية ، تلقينا ما يصل إلى
0.5 مللي ثانية لكل طلب وارد واحد .
gRPC ل PHP
الخيار الأكثر صعوبة هو استبدال طبقة HTTP بطبقة gRPC. هذه
الحزمة متاحة على جيثب .

يمكننا وكيل جميع طلبات Protobuf الواردة تمامًا إلى تطبيق PHP تابع ، حيث يمكن تفريغها ومعالجتها والرد عليها. يمكننا كتابة التعليمات البرمجية في PHP و Golang على حد سواء ، والجمع بين ونقل وظيفة من مكدس إلى آخر. الخدمة تدعم الوسيطة. كل من تطبيق قائم بذاته وبالتزامن مع HTTP يمكن أن تعمل.
خادم قائمة الانتظار
الخيار الأخير والأكثر إثارة للاهتمام هو تنفيذ
خادم قائمة الانتظار .

على جانب PHP ، كل ما نفعله هو الحصول على حمولة ثنائية ، وفك رزمها ، وتنفيذ العمل ، وإخبار الخادم بالنجاح. على جانب Golang ، نحن منخرطون بشكل كامل في إدارة الاتصالات مع الوسطاء. يمكن أن يكون RabbitMQ ، أمازون SQS أو شجرة الفاصولياء.
على جانب Golang ، ننفذ "
الإغلاق الرشيق
" للعمال. يمكننا أن ننتظر بشكل جميل تنفيذ "الاتصال الدائم" - في حالة فقد الاتصال مع الوسيط ، ينتظر الخادم لفترة من الوقت باستخدام "استراتيجية التراجع" ، فإنه يرفع الاتصال ولا يلاحظ التطبيق ذلك.
يمكننا معالجة هذه الطلبات في كل من PHP و Golang ، والانتظار في صفها على كلا الجانبين:
- من PHP من خلال بروتوكول Goridge Goridge RPC ؛
- من Golang - التواصل مع مكتبة SDK.
إذا انخفضت الحمولة ، فلن يسقط المستهلك بالكامل ، ولكن عملية منفصلة واحدة فقط. النظام يثيره على الفور ، يتم إرسال المهمة إلى العامل التالي. هذا يسمح لك بأداء المهام دون توقف.
طبقنا أحد الوسطاء مباشرةً في ذاكرة الخادم واستخدمنا وظيفة Golang. هذا يسمح لنا بكتابة تطبيق باستخدام قوائم الانتظار قبل اختيار المكدس النهائي. نرفع التطبيق محليًا ، ونبدأ تشغيله ، ولدينا قوائم انتظار تعمل في الذاكرة وتتصرف بنفس الطريقة التي يتصرفون بها في RabbitMQ أو Amazon SQS أو Beanstalk.
عند استخدام لغتين في هذه الحزمة المختلطة ، يجدر تذكر كيفية فصلهما.
مجالات المجال منفصلة
Golang هي لغة متعددة الخيوط وسريعة ومناسبة لكتابة منطق البنية التحتية ومنطق مراقبة المستخدم والترخيص.
كما أنه مفيد
لتنفيذ برامج التشغيل المخصصة للوصول إلى مصادر البيانات - فهذه قوائم انتظار ، على سبيل المثال ، كافكا ، كاساندرا.
PHP هي لغة رائعة لكتابة منطق الأعمال.
هذا هو نظام جيد لتقديم HTML ، ORM والعمل مع قاعدة البيانات.
مقارنة الأداة
قبل عدة أشهر
على Habré قارن PHP-FPM ، PHP-PM ، React-PHP ، Roadrunner وغيرها من الأدوات. تم عقد المعيار في مشروع حقيقي Symfony 4.
RoadRunner تحت الحمل يظهر نتائج جيدة وهو متقدم على جميع الخوادم. بالمقارنة مع PHP-FPM ، يكون الأداء أكثر من 6-8 مرات.

في نفس المعيار ، لم تفقد RoadRunner طلبًا واحدًا ، فقد كان كل شيء 100٪ تم إعداده. لسوء الحظ ، فقد React-PHP من 8 إلى 9 طلبات تحت الأحمال - وهذا غير مقبول. نود ألا ينهار الخادم ويعمل بثبات.

منذ نشر RoadRunner في متناول الجمهور على GitHub ، تلقينا أكثر من 30000 منشأة. ساعدنا المجتمع في كتابة مجموعة محددة من الامتدادات والتحسينات ونعتقد أن الحل له الحق في الحياة.
يعد RoadRunner جيدًا إذا كنت ترغب في
تسريع التطبيق بشكل كبير ، ولكنك غير مستعد بعد للانتقال إلى PHP غير المتزامن . هذا حل وسط يتطلب قدراً معيناً من الجهد ، لكن ليس بنفس أهمية إعادة كتابة قاعدة الشفرة بالكامل.
استخدم تطبيق RoadRunner إذا كنت تريد
مزيدًا من التحكم في دورة حياة PHP ،
وإذا لم تكن هناك قدرات PHP كافية ، على سبيل المثال ، لنظام قائمة الانتظار أو كافكا ، وعندما تحل مكتبة Golang الشائعة مشكلتك ، وهي ليست موجودة في PHP ، وتستغرق الكتابة وقتًا ، وهو ما لا تملكه أيضًا.
النتائج
ما حصلنا عليه بكتابة هذا الخادم واستخدامه في البنية التحتية للإنتاج لدينا.
- لقد قاموا بزيادة سرعة التفاعل لنقاط التطبيق بمقدار 4 مرات مقارنة بـ PHP-FPM.
- تماما تخلص من 502 أخطاء تحت الأحمال . في ذروة التحميل ، ينتظر الخادم فترة أطول قليلاً ويستجيب كما لو لم تكن هناك أية حمولات.
- بعد تحسين تسرب الذاكرة ، يتعطل العمال الذاكرة لمدة تصل إلى شهرين . يساعد هذا عند كتابة التطبيقات الموزعة ، حيث أن جميع الطلبات بين الخدمات يتم تخزينها مؤقتًا بالفعل على مستوى المقبس.
- نحن نستخدم Keep-Alive. هذا يسرع بشكل كبير التواصل بين النظام الموزع.
- داخل البنية التحتية الحقيقية ، وضعنا كل شيء في Alpine Docker في Kubernetes . أصبح الآن نظام النشر والبناء الخاص بالمشروع أسهل. كل ما هو مطلوب هو إنشاء بناء مخصص لـ RoadRunner للمشروع ، ووضعه في مشروع Docker ، وملء صورة Docker ، ثم تحميل ملف pod الخاص بنا بهدوء إلى Kubernetes.
- وفقًا للتوقيت الفعلي لأحد المشاريع إلى نقاط فردية لا تستطيع الوصول إلى قاعدة البيانات ، يبلغ متوسط وقت الاستجابة 0.33 مللي ثانية .
المؤتمر المهني القادم لمطوري PHP PHP روسيا العام المقبل فقط. في الوقت الحالي ، نقدم ما يلي:
- انتبه إلى GolangConf إذا كنت مهتمًا بالجزء "Go" وترغب في معرفة المزيد من التفاصيل أو سماع الحجج لصالح التحول إلى هذه اللغة. إذا كنت مستعدًا لمشاركة تجربتك ، فيرجى إرسال ملخصات .
- شارك في HighLoad ++ في موسكو ، إذا كان كل شيء مهم بالنسبة لك مرتبطًا بالأداء العالي ، فقم بتقديم تقرير قبل 7 سبتمبر ، أو حجز تذكرة.
- اشترك في النشرة الإخبارية وقناة التلغراف لتلقي دعوة إلى PHP Russia 2020 في وقت سابق من غيرها.