PHP غير متزامن وقصة دراجة واحدة

بعد إصدار PHP7 ، أصبح من الممكن كتابة التطبيقات طويلة العمر بتكلفة منخفضة نسبيًا. بالنسبة للمبرمجين ، أصبحت مشاريع مثل prooph ، و broadway ، و prooph ، و messenger متاحة ، prooph بحل المشكلات الأكثر شيوعًا. ولكن ماذا لو اتخذت خطوة صغيرة إلى الأمام ، الخوض في السؤال؟


دعنا نحاول معرفة مصير دراجة أخرى ، مما يسمح لك بتطبيق نشر / اشتراك.


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


PHP خلقت للموت


لفترة طويلة ، تم استخدام PHP بشكل أساسي في سير عمل الطلب / الاستجابة. من وجهة نظر المطورين ، هذا مريح للغاية ، لأنه لا داعي للقلق بشأن تسرب الذاكرة أو اتصالات الشاشة.


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


على سبيل المثال ، يمكنك أن تأخذ طلب CRUD منتظم مكتوبًا على أساس Symfony Framework. من أجل القراءة من قاعدة البيانات وإرجاع JSON ، من الضروري القيام بعدد من الخطوات (لتوفير المساحة والوقت ، واستبعاد خطوات إنشاء / تنفيذ رموز التشغيل):


  • تحليل التكوين.
  • تجميع الحاويات
  • طلب التوجيه
  • التنفيذ؛
  • تقديم النتيجة.

كما في حالة PHP (باستخدام مسرعات) ، يستخدم الإطار بنشاط التخزين المؤقت (لن يتم إكمال بعض المهام بناءً على الطلب التالي) ، وكذلك التأخير في التهيئة. بدءًا من الإصدار 7.4 ، سيكون التحميل المسبق متاحًا ، مما سيزيد من تحسين تهيئة التطبيق.


ومع ذلك ، لا يمكن إزالة جميع التكاليف العامة للتهيئة بالكامل.


دعونا مساعدة PHP البقاء على قيد الحياة


يبدو حل المشكلة بسيطًا جدًا: إذا قمت بتشغيل التطبيق في كل مرة تكون باهظة الثمن ، فيجب عليك تهيئة البرنامج مرة واحدة ثم تمرير الطلبات إليه ، والتحكم في تنفيذها.


هناك مشاريع في النظام البيئي PHP مثل php-pm و RoadRunner . كلاهما من الناحية النظرية تفعل الشيء نفسه:


  • يتم إنشاء عملية الأصل التي تعمل كمشرف.
  • يتم إنشاء مجموعة من العمليات التابعة ؛
  • عند تلقي طلب ، يسترجع الرئيسي العملية من التجمع ويمرر الطلب إليها. يتم تعليق العميل في هذا الوقت ؛
  • بمجرد اكتمال المهمة ، يقوم master بإرجاع النتيجة إلى العميل ، ويتم إرسال العملية الفرعية مرة أخرى إلى التجمع.

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


ملاحظة:
العديد من الأمثلة من سلسلة "تأخذ ReactPHP وتسريع Laravel N مرة" المشي على الشبكة. من المهم أن نفهم الفرق بين شيطنة (ونتيجة لذلك ، توفير الوقت في تمهيد التطبيق) وتعدد المهام.
عند استخدام php-pm أو roadrunner ، لا يصبح رمزك غير محظور. يمكنك فقط توفير الوقت في التهيئة.
إن مقارنة php-pm و roadrunner و ReactPHP / Amp / Swoole غير صحيحة من حيث التعريف.

PHP و I / O

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


تخيل أنك مشغل مركز اتصال وتحتاج إلى الاتصال بـ 50 عميل في ساعة واحدة.
يمكنك الاتصال بالرقم الأول ، وهناك مشغول (يناقش المشترك عبر الهاتف السلسلة الأخيرة من لعبة العروش وما هي السلسلة التي تم نقلها).
والآن أنت جالس وتحاول الوصول إليه قبل النصر. الوقت يمر ، التحول يقترب من نهايته. بعد أن فقدت 40 دقيقة من محاولة الوصول إلى المشترك الأول ، فاتتك الفرصة للاتصال بالآخرين وتلقيت من رئيسك بشكل طبيعي.
ولكن يمكنك القيام بخلاف ذلك: لا تنتظر حتى يصبح المشترك الأول مجانيًا وبمجرد سماع صوت صفير ، شنق وابدأ في طلب الرقم التالي. يمكنك العودة إلى الأولى في وقت لاحق قليلا.
مع هذا النهج ، تزداد بشكل كبير فرص الاتصال بالعدد الأقصى من الأشخاص ، ولا تعتمد سرعة عملك على أبطأ مهمة.

يُسمى الرمز الذي لا يحظر مؤشر ترابط التنفيذ (لا يستخدم حظر مكالمات I / O ، وكذلك وظائف مثل sleep() ) ، غير متزامن.


دعنا نعود إلى تطبيق Symfony CRUD. يكاد يكون من المستحيل الحصول عليه للعمل في وضع غير متزامن بسبب وفرة استخدام وظائف الحظر: جميعها تعمل مع التكوينات وذاكرة التخزين المؤقت والتسجيل وتقديم الاستجابة والتفاعل مع قاعدة البيانات.


ولكن هذه كلها اتفاقيات ، دعونا نحاول رمي Symfony واستخدام Amp ، الذي يوفر تنفيذًا لحدث الأحداث (بما في ذلك عدد من المجلدات) ، و Promises و Coroutines ، ككرز على كعكة لحل مشكلتنا.


الوعد هو طريقة واحدة لتنظيم التعليمات البرمجية غير المتزامنة. على سبيل المثال ، نحتاج إلى الوصول إلى بعض موارد http.


نقوم بإنشاء كائن طلب وننقله إلى وسيلة النقل ، والذي يعود إليه Promise لنا والذي يحتوي على الحالة الحالية. هناك ثلاث حالات ممكنة:


  • النجاح: تم إكمال طلبنا بنجاح ؛
  • خطأ: أثناء تنفيذ الطلب ، حدث خطأ ما (على سبيل المثال ، أرجع الخادم استجابة 500) ؛
  • في انتظار: معالجة الطلب لم تبدأ بعد.

يحتوي كل وعد على طريقة واحدة (في المثال ، يتم تحليل Promise بواسطة Amp ) - onResolve() ، حيث يتم تمرير وظيفة رد اتصال مع onResolve()


 $promise->onResolve( static function(?/Throwable $throwable, $result): void { if(null !== $throwable) { /**   */ return; } /**  */ } ); 

بعد تلقينا وعدًا ، يطرح السؤال: من الذي سيراقب حالته ويبلغنا بتغيير الحالة؟


لهذا ، يتم استخدام حلقة الحدث.


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


بالنسبة للفروق الدقيقة ، أوصي بقراءة مقال من نيكيتا بوبوف: تعدد المهام التعاونية باستخدام coroutines . وسوف يساعد على توضيح بعض الشيء حول ما يحدث وأين المولدات.


مسلحين بمعرفة جديدة ، دعونا نحاول العودة إلى مهمة تقديم JSON الخاصة بنا.
مثال على معالجة طلب http وارد باستخدام amphp / http-server .
بمجرد أن نتلقى الطلب ، يتم تنفيذ قراءة غير متزامنة من قاعدة البيانات (نحصل على وعد) وعند الانتهاء من ذلك ، سيتم منح المستخدم JSON المطلوب ، والذي تم تشكيله على أساس البيانات المستلمة.


إذا كنا بحاجة إلى الاستماع إلى منفذ واحد من عدة عمليات ، فيمكننا التطلع نحو amphp / cluster

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


عالم رائع من PHP غير المتزامن


تنصل
يعتبر PHP غير المتزامن في سياق الأنواع الغريبة ولا يعتبر شيئًا صحيًا / عاديًا. في الأساس ، سوف ينتظرون الضحك بأسلوب "خذ GO / Kotlin ، أحمق" ، إلخ. لن أقول إن هؤلاء الناس مخطئون ، لكن ...

هناك عدد من المشاريع التي تساعد في كتابة كود PHP غير المحجوب. في إطار المقال ، لن أحلل بالكامل جميع إيجابيات وسلبيات ، لكنني سأحاول فقط فحص كل منهم بشكل سطحي.


Swoole

إطار غير متزامن مكتوب على عكس الآخرين في C وتسليمه امتدادًا لـ PHP. تمتلك أفضل مؤشرات الأداء في الوقت الحالي.


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


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


بالنسبة إلى السلبيات ، يمكنك أيضًا إضافة أن المساهمة في المشروع (باستخدام طلب السحب) مع أي تغييرات ستكون صعبة أيضًا إذا كنت لا تعرف C في المستوى المناسب.


Workerman

إذا فقدت السرعة أمام منافسها (الحديث عن Swoole) ، فلن يكون ذلك ملحوظًا بشكل كبير ويمكن إهمال الفرق في عدد من السيناريوهات.


لقد تكامل مع ReactPHP ، والذي بدوره يوسع عدد تطبيقات قضايا البنية التحتية. لتوفير مساحة ، سوف أصف السلبيات جنبا إلى جنب مع ReactPHP.


ReactPHP

وتشمل إيجابيات مجتمع كبير إلى حد ما وعدد كبير من الأمثلة. سلبيات تبدأ في الظهور في عملية الاستخدام - وهذا هو مفهوم الوعد.
إذا كنت بحاجة إلى إجراء العديد من العمليات غير المتزامنة ، يتحول الرمز إلى سلة مهملات لا نهائية للمكالمات ثم (هنا مثال على اتصال بسيط بـ RabbiqMQ دون إنشاء تبادل / قائمة انتظار ومجلداتها ).


مع بعض التنقيح باستخدام ملف (يعتبر القاعدة) ، يمكنك الحصول على تنفيذ coroutine ، مما سيساعد في التخلص من Promise hell.


بدون مشروع الارتداد / الارتداد ، فإن استخدام ReactPHP ، في رأيي ، غير ممكن في تطبيق عاقل.


أيضا ، إلى جانب كل شيء آخر ، يشعر المرء أن تطوره تباطأ كثيرا. لا يكفي ، على سبيل المثال ، العمل العادي مع PostgreSQL.


أمبير

في رأيي ، فإن أفضل الخيارات الموجودة في الوقت الحالي.
بالإضافة إلى الوعد المعتاد ، هناك تطبيق Coroutine ، مما يسهل عملية التطوير إلى حد كبير ، ويبدو الرمز أكثر شيوعًا لمبرمجي PHP.


المطورين يكملون ويحسنون باستمرار المشروع ، مع ردود الفعل هناك أيضا أي مشاكل.


لسوء الحظ ، مع كل مزايا الإطار ، فإن المجتمع صغير نسبيًا ، ولكن في نفس الوقت هناك تطبيقات ، على سبيل المثال ، العمل مع PostgreSQL ، وكذلك جميع الأشياء الأساسية (نظام الملفات ، عميل http ، DNS ، إلخ).


ما زلت لا أفهم مصير مشروع ext-async تمامًا ، لكن الرجال يتابعونه. ما سيأتي من هذا في الإصدار 3 ، وسوف اقول الوقت.


الابتداء


لذلك ، قمنا بفرز الجزء النظري قليلاً ، لقد حان الوقت للمضي قدمًا في التدريب وملء المطبات.


أولاً ، نضفي الطابع الرسمي على المتطلبات قليلاً:


  • المراسلة غير المتزامنة (يمكن تقسيم مفهوم message نفسها إلى نوعين)
    • command : يشير إلى الحاجة إلى إكمال المهمة. لا يُرجع نتيجة (على الأقل في حالة الاتصال غير المتزامن) ؛
    • event : الإبلاغ عن أي تغيير في الحالة (على سبيل المثال ، نتيجة للأمر).
  • تنسيق غير مانع للعمل مع I / O ؛
  • القدرة على زيادة عدد المعالجات بسهولة ؛
  • القدرة على كتابة معالجات الرسائل بأي لغة.

أي رسالة بطبيعتها هي بنية بسيطة ولا يتم مشاركتها إلا من خلال دلالاتها. تعد تسمية الرسائل مهمة للغاية من وجهة نظر فهم النوع والغرض (على الرغم من تجاهل هذه النقطة في المثال).

للحصول على قائمة بالمتطلبات ، يكون التنفيذ البسيط لنمط النشر / الاشتراك أكثر ملاءمة.
لضمان التنفيذ الموزع ، سوف نستخدم RabbitMQ كوسيط للرسائل.


تم كتابة النموذج الأولي باستخدام ReactPHP و Bunny و DoctrineDBAL .
ربما لاحظ القارئ اليقظ أن Dbal يستخدم حظر المكالمات pdo / mysqli داخليًا ، لكن في المرحلة الحالية لم تكن هذه مهمة بشكل خاص ، حيث كان عليك فهم ما يجب أن يحدث في النهاية.


كانت إحدى المشكلات عدم وجود مكتبات للعمل مع PostgreSQL. هناك بعض المسودات ، لكن هذا لا يكفي للعمل الكامل (المزيد حول هذا أدناه).


بعد بحث قصير ، تمت إزالة ReactPHP لصالح Amp ، لأنه بسيط نسبيًا ومتطور للغاية.


النقل RabbitMQ

ولكن مع كل مزايا Amp ، كانت هناك مشكلة واحدة: Amp ليس لديه برنامج تشغيل لـ RabbitMQ (يدعم الأرنب ReactPHP فقط).


من الناحية النظرية ، يسمح لك Amp باستخدام وعد من منافس. يبدو أن كل شيء يجب أن يكون بسيطًا ، ولكن ReactPHP يستخدم "حلقة الأحداث" للعمل مع مآخذ في المكتبة.
في وقت ما ، من الواضح أنه لا يمكن بدء تشغيل حلقتين مختلفتين للأحداث ، لذلك لا يمكنني استخدام وظيفة adapt () .


لسوء الحظ ، فإن جودة الكود في الارنب تركت الكثير مما هو مرغوب فيه ولم يكن من الممكن استبدال تطبيق آخر بشكل كاف. من أجل عدم إيقاف العمل ، تقرر إعادة كتابة المكتبة قليلاً بحيث تعمل مع Amp ولا تؤدي إلى منع تدفق التنفيذ.


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


ووجد هذا الرجل. يوفر مشروع PHPinnacle ، من بين أشياء أخرى ، تنفيذ محول مخصص لـ Amp.


اسم المؤلف هو Anton Shabovta ، الذي سيتحدث عن php غير المتزامن في إطار PHP Russia وعن تطوير برامج تشغيل لـ PHP fwdays .

كيو

الميزة الثانية للعمل هي التفاعل مع قاعدة البيانات. في ظروف PHP "التقليدية" ، كل شيء بسيط: لدينا اتصال ويتم تنفيذ جميع الطلبات بالتسلسل.


في حالة التنفيذ غير المتزامن ، يجب أن نكون قادرين على تنفيذ عدة طلبات في وقت واحد (على سبيل المثال ، 3 معاملات). من أجل أن تكون قادرًا على القيام بذلك ، مطلوب تطبيق تجمع الاتصال.


آلية العمل بسيطة للغاية:


  • نفتح اتصالات N عند بدء التشغيل (أو تأخر التهيئة ، وليس النقطة) ؛
  • إذا لزم الأمر ، نأخذ الاتصال من المجمع ، ونضمن أنه لا يمكن لأي شخص آخر استخدامه ؛
  • نقوم بتنفيذ الطلب وإما إتلاف الاتصال أو إعادته إلى التجمع (المفضل).

أولاً ، يسمح لنا ببدء العديد من المعاملات في وقت واحد ، وثانياً ، يسرع العمل بسبب وجود اتصالات مفتوحة بالفعل. يحتوي Amp على مكون amphp / postgres . إنه يهتم بالاتصالات: يقوم بمراقبة أرقامها وعمرها وكل ذلك دون إعاقة تدفق التنفيذ.


بالمناسبة ، عند استخدام ، على سبيل المثال ، ReactPHP ، سيكون عليك تنفيذ هذا بنفسك إذا كنت تريد العمل مع قاعدة بيانات.


مزامنة

للتشغيل الفعال ، والأهم من ذلك ، التطبيق المناسب ، من الضروري تنفيذ شيء مشابه للمزاخات. يمكننا التمييز بين 3 سيناريوهات لاستخدامها:


  • في إطار عملية واحدة ، آلية بسيطة في الذاكرة مناسبة دون أي فائض ؛
  • إذا أردنا توفير تأمين في العديد من العمليات ، فيمكننا استخدام نظام الملفات (بالطبع ، في وضع عدم الحظر) ؛
  • إذا كنت في سياق العديد من الخوادم ، فأنت بحاجة بالفعل إلى التفكير في شيء مثل Zookeeper.

هناك حاجة إلى موتكسيس لحل مشاكل حالة السباق . بعد كل شيء ، نحن لا نعرف (ولا يمكننا أن نعرف) بترتيب تنفيذ مهامنا ، لكن مع ذلك يجب علينا ضمان سلامة البيانات.


قطع الأشجار / السياقات

بالنسبة للتسجيل ، أصبح Monolog قياسيًا بالفعل ، ولكن مع بعض التحذيرات: لا يمكننا استخدام المعالجات المدمجة ، حيث إنها ستؤدي إلى الإغلاق.
للكتابة إلى stdOut ، يمكنك أن تأخذ amphp / log ، أو تكتب رسالة بسيطة ترسل إلى بعض Graylog.


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


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


تنتهي

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


لإكمال التنفيذ بشكل صحيح ، نحتاج إلى:


  • إلغاء الاشتراك من قائمة الانتظار. بمعنى آخر ، تجعل من المستحيل تلقي رسائل جديدة ؛
  • إكمال جميع المهام المتبقية (انتظر حل الوعود) ؛
  • وفقط بعد الانتهاء من البرنامج النصي.

التسريبات ، تصحيح الأخطاء

على عكس الاعتقاد الشائع ، في PHP الحديثة ، ليس من السهل مواجهة المواقف التي يحدث فيها تسرب للذاكرة. من الضروري أن تفعل شيئا خاطئا تماما.


ومع ذلك ، واجه مرة واحدة مع هذا ، ولكن بسبب الإهمال عاديا. أثناء تنفيذ دقات القلب ، تمت إضافة مؤقت جديد كل 40 ثانية للاستعلام عن الاتصال. ليس من الصعب تخمين أنه بعد مرور بعض الوقت بدأ استخدام الذاكرة يزحف بسرعة كبيرة.


أيضًا ، من بين أشياء أخرى ، كتب مراقبًا بسيطًا يبدأ اختياريًا كل 10 دقائق ويتصل بـ gc_collect_cycles () و gc_mem_caches () .
لكن البدء القسري لجامع القمامة ليس شيئًا ضروريًا وأساسيًا.


لمشاهدة استخدام الذاكرة باستمرار ، تمت إضافة MemoryUsageProcessor قياسي إلى التسجيل .


إذا حصلت على فكرة أن Event Loop تمنعها بشيء ، فيمكن أيضًا التحقق من ذلك بسهولة: فقط قم بتوصيل LoopBlockWatcher .


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


النتائج


: php-service-bus , Message Based .


, :


 composer create-project php-service-bus/skeleton pub-sub-example cd pub-sub-example docker-compose up --build -d 

, , .


/bin/consumer , .
/src 3 : Ping ; Pong : ; PingService : , .
PingService , 2 :


  /** @CommandHandler() */ public function handle(Ping $command, KernelContext $context): Promise { return $context->delivery(new Pong()); } /** @EventListener() */ public function whenPong(Pong $event, KernelContext $context): void { $context->logContextMessage('Pong message received'); } 

  • handle ( 1 ). @CommandHandler ;
    • Promise , RabbitMQ ( delivery() ). , RabbitMQ .
  • whenPongPong . . @EventListener ;
    , — . , , , . php-service-bus , , .

2 : , ( ) . , , (, ).


Ping , Pong . .


, RabbitMQ:


 tools/ping 

, php-service-bus , Message based .


Ping\Pong, — , , Hello, world .


, .


- , , , Saga pattern (Process manager) .



, symfony/messenger .


, , .

Source: https://habr.com/ru/post/ar451916/


All Articles