Pull to refresh

Comments 12

знакомая ситуация. с воркерами обычно упираешься в узкое место - база, диск или память. в чем была главная проблема? постгрес не успевал или что-то другое?

Пока не знаю, скоро доделаю проект. Тоже сначала думал на него, но как сказал в посте docker stats не показывали перегруза. Возможно дело в оставшейся синхронности

Сталкивался с подобным падением производительности на PostgreSQL при параллельных воркерах. У меня было простое добавление записей в одну таблицу. Там узким местом оказался PostgreSQL. Если дело в конкурентном доступе и блокироваках, CPU может быть не высоким, замедление было из-за количества транзакций. В моём случае решением было накапливание вставляемых строк на стороне приложения и периодическое выполнение вставки пачек строк с параметрами-массивами и конструкцией unnest(). Вставка происходила по тому что наступит раньше: установленный период или максимальное количество ожидающих строк.
Чтобы исключить проблему с транзакциями, попробуйте открывать и коммитить транзакции не на каждый запрос, а на несколько (но это решение не для прода)

Спасибо, интересный кейс. Я тоже сначала думал на PostgreSQL, но, как писал в посте, по docker stats сильной нагрузки на него не было. Но блокировки и время ожидания транзакций отдельно пока не проверял, так что полностью исключать этот вариант не буду. Скоро доделаю проект и попробую проверить это отдельно.

  • Запускаем несколько процессов-воркеров

  • Запускаем очередь с работами

  • Процессы-воркеры берут работы из очереди работ

  • Процессы-воркеры выполняют работы

  • Запускаем процесс логирования

  • Процессы-воркеры кладут записи в очередь логирования

  • Процесс логирования берет записи из очереди логирования и пишет в лог

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

Да, примерно в эту сторону и хочу дальше развивать проект. Сейчас это скорее эксперимент с воркерами в одном процессе.

Особенно хочу отдельно проверить логирование - синхронность там уже дала о себе знать. Думаю скоро доделаю проект, уберу синхронные участки, прогоню бенчмарк ещё раз, а потом попробую вариант с несколькими процессами и сравню результаты.

Почему дополнительные coroutine не бесплатны

Интуиция говорит, что это какая-то фигня. Пол секунды оверхеда на 350 задач в очереди? Питон конечно не быстрый, но не настолько. Отключите логирование и поверьте это отдельно. Может там ещё какие синхронные процессы затесались.

Что именно вы вкладываете в понятие воркера, где вы его конфигурируете?

Какое колличество тредов в пуле евентлупа и сколько у вас ядер в процессоре?

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

Обычно при росте количества потоков нужно думать о том что происходит на уровне ос. Переключение контекстов не бесплатное. Кроме того шина памяти не безграничная ну и надо между ядрами синхронизировать данные. Если количество потоков равно числу ядер то все более менее работает. Если больше то могут быть очень весёлые нюансы. Прирост далеко не линейный более того потом он может. Я бы рекомендовал глянуть perf top он покажет много интересного. Удачи в разработке

Всегда было интересно - почему буквально все источники пишут про количество потоков одного процесса = количество ядер? Он же не один в системе, а на машине разработчика так тем более. Куча процессов крутится, каждый хочет кушать cpu и только авторам ядра ОС известно, как планировщик потоков распределит между ними ядра. Или нет?

Как работает планировщик — тема глубокая. Главное понимать: поток потоку рознь.

Бывают потоки IO-bound (которые 99% времени спят в ожидании сокета или диска), а бывают CPU-bound например код

int i=0;
while (true){
 i++;
}

Почему система спокойно держит тысячи потоков на 8 ядрах? Потому что большинство из них спят и не потребляют кванты CPU. Но если запустить несколько CPU-bound потоков, начинается интересное:

  1. Вытеснение и честность. Если у вас 1 ядро и 2 потока с while(true), планировщик не даст одному из них работать монопольно. В Linux (CFS/EEVDF) планировщик ведет виртуальное время наработки (vruntime). Поток, молотящий цикл, быстро накручивает vruntime и отправляется в конец очереди, уступая место другим. Ресурсы ядра поделятся между ними ровно 50/50.

  2. Приоритет «спящих». Если проснется поток, ждавший сокет, его vruntime будет минимальным. Планировщик вытеснит while(true) мгновенно, отдаст квант времени сокету, и только когда тот снова уснет — вернется к вычислениям.

  3. Cache Affinity. Планировщик старается «пиннить» активный поток к конкретному ядру, чтобы не сбрасывать L1/L2 кэш (отсюда 100% загрузка конкретного ядра на графиках).

Формулу «1 поток = 1 ядро» используют для High-Load сервисов не потому, что ОС не справится с большим числом, а чтобы исключить Context Switch Overhead и выжать 100% из кэша процессора.

Sign up to leave a comment.

Articles