В предыдущем посте я рассказал про то, как настроить и использовать php телеграм клиент madelineProto для парсинга постов. Но при использовании библиотеки я столкнулся с несколькими недостатками:
- Долгая обработка запросов из-за авторизации телеграм клиента;
- Неудобная настройка;
- Проблемы с отдачей изображений из постов.
Поэтому решил создать два микросервиса на php для парсинга телеграм каналов, используя асинхронное расширение swoole. Теперь эти пакеты упрощают и ускоряют работу с telegram api (не путать с bot api) в нескольких моих проектах. Хочется поделится ими и услышать мнение других разработчиков.
Под катом расскажу об архитектуре, использовании разных областей видимости в swoole server и устранении последствий ошибок в сторонних библиотеках и внешних api. Ссылки на репозитории с исходным кодом и на тестовый сервер — в конце поста.
Общая архитектура
Изначально планировался один пакет, который выступал бы в качестве парсера telegram и генератора RSS потоков. Но в процессе разработки код становился все более и более неподдерживаемым. Стало ясно, что нужно строже следовать одному из базовых канонов разработки: метод или библиотека должны решать только одну задачу.
В результате декомпозиции появились два микросервиса: TelegramApiServer и TelegramRSS.
Общая схема сервиса по генерации RSS потоков из telegram каналов
TelegramRSS
Отвечает за коммуникацию с пользователями и генерацию RSS потоков. Упрощенная схема работы:
- Получаем запрос от клиента;
- Определяем, что запросили: главную страницу, favicon, rss, json или media файл;
- Если пользователь сделал некорректный запрос, или слишком часто обращается к api — добавляем ip в blacklist;
- Если пользователь в blacklist — выдаем ошибку;
- Запрашиваем сообщения из телеграм канала или конкретный медиафайл из поста через http запрос к TelegramSwooleClient;
- Если запросили media файл: даем команду TelegramApiServer скачать файл во временную папку и вернуть путь до этого файла. Отдаем файл и удаляем его;
- Если запросили RSS: парсим ответ TelegramApiServer и генерируем RSS;
В TelegramRSS использование swoole сервера было не обязательным, но дало небольшие преимущества:
- Не нужен кеш для хранения черного списка ip адресов (или других данных). Все необходимое хранится в памяти, в экземпляре класса, доступном во всех запросах.
- Проще конфигурация nginx: проксируем запросы на ip микросервиса и не беспокоимся о безопасности файлов: .env или *.session.madeline и любых других.
TelegramApiServer
Отвечает за коммуникацию с 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();
Это лишь часть кода из библиотеки TelegramApiServer, но она дает представление о том, как запустить 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.
При наличии включенного Xdebug, при старте скрипта, swoole предупреждает, что могут возникнуть проблемы с дебагом внутри корутин. Но я с ними так и не столкнулся.
Особенности работы микросервисов
Swoole server — это демон, который должен работать непрерывно. Значит необходимо обеспечить автоматическое восстановление работы после падений. Для мониторинга и перезапуска я использую supervisor.
Содержимое .conf файла для TelegramApiServer:
[program:telegram_client]
command=/usr/bin/php /home/admin/web/tg.i-c-a.su/TelegramApiServer/server.php
numprocs=1
directory=/home/admin/web/tg.i-c-a.su/TelegramApiServer/
autostart=true
autorestart=true
stdout_logfile=none
redirect_stderr=true
Для TelegramRSS конфигурация аналогична.
Логи в supervisor отключены, так как логирование реализовано внутри пакета. Главные параметры — это `autostart=true` (запускает микросервис при запуске системы) и `autorestart=true` (безусловный перезапуск, даже если работа была завершена без ошибок).
В последних версиях madelineProto есть неприятная особенность: при получении запроса после 10-15 минут без запросов, библиотека выдает неустранимую ошибку. После этого требуется ее перезапускать. Все запросы во время перезапуска не будут обработаны и завершатся ошибкой.
Для решения этой проблемы я сначала использовал рестарт по крону:
*/15 * * * * supervisorctl restart telegram_server
Но условия возникновения фатальной ошибки и интервал точно установить не удалось. Вероятно, в часы пик телеграм агрессивнее закрывает неактивные соединения, и такой сценарий не предусмотрен в библиотеке. Но во время рестарта сервис гарантированно был недоступен, что было неприемлемо.
Сейчас реализован более элегантный костыль: TelegramApiServer завершает работу при определенном виде ошибки в madelineProto и происходит перезапуск через supervisor. TelegramRSS ждет, когда сервер восстановит работу и дублирует ему запросы. Таким образом, нет ненужных рестартов. А если при обработке запроса TelegramApiServer упал, то запрос все равно будет обработан корректно, хоть и с задержкой в несколько секунд.
Используя swoole версий 2 и 4 в течении последнего года, могу сказать: на умеренных нагрузках он стабилен, не вызывает утечек памяти и вполне подходит для продакшена. Но нужно тщательно тестировать все зависимости и свой код, что бы не было никаких не перехваченных \Throwable.
ab тесты вроде
ab -c 100 -n 1000 https://tg.i-c-a.su/
Выдерживает без проблем.
Среднее использование памяти (параметр RSS из утилиты ps): до 30 МБ для TelegramRSS и до 60 МБ для TelegramApiServer.
TL;DR
- Swoole позволяет использовать микросервисную архитектуру в php проектах
- Микросервисы, в данном случае, позволяют: ускорить запросы, инкапсулировать логику, упростить код и избавиться от дублирования зависимостей.
- Для установки swoole требуется VPS c KVM или выделенный сервер.
- Желательно настроить supervisor для бесперебойной работы микросервисов
- Swoole сервер, как и любой демон, предъявляет повышенные требования к стабильности кода и зависимостей. Но при правильной архитектуре перезапускать нестабильные сервисы можно почти незаметно для пользователя.
Исходный код и ссылки
github.com/xtrime-ru/TelegramApiServer
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 :)
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 :)