نقل الواجهة الخلفية لـ PHP إلى ناقل تدفقات Redis واختيار مكتبة مستقلة عن إطار العمل

نقل الواجهة الخلفية لـ PHP إلى ناقل تدفقات Redis واختيار مكتبة مستقلة عن إطار العمل

مقدمة

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

تنفيذ الفكرة

كان الإصدار الأول مجرد صفحة html على موقع الويب الشخصي الخاص بي، حيث أضع الروابط مع التوقيعات في قائمة ul. بعد أن قمت بكتابة 20 صفحة خلال فترة من الزمن، بدأت أعتقد أن هذا لم يكن فعالاً للغاية وقررت أن أحاول أتمتة العملية. في Stackoverflow، لاحظت أن العديد من الأشخاص يشيرون إلى المواقع في ملفاتهم الشخصية، لذلك كتبت محللًا بلغة php، والذي قام ببساطة باستعراض الملفات الشخصية، بدءًا من الأول (العناوين الموجودة في SO حتى يومنا هذا هي كما يلي: `/users/1` )، تم استخراج الروابط من العلامة المطلوبة وإضافتها في SQLite.

يمكن أن يسمى هذا الإصدار الثاني: مجموعة من عشرات الآلاف من عناوين URL في جدول SQLite، والتي حلت محل القائمة الثابتة في HTML. لقد قمت ببحث بسيط في هذه القائمة. لأن لم تكن هناك سوى عناوين URL، ثم كان البحث يعتمد عليها ببساطة.

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

النسخة الحديثة

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

مشكلة

تتم إضافة المواقع الجديدة بواسطة أمر وحدة التحكم الذي يقوم بما يلي بشكل متزامن:

  • تنزيل المحتوى عن طريق URL
  • يعين علامة تشير إلى ما إذا كان HTTPS متاحًا أم لا
  • يحافظ على جوهر الموقع
  • يتم حفظ مصدر HTML والعناوين في سجل "الفهرسة".
  • يوزع المحتوى، مقتطفات العنوان والوصف
  • يحفظ البيانات في مجموعة منفصلة

كان هذا كافيًا لتخزين المواقع وعرضها في قائمة:

نقل الواجهة الخلفية لـ PHP إلى ناقل تدفقات Redis واختيار مكتبة مستقلة عن إطار العمل

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

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

حل

تنفيذ قوائم الانتظار وإنشاء نظام يحركه الحدث لمعالجة جميع المهام. ولقد كنت أرغب في تجربة Redis Streams لفترة طويلة.

استخدام تدفقات Redis في PHP

لأن نظرًا لأن إطار العمل الخاص بي ليس أحد العمالقة الثلاثة Symfony وLaravel وYii، فأنا أرغب في العثور على مكتبة مستقلة. ولكن، كما اتضح (في الفحص الأول)، من المستحيل العثور على مكتبات جادة فردية. كل ما يتعلق بقوائم الانتظار هو إما مشروع من 3 التزامات قبل خمس سنوات، أو مرتبط بإطار العمل.

لقد سمعت الكثير عن Symfony كمورد للمكونات الفردية المفيدة، وأنا بالفعل أستخدم بعضها. وأيضًا يمكن أيضًا استخدام بعض الأشياء من Laravel، على سبيل المثال ORM الخاصة بها، دون وجود إطار العمل نفسه.

سيمفوني/رسول

بدا المرشح الأول مثاليًا على الفور وقمت بتثبيته دون أدنى شك. ولكن تبين أنه من الصعب البحث عن أمثلة للاستخدام خارج Symfony على Google. كيفية التجميع من مجموعة من الفئات بأسماء عالمية لا معنى لها وحافلة لتمرير الرسائل وحتى على Redis؟

نقل الواجهة الخلفية لـ PHP إلى ناقل تدفقات Redis واختيار مكتبة مستقلة عن إطار العمل

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

إن محاولة اكتشاف كيفية إنشاء مثيل لنظام باستخدام مصادر Symfony ليست أيضًا المهمة الأكثر تافهة في موعد نهائي ضيق:

نقل الواجهة الخلفية لـ PHP إلى ناقل تدفقات Redis واختيار مكتبة مستقلة عن إطار العمل

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

مضيئة / قائمة الانتظار

اتضح أن هذه المكتبة كانت مرتبطة بإحكام بالبنية التحتية لـ Laravel ومجموعة من التبعيات الأخرى، لذلك لم أقضي الكثير من الوقت عليها: لقد قمت بتثبيتها ونظرت إليها ورأيت التبعيات وحذفتها.

yiisoft/yii2-queue

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

آخر

كل شيء آخر وجدته على GitHub كان مشاريع غير موثوقة وعفا عليها الزمن ومهجورة بدون نجوم وشوك وعدد كبير من الالتزامات.

ارجع إلى Symfony/messenger، التفاصيل الفنية

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

نقل الواجهة الخلفية لـ PHP إلى ناقل تدفقات Redis واختيار مكتبة مستقلة عن إطار العمل

فقط بضع خطوات:

  • نقوم بإنشاء معالجات الرسائل التي يجب أن تكون قابلة للاستدعاء ببساطة
  • نقوم بتغليفها في HandlerDescriptor (فئة من المكتبة)
  • نقوم بلف هذه "الواصفات" في مثيل HandlersLocator
  • إضافة HandlersLocator إلى مثيل messageBus
  • نقوم بتمرير مجموعة من `SenderInterface` إلى SendersLocator، في حالتي، حالات فئات `RedisTransport`، والتي تم تكوينها بطريقة واضحة
  • إضافة SendersLocator إلى مثيل messageBus

يحتوي messageBus على أسلوب `->dispatch()` الذي يبحث عن المعالجات المناسبة في HandlersLocator ويمرر الرسالة إليهم، باستخدام `SenderInterface` المقابل للإرسال عبر الناقل (تدفقات Redis).

في تكوين الحاوية (في هذه الحالة php-di)، يمكن تكوين هذه الحزمة بأكملها على النحو التالي:

        CONTAINER_REDIS_TRANSPORT_SECRET => function (ContainerInterface $c) {
            return new RedisTransport(
                $c->get(CONTAINER_REDIS_STREAM_CONNECTION_SECRET),
                $c->get(CONTAINER_SERIALIZER))
            ;
        },
        CONTAINER_REDIS_TRANSPORT_LOG => function (ContainerInterface $c) {
            return new RedisTransport(
                $c->get(CONTAINER_REDIS_STREAM_CONNECTION_LOG),
                $c->get(CONTAINER_SERIALIZER))
            ;
        },
        CONTAINER_REDIS_STREAM_RECEIVER_SECRET => function (ContainerInterface $c) {
            return new RedisReceiver(
                $c->get(CONTAINER_REDIS_STREAM_CONNECTION_SECRET),
                $c->get(CONTAINER_SERIALIZER)
            );
        },
        CONTAINER_REDIS_STREAM_RECEIVER_LOG => function (ContainerInterface $c) {
            return new RedisReceiver(
                $c->get(CONTAINER_REDIS_STREAM_CONNECTION_LOG),
                $c->get(CONTAINER_SERIALIZER)
            );
        },
        CONTAINER_REDIS_STREAM_BUS => function (ContainerInterface $c) {
            $sendersLocator = new SendersLocator([
                AppMessagesSecretJsonMessages::class => [CONTAINER_REDIS_TRANSPORT_SECRET],
                AppMessagesDaemonLogMessage::class => [CONTAINER_REDIS_TRANSPORT_LOG],
            ], $c);
            $middleware[] = new SendMessageMiddleware($sendersLocator);

            return new MessageBus($middleware);
        },
        CONTAINER_REDIS_STREAM_CONNECTION_SECRET => function (ContainerInterface $c) {
            $host = 'bu-02-redis';
            $port = 6379;
            $dsn = "redis://$host:$port";
            $options = [
                'stream' => 'secret',
                'group' => 'default',
                'consumer' => 'default',
            ];

            return Connection::fromDsn($dsn, $options);
        },
        CONTAINER_REDIS_STREAM_CONNECTION_LOG => function (ContainerInterface $c) {
            $host = 'bu-02-redis';
            $port = 6379;
            $dsn = "redis://$host:$port";
            $options = [
                'stream' => 'log',
                'group' => 'default',
                'consumer' => 'default',
            ];

            return Connection::fromDsn($dsn, $options);
        },

هنا يمكنك أن ترى أنه في SendersLocator قمنا بتعيين "وسائل نقل" مختلفة لرسالتين مختلفتين، ولكل منهما اتصالها الخاص بالتدفقات المقابلة.

لقد قمت بإنشاء مشروع تجريبي منفصل يوضح تطبيقًا لثلاثة شياطين تتواصل مع بعضها البعض باستخدام الناقل التالي: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

لكنني سأوضح لك كيف يمكن تنظيم المستهلك:

use AppMessagesDaemonLogMessage;
use SymfonyComponentMessengerHandlerHandlerDescriptor;
use SymfonyComponentMessengerHandlerHandlersLocator;
use SymfonyComponentMessengerMessageBus;
use SymfonyComponentMessengerMiddlewareHandleMessageMiddleware;
use SymfonyComponentMessengerMiddlewareSendMessageMiddleware;
use SymfonyComponentMessengerTransportSenderSendersLocator;

require_once __DIR__ . '/../vendor/autoload.php';
/** @var PsrContainerContainerInterface $container */
$container = require_once('config/container.php');

$handlers = [
    DaemonLogMessage::class => [
        new HandlerDescriptor(
            function (DaemonLogMessage $m) {
                error_log('DaemonLogHandler: message handled: / ' . $m->getMessage());
            },
            ['from_transport' => CONTAINER_REDIS_TRANSPORT_LOG]
        )
    ],
];
$middleware = [];
$middleware[] = new HandleMessageMiddleware(new HandlersLocator($handlers));
$sendersLocator = new SendersLocator(['*' => [CONTAINER_REDIS_TRANSPORT_LOG]], $container);
$middleware[] = new SendMessageMiddleware($sendersLocator);

$bus = new MessageBus($middleware);
$receivers = [
    CONTAINER_REDIS_TRANSPORT_LOG => $container->get(CONTAINER_REDIS_STREAM_RECEIVER_LOG),
];
$w = new SymfonyComponentMessengerWorker($receivers, $bus, $container->get(CONTAINER_EVENT_DISPATCHER));
$w->run();

استخدام هذه البنية التحتية في التطبيق

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

بدا مسار إضافة موقع جديد إلى قاعدة البيانات كما يلي:

نقل الواجهة الخلفية لـ PHP إلى ناقل تدفقات Redis واختيار مكتبة مستقلة عن إطار العمل

وبعد ذلك مباشرة، أصبح من الأسهل بالنسبة لي إضافة وظائف جديدة، على سبيل المثال، استخراج وتحليل Rss. لأن تتطلب هذه العملية أيضًا المحتوى الأصلي، ثم يقوم معالج مستخرج ارتباط RSS، مثل WebsiteIndexHistoryPersistor، بالاشتراك في رسالة "Content/HtmlContent"، ومعالجتها وتمرير الرسالة المطلوبة على طول مسارها بشكل أكبر.

نقل الواجهة الخلفية لـ PHP إلى ناقل تدفقات Redis واختيار مكتبة مستقلة عن إطار العمل

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

الآن، بدلاً من الاختيار من قاعدة البيانات، يتم ببساطة نقل المعرفات المطلوبة بعد الإدراج بواسطة المثبت عبر الناقل إلى جميع المعالجات المعنية.

المصدر: www.habr.com

إضافة تعليق