Микросервисы на php и swoole для конвертации телеграм каналов в RSS

  • Tutorial


В предыдущем посте я рассказал про то, как настроить и использовать php телеграм клиент madelineProto для парсинга постов. Но при использовании библиотеки я столкнулся с несколькими недостатками:

  • Долгая обработка запросов из-за авторизации телеграм клиента;
  • Неудобная настройка;
  • Проблемы с отдачей изображений из постов.

Поэтому решил создать два микросервиса на php для парсинга телеграм каналов, используя асинхронное расширение swoole. Теперь эти пакеты упрощают и ускоряют работу с telegram api (не путать с bot api) в нескольких моих проектах. Хочется поделится ими и услышать мнение других разработчиков.

Под катом расскажу об архитектуре, использовании разных областей видимости в swoole server и устранении последствий ошибок в сторонних библиотеках и внешних api. Ссылки на репозитории с исходным кодом и на тестовый сервер — в конце поста.

Общая архитектура


Изначально планировался один пакет, который выступал бы в качестве парсера telegram и генератора RSS потоков. Но в процессе разработки код становился все более и более неподдерживаемым. Стало ясно, что нужно строже следовать одному из базовых канонов разработки: метод или библиотека должны решать только одну задачу.

В результате декомпозиции появились два микросервиса: TelegramSwooleClient и TelegramRSS.

схема
Общая схема сервиса по генерации RSS потоков из telegram каналов

TelegramRSS
Отвечает за коммуникацию с пользователями и генерацию RSS потоков. Упрощенная схема работы:

  1. Получаем запрос от клиента;
  2. Определяем, что запросили: главную страницу, favicon, rss, json или media файл;
  3. Если пользователь сделал некорректный запрос, или слишком часто обращается к api — добавляем ip в blacklist;
  4. Если пользователь в blacklist — выдаем ошибку;
  5. Запрашиваем сообщения из телеграм канала или конкретный медиафайл из поста через http запрос к TelegramSwooleClient;
  6. Если запросили media файл: даем команду TelegramSwooleClient скачать файл во временную папку и вернуть путь до этого файла. Отдаем файл и удаляем его;
  7. Если запросили RSS: парсим ответ TelegramSwooleClient и генерируем RSS;

В TelegramRSS использование swoole сервера было не обязательным, но дало небольшие преимущества:

  • Не нужен кеш для хранения черного списка ip адресов (или других данных). Все необходимое хранится в памяти, в экземпляре класса, доступном во всех запросах.
  • Проще конфигурация nginx: проксируем запросы на ip микросервиса и не беспокоимся о безопасности файлов: .env или *.session.madeline и любых других.


TelegramSwooleClient
Отвечает за коммуникацию с telegram api. По сути — это обертка над madelineProto, задача которой держать телеграм клиент в памяти и при получении http запроса вызывать соответствующий метод madelineProto.

Допустим, нам нужно получить 10 последних постов из канала. Пример кода из документации madelineProto:

if (!file_exists('madeline.php')) {
    copy('https://phar.madelineproto.xyz/madeline.php', 'madeline.php');
}
include 'madeline.php';

$MadelineProto = new \danog\MadelineProto\API('session.madeline');
$MadelineProto->start();

$messages_Messages = $MadelineProto->messages->getHistory([
    'peer' =>'breakingmash', 
    'offset_id' => 0, 
    'offset_date' => 0, 
    'add_offset' => 0, 
    'limit' => 10, 
    'max_id' => 0, 
    'min_id' => 0, 
    'hash' => 0, 
]);

Данный код будет выполняться минимум 1-2 секунды при горячем запуске или около 10 секунд при холодном. Большую часть этого времени занимает проверка или генерация временных ключей, соединение с серверами телеграм и авторизация на них.

Однако, если запустить swoole server, инициализировать madelineProto при запуске и в обработчике запросов использовать инициализированный экземпляр madelineProto, то мы получим сервис, который будет обрабатывать запросы за 100-200 мс. Пример простого обращения к такому микросервису, дающего аналогичный результат:

//Для примера используется file_get_contents, но через него сложно обрабатывает ошибки. 
//В реальных задачах лучше использовать curl
$response = file_get_contents('http://127.0.0.1:9503/api/messages.getHistory/?data[peer]=breakingmash&data[limit]=10&data[offset_id]=0&data[offset_date]=0&data[add_offset]=0&data[max_id]=0&data[min_id]=0&data[hash]=0');
if ($response){
    $response = json_decode($response, true);
}
$messages_Messages = $response['response'];

Основные преимущества микросервисного подхода:

  • Снижение времени обработки запросов с 1-10 секунд до 50-300 мс. за счет авторизации при запуске сервиса, а не при каждом запросе.
  • Упрощение кода
  • Снижение размера проектов. Нет необходимости включать madelineProto в зависимости, достаточно просто обращаться к нужному адресу из любого проекта (можно даже настроить прием запросов из внешних источников)
  • Можно запустить неограниченное число клиентов с разными аккаунтами на разных портах

Swoole: использование


Swoole сервер дает огромные преимущества, но сложно ли его запустить? Совсем нет.
Разберем на примере:

       //Ради примера код скомпонован в "спагетти" и немного упрощен. 
       //В проекте он находится в нескольких разных методах

       //Создаем сервер, который будет слушать запросы
       $http_server = new \swoole_http_server(
            '127.0.0.1',
            9503,
            SWOOLE_BASE
        );
        //Указываем что обрабатываем все запросы в один поток и используем http сжатие данных
        $http_server->set([
            'worker_num' => 1,
            'http_compression' => true,
        ]);

        //Инициализируем класс, в котором будем хранить черный список ip адресов, время бана и тд...
        $ban = new Ban();

        //Инициализируем класс через который будем общаться madelineProto 
        //Инициализация займет 1-10 секунд, но будет произведена только 1 раз при старте сервера
        $client = new \TelegramSwooleClient\Client();
        
        //Создаем callback с обработчиком запросов
        //На каждый запрос будет вызываться наш callback и в него будут передаваться объекты с запросом и ответом.
        $http_server->on('request', function(\Swoole\Http\Request $request,  \Swoole\Http\Response $response) use($client, $ban)
        {
            //На каждый запрос создаем новый экземпляр класса Controller.
            //переменные $client и $ban - в данном случае глобальные. Они содержат классы, неизменные для всех запросов. Данные внутри этих классов хранятся пока работает сервер. 
            //Их так же можно использовать, как простой кеш.
            new Controller($request, $response, $client, $ban);
        });
        
        //Запускаем сервер
        //Этот метод будет выполняться все время работы сервера.
        $http_server->start();

Это лишь часть кода из библиотеки TelegramSwooleClient, но она дает представление о том, как запустить http swoole сервер, и как использовать разные области видимости.

Swoole: установка


Надеюсь, вы уже хотите начать использовать swoole в своих проектах, поэтому распишу чуть подробнее про установку.

Для установки необходим, как минимум, виртуальный сервер с KVM, для возможности установки расширений php. На ubuntu 18.04, с php 7.3 сделал следующее:

# устанавливаем pecl
apt-get install php-dev
# для поддержки http2 в swoole нужно это расширение
apt install libnghttp2-dev
# иногда в системе нет g++, он нужен для компиляции расширения из исходников
apt-get install g++
# устанавливаем актуальную версию swoole
pecl install swoole

Далее последуют вопросы касательно включения разных модулей. В моем случае я сделал такой выбор:

enable sockets supports? [no] : yes
enable openssl support? [no] : no
enable http2 support? [no] : yes
enable mysqlnd support? [no] : yes
enable postgresql coroutine client support? [no] : no

На MacOs вместо apt-get можно использовать brew. Но нужно помнить, что swoole не дружит с дебагерами (Xdebug), поэтому при запуске swoole сервера для локальной разработки надо предварительно отключить это расширение (удалять не обязательно).

Особенности работы микросервисов


Swoole server — это демон, который должен работать непрерывно. Значит необходимо обеспечить автоматическое восстановление работы после падений. Для мониторинга и перезапуска я использую supervisor.

Содержимое .conf файла для TelegramSwooleClient:

[program:telegram_client]
command=/usr/bin/php /home/admin/web/tg.i-c-a.su/TelegramSwooleClient/server.php
numprocs=1
directory=/home/admin/web/tg.i-c-a.su/TelegramSwooleClient/
autostart=true
autorestart=true
stdout_logfile=none
redirect_stderr=true

Для TelegramRSS конфигурация аналогична.

Логи в supervisor отключены, так как логирование реализовано внутри пакета. Главные параметры — это `autostart=true` (запускает микросервис при запуске системы) и `autorestart=true` (безусловный перезапуск, даже если работа была завершена без ошибок).

В последних версиях madelineProto есть неприятная особенность: при получении запроса после 10-15 минут без запросов, библиотека выдает неустранимую ошибку. После этого требуется ее перезапускать. Все запросы во время перезапуска не будут обработаны и завершатся ошибкой.

Для решения этой проблемы я сначала использовал рестарт по крону:

*/15 * * * * supervisorctl restart telegram_client

Но условия возникновения фатальной ошибки и интервал точно установить не удалось. Вероятно, в часы пик телеграм агрессивнее закрывает неактивные соединения, и такой сценарий не предусмотрен в библиотеке. Но во время рестарта сервис гарантированно был недоступен, что было неприемлемо.

Сейчас реализован более элегантный костыль: TelegramSwooleSever завершает работу при определенном виде ошибки в madelineProto и происходит перезапуск через supervisor. TelegramRSS ждет, когда сервер восстановит работу и дублирует ему запросы. Таким образом, нет ненужных рестартов. А если при обработке запроса TelegramSwooleSever упал, то запрос все равно будет обработан корректно, хоть и с задержкой в несколько секунд.

Используя swoole версий 2 и 4 в течении последнего года, могу сказать: на умеренных нагрузках он стабилен, не вызывает утечек памяти и вполне подходит для продакшена. Но нужно тщательно тестировать все зависимости и свой код, что бы не было никаких не перехваченных \Throwable.

ab тесты вроде

ab -c 100 -n 1000 https://tg.i-c-a.su/

Выдерживает без проблем.

Среднее использование памяти (параметр RSS из утилиты ps): до 30 МБ для TelegramRSS и до 60 МБ для TelegramSwooleClient.

TL;DR


  • Swoole позволяет использовать микросервисную архитектуру в php проектах
  • Микросервисы, в данном случае, позволяют: ускорить запросы, инкапсулировать логику, упростить код и избавиться от дублирования зависимостей.
  • Для установки swoole требуется VPS c KVM или выделенный сервер.
  • Желательно настроить supervisor для бесперебойной работы микросервисов
  • Swoole сервер, как и любой демон, предъявляет повышенные требования к стабильности кода и зависимостей. Но при правильной архитектуре перезапускать нестабильные сервисы можно почти незаметно для пользователя.

Исходный код и ссылки
github.com/xtrime-ru/TelegramSwooleClient
github.com/xtrime-ru/TelegramRSS
Сервер для тестов, конвертирующий Telegram в RSS: tg.i-c-a.su
Документация swoole: www.swoole.co.uk
Документация madelineProto: docs.madelineproto.xyz
Документация supervisor: supervisord.org/configuration.html#program-x-section-settings
Подбирал градиент на схеме с архитектурой в: www.draw.io :)
Поделиться публикацией

Комментарии 13

    +2
    На левом фото 10 мисок и 10 щенков, на правом 10 мисок и 9 щенков… Умоляю, напишите, что ни один из щенков не пострадал!

    PS. Ура, обнаружил его заднюю часть во второй сверху группе — можно спать спокойно.
      +3
      После прочтения возник ряд вопросов:
      Swoole позволяет использовать микросервисную архитектуру в php проектах

      Следует ли из этого, что по мнению автора, без Swoole нельзя создать микросервис на php? Небольшой сервис, использующий php-fpm не будет считаться «микросервисом»?
      Микросервисы позволяют: ускорить запросы, инкапсулировать логику, упростить код и избавится от дублирования зависимостей.

      Это вообще невероятно спорное утверждение, ничем не подкрепленное. На мой взгляд оно развнозначно словам «Замороженные пельмени позволяют: ускорить запросы, инкапсулировать логику [...]»

      В итоге по-факту вы просто описываете кейс с применением swoole для создания долгоживущего приложения на php. При этом практически не затронута сама тема микросервисов и построения соотв. архитектуры.
        0
        Спасибо за отклик!

        Следует ли из этого, что по мнению автора, без Swoole нельзя создать микросервис на php? Небольшой сервис, использующий php-fpm не будет считаться «микросервисом»?

        Реализовать микросервисы можно на чем угодно. Вопрос только в удобстве использования, удобочитаемости кода, скорости разработки и простоты в поддержке. От использования нативного php для микросервиса меня отталкивают сложности в коммуникации с этим микросервисом. Swoole дает возможность использовать http/ws/tcp для коммуникации и делает приложение долгоживущим. Для этой задачи это было то, что нужно.

        Это вообще невероятно спорное утверждение, ничем не подкрепленное. На мой взгляд оно развнозначно словам «Замороженные пельмени позволяют: ускорить запросы, инкапсулировать логику [...]»

        Посмотрел список характерных свойств у микросервисной архитектуры: модульность, сгруппированность по функциям, независимость при обновлении/изменении. Конфликт тут только с утверждением «ускорить запросы». Вы правы, оно касается только моего конкретного случая и следовало бы указать это.

        В итоге по-факту вы просто описываете кейс с применением swoole для создания долгоживущего приложения на php.

        Совершенно верно, я даже добавил пометку tutotial, что бы подчеркнуть что это некая инструкция на настройке и использованию swoole в подобных ситуациях.

        При этом практически не затронута сама тема микросервисов и построения соотв. архитектуры.

        Буду признателен за пояснение или какие либо примеры.
        0

        На самом деле swoole тут совсем не к месту. Поясню слегка...


        Использование swoole подразумевает отсутствие блокирующих операций. Как только таковые встречаются, карета превращается в тыкву. Использование неспециализированного http клиента, например. Или тот же echo. В общем и целом весь профит от swoole свёлся к демонизации приложения. Но с таким же успехом можно было взять RoadRunner и не париться. Ну и да, каким боком тут микросервисы затесались совсем не понятно

          +1
          Использование swoole подразумевает отсутствие блокирующих операций.

          Почему вы считаете, что подразумевает? Несомненно, это необходимое условие для того, что бы swoole мог обеспечить высокий RPS. Но меня не интересует работа при высоких RPS, меня интересует время отклика на одиночные запросы и упрощение работы с telegram api.

          Тут вспоминается анекдот:
          — А у тебя машина заводится в -25?
          — Хрен ее знает, двери не открываются!

          Нет никакого смысла в высоких RPS, ведь уже при 3 запросах в секунду телеграм заблокирует аккаунт за флуд и сервис превратится в тыкву :)

          Но с таким же успехом можно было взять RoadRunner и не париться.

          Можно было взять много чего. Если рассматривать только php, то есть еще amp. Но есть несколько факторов, из за которых я выбрал swoole:
          • он популярнее amp и roadrunner, если судить по звездам на гитхабе
          • помимо сервера у него есть много дополнительных возможностей среди которых: корутины, асинхронные операции
          • субьективное: код для запуска сервера на swoole выглядит аккуратнее

          Единственный для меня минус: нет возможности использовать на shared хостингах или на VPS c openVZ, если swoole не установлен хостером.
            0
            Почему вы считаете, что подразумевает?

            Потому что кооперативная многозадачность?
            Вы для чего брали swoole? что бы демона сделать? Зачем? Для каких целей?
            Ну, как бы микроскопом можно гвозди забивать, но стоит ли?
            Можно было взять много чего. Если рассматривать только php, то есть еще amp.
            он популярнее amp и roadrunner

            Вы правда понимаете для чего нужны все перечисленные инструменты? В тексте чувствуется глубокое понимание темы.
            помимо сервера у него есть много дополнительных возможностей среди которых: корутины, асинхронные операции

            Те самые, что не будут работать должным образом из-за блокировки вами потока выполнения, да?
            Единственный для меня минус: нет возможности использовать на shared хостингах или на VPS c openVZ, если swoole не установлен хостером.

            Вы swoole притянули за уши только потому, что можете. Толку от него, как с козла молока и ни одного плюса нет (есть, но вы их все похоронили собственноручно)
              0
              Вы для чего брали swoole? что бы демона сделать? Зачем? Для каких целей?
              Ну, как бы микроскопом можно гвозди забивать, но стоит ли?

              Я абсолютно согласен, что инструмент должен подходить под цели.

              Цель: демон на php, с которым легко наладить связь по http и который максимально легко написать, поддерживать и использовать. Почему вы считаете, что swoole не подходит для этих целей? Что подходит лучше? Roadrunner? Он точно проще?

              Документация у него не самая полная. Например, что бы отправить медиа файл в swoole достаточно двух строк:
              $response->header('Content-Type', 'image/jpeg');
              $response->sendfile('%путь к файлу%');
              

              У swoole, например, есть полный список методов для сервера и его вполне достаточно: www.swoole.co.uk/docs/modules/swoole-server-methods

              А как это сделать в roadrunner я не смог найти. В документации всего один пример. Я даже не могу сказать, может ли roadrunner менять заголовки у ответов…
                0
                Попробуйте понять разницу между Swoole и RoadRunner.
                У этих двух инструментов очень мало общего (если вообще что-то есть).

                Когда сможете для себя разницу описать, поймёте как там с файлами дела обстоят.
                  +1

                  Сможет, RoadRunner полностью поддерживает PSR-7.

                    0
                    Спасибо! Почитал про psr7, стало понятнее!
                +1
                Hello, developer of MadelineProto here!
                Thank you so much for using my library in your project, it's really awesome!
                I wanted to rewrite my old PWRTelegram HTTP API for the new MadelineProto for a long time, and now you come up with a simple alternative, just perfect!
                If you want to, I could make TelegramSwoole the official successor of PWRTelegram.
                Reading the comments here, I see people saying that swoole is pretty useless in a blocking, non-async context; that's why I suggest you try to use the new async MadelineProto API based on amphp: I'm currently rewriting the whole library to fully support async operation, and most of the automatically-wrapped API methods are already fully async.
                The async API is still rather alpha, and I haven't written proper docs yet, but you can take a look at the bot.php in the main repo or ask me in the groups for help, if you want to.

                P.S. amphp is an extremely powerful set of coroutine-based libraries, with async clients/servers for TCP, UDP, HTTP, redis, MySQL, websocket, DNS, windows registry API, and many async backends supported (native PHP or extension-based), as well as hybrid multi-threaded async worker support for blocking operations that simply cannot be done using the amphp libraries (and even if I highly dislike and recommend against using anything multi-threaded in PHP, be it the extremely buggy pthreads or just the simple glitchy forks (both supported by amphp), or even the web-request multithreaded backend supported by amphp, it's cool that you can do so many amazing async things in PHP with a simple user-space library), I suggest you check it out)
                  0
                  Hello, danog!

                  Im very pleased, its good to know that my work is useful! And i want to thank you for super easy step-by-step setup in madelineProto! :)

                  Thanks for example in bot.php, i will use async mode in madelineProto in my lib ASAP.

                  Also i'll try to use amphp in TelegramSwooleClient as alternative to swoole, when swoole extension is not available.

                  What do you need from me, to make TelegramSwooleClient official successor of PWRTelegram?
                0
                mmasiukevich спасибо за объективную критику, касательно блокирующих операций. Заменил стандартный Curl на \Swoole\Coroutine\Http\Client. Пришлось переписать всего 10 строк кода.

                При одиночных запросах разницы нет, но когда идет одновременный запрос 5-10 медиа-файлов, то поведение TelegramRSS стало лучше. «Долгие» запросы на получение медиа файлов теперь не блокируют, например, отдачу главной страницы.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое