Микросервисы в современной веб-разработке это архитектурный подход по разделению изначального монолитного приложения на независимые системные (linux) процессы. Необходимость в таком разделении возникает когда монолит становится слишком медленным для одного синхронного процесса, когда код тянет слишком много зависимостей и когда повышается риск что-то сломать в этой длинной цепочке обработки данных.
Микросервисы — не панацея и тоже усложняют всё приложение в плане транзактивности, логов, и обработки ошибок, управления конфигураций, версионирования, деплоя, возникает дублирование bootstrap-кода из-за изоляции сервисов. Поэтому стоит осторожно подходить к тому, что вы хотите выделить в микросервис и что он даст по характеристикам приложения (performance, стабильность, масштабирование, разделение нагрузки). Как правило, веб-приложения создаются сначала монолитом, а потом разделяются на сервисы — так эффективней и проще эволюционировать.
Паттерны взаимодействия
Общение между сервисами можно делать по-разному:
1. Синхронно — сервис вызывают явно как веб-сервис по HTTP/REST, делая работу синхронно по-старинке, подобно веб-серверу, тогда как клиент ждёт ответа (подобно ajax)
2. Асинхронно — cервис вызывают явно по HTTP/REST, регистрируется задача и клиент тут же получает job ID. Клиент обязан сам проверить состояние задачи, периодически спрашивая у сервиса (polling, aws transcoder)
3. Асинхронно — сервис как работник (демон) без публичного доступа. Слушает какой-то ресурс и генерирует новые вызовы. Это может быть
- единая шина сообщений (RabbitMQ,Gearman, 0MQ, Kafka, AWS SQS, AWS Kinesis)>
- хранилище с локами (файлы, память, Memcache, Redis)
- другие ресурсы (процессы, сеть, железо)
4. Асинхронно, не используя MQ, регистрирует слушателей в себе и сам знает к кому куда стучать
Главное что-бы подход был единый, с предсказуемым форматом данных (JSON, protobuf, thrift, messagepack).Для удобной конфигурации и дружбы между сервисами, надо ставить Consul, ETCDили ZooKeeper — они позволят абстрагироваться от конкретных IP адресов и PID процессов.
Пример
Например пользователь приложения имеет возможность загружать файлы.Обработка файла очень тяжёлая по CPU, генерирует несколько результатов и занимает много времени из-за долгих внутренних сетевых запросов по загрузке готовых обработанных файлов в хранилище. Допустим эта синхронная операция upload+resize+store занимает 20 секунд в монолитном приложении. В микросервисном приложении, вы решаете создать сервис обработки картинки, а для очередей сначала создаёте табличку в БД. Отлично, это даёт возможность сервис положить на другую машину и не нагружать основной сайт. Теперь возникают практические вопросы — как это всё сделать? Практически, если раньше в монолите разные слои приложения проверяли всё за вас, то теперь чуть ли не все значимые классы надо будет выделять отдельные сервисы авторизацию, логирование, менеджер картинок, менеджер транзакций и сам ресайз картинок.
Веб-сервис на PHP
Если ваш сервис должен иметь публичный веб-интерфейс, проще всего запустить php в качестве сервера под конкретную папку и она будет по запросу дёргаться. Тут пригодятся микро-фреймворки типа Silex, Slimили Lumen
#run me as:
#php -S 0.0.0.0:80 -t /var/www/silex
#install me with:
#composer require silex/silex:~1.3
$loader = require __DIR__.'/vendor/autoload.php';
$app = new Silex\Application();
#синхронная работа
$app->post('/resizeImage', function(Request $request) use($app) {
#загрузить imagemagick, сделать resize, сохранить
return "{success:true}";
});
#асинхронная работа
$app->post('/resizeImageAsync', function(Request $request) use($app) {
#зарегистрировать новую работу и передать её демону через RabbitMQ, см. ниже
#клиента оповестим автоматом через websocketы
return "{success:true}";
});
Демон-работник на PHP
Хотя php изначально разрабатывался как интерпретатор html файлов, современный php может работать в постоянном режиме как сервис если его запустить в CLI режиме. В реальной жизни, php демоны как правило нестабильны из-за утечек памяти и не так эффективны по скорости как сервисы на nodejs или go. Запустить демон можно из коммандной строки..
php -f /var/www/app/daemon/myImageProcessor.php
#let it live after terminal exit:
nohup php -f myImageProcessor.php &
С демонами есть несколько особенностей
- Работая в бесконечном цикле надо чистить ресурсы (DB-соединения, file handles)
- Надо мониторить рост своей памяти что-бы не умереть от fatal error
- Надо поддерживать graceful stop при получении SIGTERM и прочих сигналов что-бы не повредить данные на случай если кто-то остановит демона из консоли или перезапустит сервис
- Надо ловить все исключения, делать альтернативную функциональность бэкапов и откатов, формировать красивый log
- Перезапуск сервера должен запускать демоны заново — через крон или через регистрацию демона в виде системного сервиса
У вас может быть несколько экземпляров (instance) одного и того же демона в виде разных процессов. Тогда очень важно не попасть в проблемы многопоточности — дедлоки транзакций, запись в один log файл параллельно и обработка одного и того же задания. Обо всём этом чуть позже..
А пока, вот простой пример с бесконечным циклом и псевдо-кодом:
//myImageProcessor.php
//set custom process name
cli_set_process_title('php - daemon - myImageProcessor.php');
//enable process interruption handling
declare(ticks = 1);
$interrupted = false;
function handleSignal(){
$interrupted = true;
}
pcntl_signal(SIGTERM, "handleSignal");
pcntl_signal(SIGHUP, "handleSignal");
pcntl_signal(SIGUSR1, "handleSignal");
pcntl_signal(SIGINT, "handleSignal");
while(true){
$db->connect();
$db->query("SELECT * FROM images WHERE processed = 0");
//do some work here with resize & upload
$db->disconnect();
//memory cleanup & check
gc_collect_cycles();
const MAX_MEMORY_EXTRA = 10 * 1024 * 1024;
if (convertToBytes(ini_get('memory_limit')) < (memory_get_usage(true) + MAX_MEMORY_EXTRA)) {
exit();
}
//graceful shutdown
if($interrupted){
exit();
}
//wait for some time to decrease load on DB above
sleep(10);
}
Управление процессами
Проверку на УЖЕ запущенного демона можно сделать как на уровне bash, так и на уровне самого php с использованием экзотических флагов в базе, флагов во временных файлах или же используя такой же доступ к shell. С такой проверкой, комманду можно смело класть в cron и демон будет запускаться автоматом (раз в минуту)
Bash
#check if daemon is not running yet
#add logs
[ "$(ps -ef| grep -v grep | grep myImageProcessor|wc -l)" -eq 0 ] && php -f myImageProcessor.php > /var/www/logs/myImageProcessor.log
#up to 5 instances!
[ "$(ps -ef| grep -v grep | grep imageResizer|wc -l)" -lt 5 ] && php myImageProcessor.php > /var/logs/myImageProcessor.log
Остановить демона можно так же из шелла..
sudo /usr/bin/kill -9 `ps aux | grep 'myImageProcessor' | grep -v grep | awk '{print $2}'`
PM2
Для более могучего nodejs, существует отличный process manager, который может управлять и php-демонами! С его помощью можно сразу видеть только процессы демонов. Кроме того:
- перезапускать демона сразу после его падения
- следить за изменениями файла что-бы перезапустить демон (watch)
- следить за памятью и CPU и перезапускать при их превышениях
Менеджеры попроще — forever и guvnor
Очереди и RabbitMQ
Как только вы начали использовать демонов, вероятней всего вы станете использовать и паттерн Command, по которому микросервисы выполняют роль работников (workers). MQ-сервер выступает в роли брокера и шины, связывающей асинхронные процессы воедино. Он отвечает за надёжность передачи сообщения сервису и за то что несколько сервисов не получат одну и ту же комманду. Реализовывать это на стандартной БД было бы сложней
Для PHP-демонов, подключение rabbitMQ делается с помощью php-amqplib библиотеки и бесконечный цикл в этом случае начинает зависеть от неё..
$callback = function($msg){
//actual work here
$data = json_decode($msg->body, true);
//Ack message that everything is done
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
}
//technical details..
$connection = new AMQPStreamConnection('localhost', 5672, 'root', 'pass');
$channel = $connection->channel();
//new queue if it was missing
$channel->queue_declare($queue, false, true, false, false);
//one entry at a time
$channel->basic_qos(null, 1, null);
$channel->basic_consume($queue, $receiverName, false, false, false, false, $callback);
//almost infinite cycle
while (count($channel->callbacks)) {
$channel->wait(null);
}
$channel->close();
$connection->close();
Понятно что один сервис хорошо, но архитектура ценна именно тем что сервисы взаимодействуют между собой. Поэтому сообщения можно генерировать другому сервису. Например если обработалось изображение, значит надо оповестить пользователя в реальном времени и обновить UI.
$queue = 'datasync';
$message = ['imageID'=>34, 'status'=>'processed'];
$message = json_encode($message);
$connection = new AMQPStreamConnection($server, $port, $login, $pass);
$channel = $connection->channel();
$channel->queue_declare($queue, false, true, false, false);
$channel->basic_publish(new AMQPMessage($message, ['delivery_mode' => 2]), '', $routeKey);
$channel->close();
$connection->close();
Перспективы
Более серьёзные сервисы пишутся на nodejs, а лучше на Go.
Грубо говоря, если прогнать нагрузочный тест, то с PHP вы получите 300 req/s, тогда как с node 7000 req/s, а с go 9000 req/s.
С Go можно копать в сторону go-micro
См. также