انتقال باطن PHP به گذرگاه Redis streams و انتخاب یک کتابخانه مستقل از چارچوب

انتقال باطن PHP به گذرگاه Redis streams و انتخاب یک کتابخانه مستقل از چارچوب

پیش گفتار

وب سایت من که به عنوان یک سرگرمی راه اندازی می کنم، برای میزبانی صفحات اصلی و سایت های شخصی جالب طراحی شده است. این موضوع در همان ابتدای سفر برنامه نویسی من برای من جالب شد؛ در آن لحظه من مجذوب یافتن حرفه ای های بزرگی شدم که در مورد خود، سرگرمی ها و پروژه های خود می نویسند. عادت به کشف آنها برای خودم تا به امروز باقی مانده است: تقریباً در هر سایت تجاری و نه چندان تجاری، من همچنان در جستجوی پیوندهایی به نویسندگان در پاورقی جستجو می کنم.

اجرای ایده

نسخه اول فقط یک صفحه html در وب سایت شخصی من بود، جایی که من پیوندهایی را با امضا در یک لیست ul قرار دادم. پس از تایپ 20 صفحه در یک دوره زمانی، به این فکر کردم که این کار چندان موثر نیست و تصمیم گرفتم سعی کنم فرآیند را خودکار کنم. در stackoverflow، متوجه شدم که بسیاری از افراد سایت‌ها را در نمایه‌های خود نشان می‌دهند، بنابراین من یک تجزیه‌کننده در php نوشتم، که به سادگی نمایه‌ها را مرور می‌کرد و با اولین شروع می‌شد (آدرس‌ها در SO تا به امروز به این صورت است: `/users/1` )، لینک ها را از تگ مورد نظر استخراج و در SQLite اضافه کرد.

این را می‌توان نسخه دوم نامید: مجموعه‌ای از ده‌ها هزار URL در یک جدول SQLite که جایگزین لیست ثابت در html شد. من یک جستجوی ساده در این لیست انجام دادم. زیرا فقط URL ها وجود داشت، سپس جستجو به سادگی بر اساس آنها بود.

در این مرحله پروژه را رها کردم و پس از مدت ها به آن بازگشتم. در این مرحله سابقه کاری من بیش از سه سال بود و احساس می کردم می توانم کار جدی تری انجام دهم. علاوه بر این، تمایل زیادی برای تسلط بر فناوری های نسبتاً جدید وجود داشت.

نسخه مدرن

پروژه در Docker مستقر شد، پایگاه داده به mongoDb منتقل شد و اخیراً، radish اضافه شد که در ابتدا فقط برای ذخیره سازی بود. یکی از میکروفریم‌ورک‌های PHP به عنوان پایه استفاده می‌شود.

مشکل

سایت های جدید توسط یک فرمان کنسول اضافه می شوند که به طور همزمان موارد زیر را انجام می دهد:

  • محتوا را بر اساس URL بارگیری می کند
  • پرچمی را تنظیم می کند که نشان می دهد HTTPS در دسترس بوده است یا خیر
  • ماهیت وب سایت را حفظ می کند
  • HTML منبع و هدرها در تاریخچه «نمایه‌سازی» ذخیره می‌شوند
  • تجزیه محتوا، استخراج عنوان و توضیحات
  • داده ها را در یک مجموعه جداگانه ذخیره می کند

این برای ذخیره سایت ها و نمایش آنها در یک لیست کافی بود:

انتقال باطن PHP به گذرگاه Redis streams و انتخاب یک کتابخانه مستقل از چارچوب

اما ایده نمایه سازی خودکار، دسته بندی و رتبه بندی همه چیز، به روز نگه داشتن همه چیز، به خوبی در این پارادایم نمی گنجید. حتی به سادگی افزودن یک روش وب برای افزودن صفحات نیاز به تکرار کد و مسدود کردن برای جلوگیری از DDoS بالقوه دارد.

به طور کلی، البته، همه چیز را می توان به صورت همزمان انجام داد، و در روش وب به سادگی می توانید URL را ذخیره کنید تا دیمون هیولایی تمام وظایف URL های لیست را انجام دهد. اما هنوز، حتی در اینجا کلمه "صف" خود را نشان می دهد. و اگر یک صف اجرا شود، تمام وظایف را می توان تقسیم کرد و حداقل به صورت ناهمزمان انجام داد.

تصمیم

صف ها را پیاده سازی کنید و یک سیستم رویداد محور برای پردازش همه وظایف بسازید. و من مدت زیادی است که می خواهم Redis Streams را امتحان کنم.

استفاده از استریم های Redis در PHP

زیرا از آنجایی که چارچوب من یکی از سه غول Symfony، Laravel، Yii نیست، می‌خواهم یک کتابخانه مستقل پیدا کنم. اما، همانطور که مشخص شد (در اولین بررسی)، یافتن کتابخانه های جدی فردی غیرممکن است. همه چیز مربوط به صف ها یا پروژه ای از 3 commit پنج سال پیش است یا به چارچوب گره خورده است.

من درباره Symfony به‌عنوان تامین‌کننده قطعات مفید منفرد چیزهای زیادی شنیده‌ام و قبلاً از برخی از آنها استفاده می‌کنم. و همچنین برخی از چیزهای لاراول را نیز می توان استفاده کرد، به عنوان مثال ORM آنها، بدون حضور خود فریمورک.

سمفونی/پیام رسان

نامزد اول بلافاصله ایده آل به نظر می رسید و بدون هیچ شکی آن را نصب کردم. اما معلوم شد که جستجوی نمونه های استفاده در خارج از Symfony در گوگل دشوارتر است. چگونه یک اتوبوس برای ارسال پیام از دسته ای از کلاس ها با نام های جهانی و بی معنی و حتی در Redis جمع آوری کنیم؟

انتقال باطن PHP به گذرگاه Redis streams و انتخاب یک کتابخانه مستقل از چارچوب

مستندات موجود در سایت رسمی کاملاً دقیق بود، اما مقدار دهی اولیه فقط برای Symfony با استفاده از YML مورد علاقه آنها و سایر روش‌های جادویی برای غیر سمفونی شرح داده شد. من هیچ علاقه ای به فرآیند نصب نداشتم، به خصوص در تعطیلات سال نو. اما من مجبور شدم این کار را برای مدت طولانی غیر منتظره انجام دهم.

تلاش برای کشف چگونگی نمونه سازی یک سیستم با استفاده از منابع Symfony نیز پیش پا افتاده ترین کار برای یک ضرب الاجل محدود نیست:

انتقال باطن PHP به گذرگاه Redis streams و انتخاب یک کتابخانه مستقل از چارچوب

بعد از کاوش در همه اینها و تلاش برای انجام کاری با دستانم، به این نتیجه رسیدم که در حال انجام نوعی عصا هستم و تصمیم گرفتم چیز دیگری را امتحان کنم.

روشن/صف

معلوم شد که این کتابخانه به شدت به زیرساخت لاراول و تعدادی وابستگی دیگر گره خورده است، بنابراین زمان زیادی را برای آن صرف نکردم: آن را نصب کردم، به آن نگاه کردم، وابستگی ها را دیدم و آن را حذف کردم.

yiisoft/yii2-queue

خوب، در اینجا بلافاصله از نام فرض شد، دوباره، یک ارتباط دقیق با Yii2. من مجبور شدم از این کتابخانه استفاده کنم و بد نبود، اما به این موضوع فکر نکردم که کاملاً به Yii2 بستگی دارد.

بقیه

هر چیز دیگری که در GitHub یافتم پروژه های غیرقابل اعتماد، قدیمی و رها شده بدون ستاره، فورک و تعداد زیادی commit بود.

بازگشت به سیمفونی/مسنجر، جزئیات فنی

من باید این کتابخانه را کشف می کردم و پس از صرف زمان بیشتر، توانستم. معلوم شد که همه چیز کاملاً مختصر و ساده است. برای نمونه سازی اتوبوس، یک کارخانه کوچک ساختم، زیرا... قرار بود چندین لاستیک و با هندلرهای مختلف داشته باشم.

انتقال باطن PHP به گذرگاه Redis streams و انتخاب یک کتابخانه مستقل از چارچوب

فقط چند قدم:

  • ما کنترل کننده های پیام را ایجاد می کنیم که باید به سادگی قابل فراخوانی باشند
  • آنها را در HandlerDescriptor (کلاس از کتابخانه) قرار می دهیم.
  • ما این «توصیف‌گرها» را در یک نمونه HandlersLocator قرار می‌دهیم
  • افزودن HandlersLocator به نمونه MessageBus
  • ما مجموعه‌ای از «SenderInterface» را به SendersLocator ارسال می‌کنیم، در نمونه‌های موردی من از کلاس‌های «RedisTransport»، که به روشی واضح پیکربندی شده‌اند.
  • افزودن SenderLocator به نمونه 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 streams و انتخاب یک کتابخانه مستقل از چارچوب

و بلافاصله پس از آن، اضافه کردن قابلیت های جدید، به عنوان مثال، استخراج و تجزیه Rss برای من بسیار آسان تر شد. زیرا این فرآیند همچنین به محتوای اصلی نیاز دارد، سپس کنترل کننده استخراج کننده پیوند RSS، مانند WebsiteIndexHistoryPersistor، در پیام "Content/HtmlContent" مشترک می شود، آن را پردازش می کند و پیام مورد نظر را در امتداد خط لوله خود ارسال می کند.

انتقال باطن PHP به گذرگاه Redis streams و انتخاب یک کتابخانه مستقل از چارچوب

در نهایت، ما با چندین دیمون مواجه شدیم که هر کدام فقط با منابع لازم ارتباط برقرار می کنند. مثلا یک دیو خزنده شامل تمام کنترل کننده هایی است که برای محتوا نیاز به مراجعه به اینترنت دارند و دیمون اصرار ورزیدن اتصال به پایگاه داده را نگه می دارد.

اکنون به جای انتخاب از پایگاه داده، شناسه های مورد نیاز پس از درج توسط Persister به سادگی از طریق گذرگاه به همه گردانندگان علاقه مند منتقل می شود.

منبع: www.habr.com

اضافه کردن نظر