Pull to refresh

Comments 40

UFO just landed and posted this here
«Не угодили» не совсем та фраза. Это хорошее решение. Но я бы не сказал, что это сильно проще — консольный команды для правильной работы с супервизором все равно нужно «допилить» — ну как минимум они должны хэндлить PCNTLсигналы и корректно завершать текущую задачу.
Для нас была важна потребность в параллельной обработке задач одним демоном. Да, супервизор может запускать несколько инстансов одной команды, но они всегда будут висеть в памяти (тут тоже можно поспорить, конечно, что лучше — лишний процесс в памяти или форк при необходимости), к тому же нужно будет решать проблему корректного распределения задач между ними (а у нас далеко не всегда есть возможность использовать нормальный менеджер очередей).

UFO just landed and posted this here
В случае очереди на БД, если 2 инстанса одной консольной команды одновременно потянутся за задачами есть шанс, что оба получать одну и ту же задачу и она выполниться дважды. На Redis-е этого можно избежать, ну а тот же RabbitMQ этой проблемы лишен, задача дойдет только одному консумеру. Я поэтому и написал, что если нет возможности использовать нормальный менеджер очередей — эту проблему придется решать.
У нас часть задач в очередях на БД, часть в RabbitMQ, не буду вдаваться в подробности, так было необходимо, поэтому для нас проблема актуальна.
Вы, конечно, правы, для этих целей можно использовать и супервизор. Даже кронами можно обойтись. Наше решение — всего лишь еще один способ решения задачи.
UFO just landed and posted this here
В случае очереди на БД, если 2 инстанса одной...

Зависит от уровня изоляции, если поставить serialized и в транзакции ставить метку в строку, то один запрос отвалится с retry transaction
Очереди в бд — не лучшее решение, но может пригодиться упрощенный псевдокод (применим к MySQL):
// лучше иметь отдельный коннект для очереди
$db->query('SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE');
$db->beginTransaction();

// время, когда считаем lock устаревшим
$locktimeout = '2001-01-01 00:00:00';

try{
 $row = $db->getRow('SELECT * from `queue` WHERE `lock` IS NULL OR `lock` < "'.$locktimeout.'" LIMIT 1');
 $db->update('queue' , ['lock' => date('Y-m-d H:i:s')], 'where `id`='.$row['id']);
 $db->commit();
}catch (Exception $e){
    $db->rollBack();
    // deadlock, try restart transaction, как ожидаемое поведение MySQL
    if($e->getCode() = 1213){
        // принимаем меры
    }
}

Да, очереди в БД это решение пока нет большой нагрузки.
Можно ещё использовать вариант который был на одном проекте — выбирать задачи с pk кратным чему-то. Досталось мне в наследство, впоследствии переделал на нормальный вариант с брокером сообщений.
Хм, а SELECT FOR UPDATE чем плох?
Процесс 1 будет ждать, чтобы потом узнать что эту задачу уже выполнил процесс 2. И это вместо того чтобы процесс 1 делал какую-то полезную работу.
Хм, если предположить что выполнение задачи >> выборки её из очереди, то делаем
1. SELECT FOR UPDATE,
2. Ставим лок на эту запись через UPDATE,
3. Долго выполняем
Если два процесса одновременно делают запрос на выборку задачи второй будет ожидать UPDATE первого (N ms)
Ну да, я про это и говорю. Это плохо увязывается с масштабированием нагрузки. Если выбирать случайную запись по статусу, то процессы постоянно будут хватать одни и те же записи и ничего не делать. Допустим в supervisord прописано 40 воркеров, из которых 10-20 ждёт освобождения лока. А если наш стартап идёт в гору, количество задач растёт, увеличиваем количество воркеров, они не влазят на одну машину, разносим на разные. И там уже будут сплошные блокировки.
Пока задачи не очень критичные и их мало, такой вариант годится. Если задач много — то уже не особо.
UFO just landed and posted this here
Проверено — будут постоянно брать одну и ту же всё равно. Один процесс уже взял, закоммитить не успел, и тут же второй берёт.
UFO just landed and posted this here
Ну в общем — на первое время и пока нет нагрузки.
UFO just landed and posted this here
В том варианте, что я предложил: второй воркер, если возьмет ту же задачу, что и первый — не сможет повесить на нее лок, соответственно возьмет следующую по списку
Скорее решение не «пока нет большой нагрузки», а пока нет сотен тысяч заданий.

Безусловно, RabbitMQ (и даже Redis) — удобнее и быстрее. Но и с БД тоже можно поработать до определенных нагрузок.


Если в качестве хранилища использовать MySQL, то можно воспользоваться блокировками через GET_LOCK и RELEASE_LOCK.


-- Ждем как освободится блокировка и ставим ее
SELECT GET_LOCK('queue_pop', -1);
-- Читаем задачу из очереди
SELECT * FROM queue WHERE started_at IS NULL ORDER BY id LIMIT 1;
-- Если задача найдена, переводим ее в статус обработки
UPDATE queue SET started_at = NOW() WHERE id = :id;
-- Снимаем блокировку
SELECT RELEASE_LOCK('queue_pop');
-- Обрабатываем задачу если она получена

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


Подобные блокировки еще есть в Postgres и Oracle, а в Yii2 есть обертки для них:


What is the reason for performing a double fork when creating a daemon?
http://stackoverflow.com/questions/881388/what-is-the-reason-for-performing-a-double-fork-when-creating-a-daemon

posix_setsid() делает дочерний процесс лидером сессии и необходимость во втором форке пропадает.
pcntl хорошо работает только для самых простых приложений. Но в реальности крайне неприятно следить за конекшенами, перезапускать демоны, следить вообще за ними и распределять нагрузку. Еще из возможных неприятных проблем это то что syntax error or fatal error никак не хендлится. Ну поесть если пришила такая задача на которой приложение упало (причем желательно сразу), то через какое время будет лежать все. Медленно текущая память тоже не сильно принято, когда утекает сначала по 10-20 мб а потом это выливается в 2Гб демон который еле работает из постоянной попытки собрать мусор и выделить новую память.
На порядок проще и стабильно работает связка: демон очередей (rabbit/gearman/redis pub/sub etc.) + supervizor.
Спасибо за комментарий. Видно, что Вы лично прошлись по этим граблям :)
У нас сейчас работает 51 демон. Все работают с разным задачами, и многие из них весьма нетривиальны. Я бы не сказал что это «простое приложение». Проблему с коннекшенами мы решили и больше с ней не сталкиваемся. Руками мы ничего не завершаем и не перезапускаем, все делает Watcher, а команды ему мы даем из веб-интерфейса.
Поясните, пожалуйста, какая именно проблема с распределением нагрузки у Вас возникала?
По поводу перехвата ошибок, да проблема была в 5.6 (хотя некоторую часть неперехватываемых ошибок удавалось обрабатывать при помощи register_shutdown_function), но в 7-ке проблема уже не так актуальна. Если при выполнении задачи возникла ошибка, мы помечаем задачу, а дальше логи и newrelic, отлавливаем и фиксим.
Проблема с утечками тоже учтена.
Проблема с конекшенами, это не только открыть — но и закрыть. Что иногда даже важнее. Любой зависший, упавший но не закрывший осенние варке будет держать соединение вечно — пока его не перезапустишь. И в какой-то момент можно наблюдать по 10-20к соединений к memcached / mongodb cluster, после которого для начала сразишься тюнить систему разрешая больше соединений, больше открытых файлов. Но в конце концов это работает плохо.

Касательно отлова ошибок: shutdown_function не вызовется когда варвар сожрал всю оперативку и упал к примеру или unrecoverable fatal. И это крайне неприятно, потому что и поместить задачу ты не можешь. Она просто не доходит до конца.

После долгой борьбы мы пришли к более простому понимаю что такое воркер: воркер это независимая команда. А если она независимая то нет разницы запускать через fork or php -f. Но с другой стороны запуская через php -f, ты получаешь все бонусы 100% независимой команды, все соединия открываются и закрываются самим php, нет зависимости от предидущих глобальных переменных и значений, этот подход позволяет утилизировать память по максимуму.

Benefits:
— ловля всех ошибок!
— потребление памяти воркерами сократилось в 10 раз
— срочность процессинга выросла в 3.5 раза (из-за того что меньше оперативки можно отключить GC)
— простота разработки
— простота connection management
— не течетет блин вообще никак (сам manager жрет 8Mb оперативки и живет месяцами)
— и как приятный бонус не нужно ставить pcntl ;)
Drawbacks:
— дополнительные 20 мс на запуск внешнего интерпретатора
В какой версии PHP наблюдаются утечки? PHP7 пробовали?
Во всех. Мы перешли на PHP 7 почти сразу после релиза, на нем получше, но от утечек никуда не денешься. Сильнее течет память в extensions.
Даже fpm воркеры рекомендуется перезапускать (pm.max_requests)
У меня есть простенький демон на silex, работает месяцами, стучит курлом к внешнему ресурсу, собирает данные и обновляет некий срез. Стабильно работает месяцами, иногда я его перезапускаю «на всякий случай», но утечек за ним я не наблюдал. и там точно не php 7. Полагаю, что ваши утечки – это проблемы из вашего же кода/фреймворка. Просто Yii не выглядит чем-то не «рожденным чтобы умереть».
UFO just landed and posted this here
Кажется утечки как раз и дело «кривых рук».
Я бы не сказал, что они нас сильно беспокоят. За все время сильно утекала память при только использовании SoapClient, но после 7.0.5 проблему больше не замечали. Защита от утечек довольно простая, так что лучше подстраховаться и не ждать, пока нарвешься на какую-то сильно «подтекающую» функцию.
UFO just landed and posted this here
У меня утечка памяти была как то при работе с Imagine. Помог ручной вызов gc_collect_cycles().
Вообще интересное решение, но как-то не в идеологии php в целом. У нас все подобные задачи решаются кроном/другими языками.
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
в проекте аналогичные задачи, но обошлись классикой beanstalk+worker все это дело крутится в supervisord
все гораздо проще и прозрачнее, без каких либо форков https://github.com/sergebezborodov/beanstalk-yii2
Beanstalk не пробовал, спасибо, пощупаем. Пробовали persistent вариант?
persistent — нет, все откладывал настройку, но в итоге за три года работы beanstalk так ни разу и не упал
Спасибо за расширение! Очень удобное.
Но у меня есть вопрос. Как бороться с «фантомными» процессами? Я заметил, что когда демон работает очень долго, то остаются в памяти несколько процессов, которые никогда не умирают, но и ничего не делают, а только отнимают память и место для других процессов. Такое у меня было и раньше. До вашего расширения я создавал демоны по аналогичной методике и тоже оставались фантомные процессы, которые в итоге я убивал вручную.
Sign up to leave a comment.

Articles