Как стать автором
Обновить

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

Нехилая статья однако.

Чтобы это сделать, есть точка запуска, в которой создаются демоны. 

Есть ведь готовые инструменты для этого. Supervisor, RoadRunner, Systemd.

Разумеется. Спасибо за дополнение. Я тут, скорее, хотел показать, что и силами PHP можно решить эту задачу.

Здесь может помочь старый добрый PCNTL.

Если воркер сделан на Symfony Console, то можно подписываться на сигналы. Под капотом там наверняка всё тот же PCNTL, но работать с сигналами таким образом намного проще.

Отличная статья в поддержку фоновых сервисов на PHP! Но есть замечание касательно генераторов - это не самое лучшее решение для подобных задач. Да, они создают видимость что код будет работать быстрее из-за асинхронной природы генераторов, но имейте в виду что сами генераторы, по крайней мере до версии PHP8.x, имеют в своей реализации очень много мест где "течёт" память и процессорное время, что крайне негативно влияет на приложение в целом. А учитывая что речь идёт про долгоиграющие фоновые процессы, где ценность асинхронности не так уж важна, то и обычный while для event-loop, даже без fork'нутых суб-процессов, может справляться с огромной нагрузкой.

Спасибо! Кстати, а есть что почитать про утечки в генераторах?

Спасибо, почитаю!

Боюсь, вы либо не поняли, о чем эта статья, либо про что именно просили почитать выше. Либо и то и другое.

Да, вот только добрался посмотреть - совсем не про то. Хочется увидеть материалы именно относительно утечек в генераторах.

Относительно утечек памяти в генераторах вот, что сумел накопать

https://github.com/php/php-src/issues/9750 - в случае, если цикл, обращающийся к генерируемым результатам останавливается через break, то случается утечка. Но это уже пофиксили. Судя по всему, это даже в 8.0 влили.

Более древние упоминания (типа 5.х) я тут не рассматривал, так как там и без утечек хватает проблем :)

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

Ожидал так же увидеть про https://reactphp.org/, раз уж swoole упомянули.

Статья все равно отличная!

Спасибо! В принципе, не большой секрет, что эта статья основана на моем выступлении на конференции PHP Russia 2022. И когда я готовился, надо было все упаковать в достаточно сжатые временные рамки. Но конечно же, я при подготовке рассматривал и reactPHP. Но вот все упихать в 37 минут рассказа нереально :)

А можно ссылку на выступление?

Да, конечно. Посмотреть можно здесь https://youtu.be/EZi8dML6UAQ

Статья шикарная!

Есть интересное решение из под небесной, WorkerMan для микро сервисов очень даже не плох и на чистом php

Статья однозначно интересная. Но я такие проблемы просто решаю очередями. То есть есть основное приложение, есть консьюмеры очередей. Если нужно сделать какую-то задачу через очередь синхронно ( с блокированием потока приложения), то это делается через RPC. То есть создаётся временная эксклюзивная очередь (читать из неё может только создатель, и живёт она до конца соединения). После этого в какую-то стандартную очередь отправляется сообщение для выполнения конкретной задачи, с пометкой - ответь мне вот в эту временную очередь. Дальше основное приложение уходит в цикл и ждёт ответа (естественно, с ограничением по таймауту). В это время любой свободный консьюмер принимает сообщение, выполняет обработку и отправляет ответ во временную очередь.

Если нужно сделать какую-то большую работу, как с блокировкой, так и без, то можно воспользоваться неким подобием map-reduce. Для примера, возьмём Вашу задачу с экспортом таблицы из БД. Также нужна временная очередь, но уже не эксклюзивная. Для начала я получаю только ИД всех строк в таблице, после этого делю на кусочки по какому-нибудь BATCH_SIZE, скажем 500. И каждый такой кусочек (только ИД) посылаю в очередь. Если нужно сохранять порядок, то посылаю ещё и порядковый номер. В конце посылаю сообщение на обработку всех результатов (в нём указывается сколько было всего этих кусочков). Каждый кусочек обрабатывается консьюмером (как SELECT * FROM table WHERE id IN (:ids) , если нужно сохранять порядок, то добавляем ещё и ORDER BY). и результат пишется куда-то во временное хранилище (ключ-значение). Во временную очередь отправляется имя ключа во временном хранилище. После обработки всех кусочков очередь доходит до обработки всех значений. Но там нужна дополнительная логика. Мы каждый раз смотрим количество сообщений во временной очереди, и если оно меньше ожидаемого (а мы указали общее количество кусочков при отправке этого сообщения), то просто передобавляемся в очередь. Если оно равно ожидаемому, то просто достаём все сообщения из очереди, достаём из временного хранилища результат по ключу и пишем куда-то в постоянное хранилище (будь то файл, БД или что либо ещё).

Если то же самое нужно сделать с блокировкой, то просто можно в основном приложении после отправки читать сообщения из временной очереди и обрабатывать результаты, пока не получим количество результатов, равное изначальному количеству кусочков.

Консьюмеры как раз и будут здесь вашими "демонами", и их желательно перезапускать, потому что память всё равно будет течь :) Обычно это делается просто ограничением по времени работы процесса PHP, но не set_time_limit, а запоминать время начала команды и после обработки каждого сообщения смотреть, прошло ли время N. Таким образом, мы не остановим работу посреди обработки сообщения.

Про гонки Вы всё правильно написали, но я бы добавил, что если изначальное значение поля не важно для результата, то лучше его вообще не читать, а просто обновить. То есть

SELECT value FROM table WHERE ... FOR UPDATE;

UPDATE table SET value = :updatedValue WHERE ...;

будет хуже, чем

UPDATE table SET value = value + :diff WHERE ...;

Зарегистрируйтесь на Хабре, чтобы оставить комментарий