Пераклад PHP бэкенда на шыну Redis streams і выбар незалежнай ад фрэймворкаў бібліятэкі

Пераклад PHP бэкенда на шыну Redis streams і выбар незалежнай ад фрэймворкаў бібліятэкі

Прадмова

Мой сайт, якім я займаюся ў якасці хобі, прызначаны для захоўвання цікавых хатніх старонак і персанальных сайтаў. Гэтая тэма стала цікавіць мяне ў самым пачатку майго шляху ў праграмаванні, у той момант мяне захапляла знаходжанне вялікіх прафесіяналаў, якія пішуць пра сябе, свае захапленні і праекты. Звычка адчыняць іх для сябе засталася і цяпер: амаль на кожным камерцыйным і не вельмі сайце я працягваю зазіраць у футэр у пошуках спасылак на аўтараў.

Рэалізацыя ідэі

Першая версія была проста html-старонкай на маім персанальным сайце, дзе я складаў спасылкі з подпісамі ў ul-спіс. Набраўшы за нейкі час старонак 20, я пачаў думаць, што гэта не вельмі эфектыўна і вырашыў паспрабаваць аўтаматызаваць працэс. На stackoverflow я заўважаў, што шматлікія паказваюць сайты ў сваіх профілях, таму я напісаў парсер на php, які проста ішоў па профілях, пачынальна з першага (адрасы на SO і дагэтуль такога выгляду: `/users/1`), здабываў спасылкі з патрэбнага тэга і складаў у SQLite.

Гэта можна назваць другой версіяй: калекцыя з дзясятка тысяч урлаў у SQLite таблічцы, якая замяніла статычны спіс у html. Па гэтым спісе я зрабіў просты пошук. Т.к. былі толькі урлы, то і пошук быў проста па іх.

На гэтым этапе я закінуў праект і вярнуўся да яго, праз доўгі час. На гэтым этапе вопыт маёй працы складаў ужо больш за тры гады і я адчуваў, што магу зрабіць нешта больш сур'ёзнае. Да таго ж было вялікае жаданне асвойваць адносна новыя для сябе тэхналогіі.

Сучасная версія

праект разгорнуты ў докеры, база пераведзена на mongoDb, і з адносна нядаўніх часоў, дададзены радыска, які спачатку быў проста для кэшавання. У якасці асновы выкарыстоўваецца адзін з мікрафрэймворкаў PHP.

праблема

Новыя сайты дадаюцца кансольнай камандай, якая сінхронна робіць наступнае:

  • Спампоўвае кантэнт па URL
  • Выстаўляе сцяг аб тым, ці даступны быў HTTPS.
  • Захоўвае сутнасць вэб-сайта
  • Зыходны HTML і загалоўкі захоўвае ў гісторыю "індэксавання"
  • Парсіт кантэнт, здабывае Title і Description
  • Дадзеныя захоўвае ў асобную калекцыю

Гэтага было дастаткова, каб проста захоўваць сайты і адлюстроўваць іх у спісе:

Пераклад PHP бэкенда на шыну Redis streams і выбар незалежнай ад фрэймворкаў бібліятэкі

Але ідэя ўсё аўтаматычна індэксаваць, катэгарызаваць і ранжыраваць, трымаючы ўсё ў актуальным стане ў гэтую парадыгму ўкладвалася слаба. Нават простае даданне web-метаду для дадання старонак запатрабавала дубліраванні кода і блакіровак для пазбягання патэнцыйнага DDoS.

Наогул, вядома, усё можна рабіць і сінхронна, а ў web-метадзе вырабляць проста захаванне УРЛа для таго, каб монструозны дэман выконваў усе задачы для УРЛаў са спісу. Але ўсё роўна нават тут напрошваецца слова "чарга". А калі чарга ўкараніць, то можна ўсе задачы падзяліць і выконваць прынамсі асінхронна.

рашэнне

Укараніць чэргі і зрабіць event-driven сістэму апрацоўкі ўсіх задач. І якраз даўно хацелася паспрабаваць Redis Streams.

Выкарыстанне Redis streams у PHP

Т.к. фрэймворк у мяне не з тройкі гігантаў Symfony, Laravel, Yii, то і бібліятэку хацелася б знайсці незалежную. Але, як аказалася (пры першым разглядзе) — асобных сур'ёзных бібліятэк знайсці немагчыма. Усё, што звязана з чэргамі, альбо з'яўляецца праектыкам з 3 комітаў пяцігадовай даўніны, альбо прывязана да фрэймворка.

Я чуў аб Symfony, як аб пастаўшчыку асобных карысных кампанентаў, да таго ж некаторыя я ўжо выкарыстоўваю. А таксама ад Laravel тое-сёе таксама можна выкарыстоўваць, напрыклад іх ORM, без прысутнасці самога фрэймворка.

symfony/messenger

Першы ж кандыдат адразу ж здаўся ідэальным і без усялякіх сумневаў я яго ўсталяваў. Але нагугліць прыклады выкарыстання па-за Symfony аказалася складаней. Як сабраць з кучы класаў з універсальнымі, ні пра што не размаўлялымі назвамі, шыну для перадачы паведамленняў, ды яшчэ і на Redis?

Пераклад PHP бэкенда на шыну Redis streams і выбар незалежнай ад фрэймворкаў бібліятэкі

Дакументацыя на афіцыйным сайце была дастаткова падрабязнай, але ініцыялізацыя была апісана толькі для Symfony з дапамогай іх каханага YML і іншых магічных метадаў для не сімфаніста. Цікавасці ў самім працэсе ўстаноўкі ў мяне не было, асабліва ў навагоднія канікулы. Але прыйшлося займацца гэтым і нечакана доўга.

Спроба разабрацца з інстанцыяваннем сістэмы па зыходніках Symfony задача таксама не самая трывіяльная для сціслых тэрмінаў:

Пераклад PHP бэкенда на шыну Redis streams і выбар незалежнай ад фрэймворкаў бібліятэкі

Пакалупаўшыся ў гэтым усім і паспрабаваўшы нешта зрабіць рукамі, я прыйшоў да высновы, што займаюся нейкімі мыліцамі і вырашыў паспрабаваць што-небудзь яшчэ.

illuminate/queue

Аказалася, што гэтая бібліятэка намёртва прывязана да інфраструктуры Laravel і кучы іншых залежнасцяў, таму шмат часу я на яе не марнаваў: паставіў, паглядзеў, убачыў залежнасці і выдаліў.

yiisoft/yii2-queue

Ну тут адразу меркавалася з назову, ізноў жа цвёрдая прывязка да Yii2. Гэтай бібліятэкай мне даводзілася карыстацца і яна была нядрэннай, але пра тое, што яна поўнасцю залежыць ад Yii2 я не думаў.

Астатнія

Усё іншае, што я знаходзіў на гітхабе - ненадзейныя састарэлыя і закінутыя праектыкі без зорак, форкаў і вялікай колькасці коммітаў.

Зварот да symfony/messenger, тэхнічныя падрабязнасці

Прыйшлося разабрацца з гэтай бібліятэкай і, патраціўшы яшчэ нейкі час, я змог. Аказалася, што ўсё дастаткова лаканічна і проста. Для інстанцыявання шыны я зрабіў невялікую фабрыку, т.я. шын у мяне меркавалася некалькі і з рознымі апрацоўшчыкамі.

Пераклад PHP бэкенда на шыну Redis streams і выбар незалежнай ад фрэймворкаў бібліятэкі

Усяго некалькі крокаў:

  • Ствараем апрацоўшчыкі паведамленняў, якія павінны быць проста callable
  • Заварочваем іх у HandlerDescriptor (клас з бібліятэкі)
  • Гэтыя «Дэскрыптары» заварочваем у інстанс HandlersLocator
  • Дадаем HandlersLocator у інстанс MessageBus
  • Перадаем у SendersLocator набор `SenderInterface`, у маім выпадку інстансы класаў `RedisTransport`, якія канфігуруюцца відавочнай выявай
  • Дадаем SendersLocator у інстанс MessageBus

MessageBus мае метад `->dispatch()`, які шукае адпаведныя апрацоўшчыкі ў HandlersLocator і перадае паведамленне ім, карыстаючыся адпаведнымі `SenderInterface` для адпраўкі праз шыну (Redis streams).

У канфігурацыі кантэйнера (у дадзеным выпадку 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 і выбар незалежнай ад фрэймворкаў бібліятэкі

У канчатковым выніку атрымалася некалькі дэманаў, кожны з якіх трымае падключэнні толькі да патрэбных рэсурсаў. Напрыклад дэман паўзункі змяшчае ў сабе ўсе апрацоўшчыкі, якія патрабуюць паходу ў інтэрнэт за кантэнтам, а дэман захоўваюцца трымае падлучэнне да базы дадзеных.

Зараз замест селектаў з базы дадзеных, патрэбныя id пасля ўстаўкі persister'ам проста перадаюцца праз шыну ўсім зацікаўленым апрацоўшчыкам.

Крыніца: habr.com

Дадаць каментар