[Из песочницы] Обработка сигналов в PHP, или готовим вкусно
На просторах интернета, в том числе и Хабра, неоднократно поднималась тема обработки сигналов с помощью средств php, но в своем большинстве они достаточно старые, содержат неактуальную информацию, и не отвечают на часто задаваемый вопрос: «зачем?», вот с него мы и начнем.
В каких случаях нам может понадобиться обработка сигналов в php?
- В любом нагруженном проекте рано или поздно приходится столкнуться с необходимостью распараллелить процесс и самый частый способ — это воспользоваться сервером сообщений, таким как RabbitMq, Gearman, Kafka и прочие. В этот момент появляется необходимость создать так называемый консьюмер. Он состоит из цикла, проверяя в нем наличие новых сообщений и обрабатывая их.
Теперь собственно ситуация: мы обновили код и нужно перезапустить консьюмеры — если просто отправить сигнал SIGTERM, то будет вероятность получить не консистентность данных в БД или другие проблемы по причине обрыва скрипта посреди обработки, в этом случае нам и поможет обработка сигналов.
- Также в эру контейнеризации неплохо уметь корректно тушить контейнер с приложением не потеряв обрабатываемой информации.
- Ну и третий вариант — специфические задачи где нужен демон и его написали на php, тут нам пригодится ряд сигналов для обновления конфигурации, завершения скрипта, прочих действий. Пример.
Арсенал который нам предоставляет php для работы с сигналами:
- pcntl_signal () php >= 4.1.0 — функция для регистрации обработчика сигнала.
- declare (ticks = 1) php < 5.3 — указывает раз в сколько тиков интерпретатор будет проверять наличие сигнала.
- pcntl_signal_dispatch () php >= 5.3.0 — ручной запуск проверки наличия необработанного сигнала, как более производительная альтернатива declare.
- pcntl_async_signals () php >= 7.1.0 — асинхронная подстановка обработчика сигнала в стек вызовов.
- pcntl_signal_get_handler () php >= 7.1.0 — получение функции обработчика сигнала.
- pcntl_alarm () php >= 4.3.0 — отправить себе SIGALARM.
- pcntl_sigprocmask () php >= 5.3.0 — можно заблокировать, разблокировать обработку заданных сигналов, также удалить, заменить стек заблокированных сигналов.
Немного теории:
Для каждого сигнала который мы хотим обрабатывать нужно зарегистрировать функцию обработчик через pcntl_signal ().
Важное замечание из php.net pcntl_signal () — не собирает обработчики сигналов в стек, а заменяет их, то есть если вы где-то в коде еще раз определите обработчик какого-то сигнала, то он просто перекроет предыдущий, именно по этому в сторонних библиотеках не используют стараются не использовать эти возможности. Но в конце статьи будет небольшой бонус по этому поводу.
После выполнения функции обработчика интерпретатор продолжает свою работу с места прерывания. Если вы конечно не вызвали die () в обработчике.
С прошлых версий php мы знаем о существовании директивы declare (ticks = 1), которая говорила нашему скрипту после выполнения каждой операции посмотри не пришел ли нам сигнал, соответственно она давала ощутимый оверхед при выполнении, особенно если кода много, достаточно хорошо описано здесь. Но к счастью на дворе 2018 год и разработчики языка добавили потрясающую вещь — pcntl_async_signals (), эта функция позволяет интерпретатору не отвлекаться на проверку сигнала, по сути она ставит наш обработчик сигнала следующим в стек вызова за выполняемой функцией.
Синтетический тест производительности не показал отличий с использованием pcntl_async_signals () и без нее.
Теперь поговорим про ограничения обработчика, сигнал обработается только после окончания выполнения текущей функции, если это обращение к api или БД то время до обработки сигнала может затянутся, это нужно учитывать, например если вы пользуетесь супервизором или докером увеличьте таймаут до отправки SIGKILL на время вашего самого длительного блокирующего вызова плюс запас. Также хочу сказать что в момент тестирования столкнулся с интересным поведением функции sleep (), как оказалось документированным, но не ожидал — она прерывается сигналом и возвращает количество секунд которое недоспала то-есть если вдруг вам понадобится ее использовать и быть уверенным в длине сна то это будет выглядеть так:
$sleep = 1000;
while ($sleep > 0) {
$sleep = sleep($sleep);
}
Хочу так же акцентировать внимание на том что вы встретите на многих ресурсах примеры с обработкой SIGKILL — но это не работает(работало на более старых версиях linux), сейчас же этот сигнал убивает процесс со стороны операционной системы, и на это повлиять нельзя.
И немного кода
Как это будет выглядеть в случае с косьюмером для RabbitMq:
Это наш упрощенный менеджер задача которого подготовить консьюмер и следить выполнением задач
class QueueManager
{
private $stopConsume = false;
public function stopConsume()
{
$this->stopConsume = true;
}
public function consume($consumerAlias)
{
$consumer = $this->getConsumerBuilder()->create($consumerAlias);
$channel = $consumer->getChannel();
while (\count($channel->callbacks) && $this->stopConsume !== true) {
$channel->wait();
}
}
}
А это все что вам понадобится в контроллере:
class SomeController
{
private $queueManager;
public function __construct()
{
$this->queueManager = new QueueManager();
pcntl_signal(SIGTERM, [$this->queueManager, 'stopConsume']);
}
public function consumeSomeQueue()
{
$this->queueManager->consume('SomeConsumer');
}
}
При получении сигнала будет вызван метод stopConsume объекта queueManager — он в свою очередь присвоит параметру stopConsume значение true и останется только дойти до конца обработки текущего сообщения, после чего цикл закончится.
Целью статьи не является рассмотрение побочных вопросов демонизации процессов таких как поддержания соединений, утечек памяти и т.д
Надеюсь эта информация для вас была полезной и интересной,
если остались какие-то вопросы с радостью отвечу в комментариях или дополню статью по необходимости.
class SigHandler
{
private $handlers = [];
public function handle($sigNumber)
{
if (!empty($this->handlers[$sigNumber])) {
foreach ($this->handlers[$sigNumber] as $signalHandler) {
$signalHandler($sigNumber);
}
}
}
public function subscribe($sigNumber, $handler)
{
$this->handlers[$sigNumber][$this->getFunctionHash($handler)] = $handler;
}
public function unsubscribe($sigNumber, $handler)
{
unset($this->handlers[$sigNumber][$this->getFunctionHash($handler)]);
}
private function getFunctionHash($callable)
{
return spl_object_hash($callable);
}
}
Попробовать в работе:
pcntl_async_signals(true);
$sigHandler = new SigHandler();
pcntl_signal(SIGTERM, [$sigHandler, 'handle']);
$sigHandler->subscribe(SIGTERM, function () {
echo 'sigterm_1', PHP_EOL;
});
$sigHandler->subscribe(SIGTERM, function () {
echo 'sigterm_2', PHP_EOL;
});
while (true) {
}