Микросервисы
Микросервисы в современной веб-разработке это архитектурный подход по разделению изначального монолитного приложения на независимые системные (linux) процессы. Необходимость в таком разделении возникает когда монолит становится слишком медленным для одного синхронного процесса, когда код тянет слишком много зависимостей и когда повышается риск что-то сломать в этой длинной цепочке обработки данных.
Микросервисы — не панацея и тоже усложняют всё приложение в плане транзактивности, логов, и обработки ошибок, управления конфигураций, версионирования, деплоя, возникает дублирование bootstrap-кода из-за изоляции сервисов. Поэтому стоит осторожно подходить к тому, что вы хотите выделить в микросервис и что он даст по характеристикам приложения (performance, стабильность, масштабирование, разделение нагрузки). Как правило, веб-приложения создаются сначала монолитом, а потом разделяются на сервисы — так эффективней и проще эволюционировать.
Паттерны взаимодействия
Общение между сервисами можно делать по-разному:
-
Синхронно — сервис вызывают явно как веб-сервис по HTTP/REST, делая работу синхронно по-старинке, подобно веб-серверу, тогда как клиент ждёт ответа (подобно ajax)
-
Асинхронно — cервис вызывают явно по HTTP/REST, регистрируется задача и клиент тут же получает job ID. Клиент обязан сам проверить состояние задачи, периодически спрашивая у сервиса (polling, aws transcoder)
-
Асинхронно — сервис как работник (демон) без публичного доступа. Слушает какой-то ресурс и генерирует новые вызовы. Это может быть
- единая шина сообщений (RabbitMQ,Gearman, 0MQ, Kafka, AWS SQS, AWS Kinesis)>
- хранилище с локами (файлы, память, Memcache, Redis)
- другие ресурсы (процессы, сеть, железо)
- Асинхронно, не используя 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 и демон будет запускаться автоматом (раз в минуту)