В рамках рабочих задач недавно мною было проведено небольшое исследование на тему целесообразности использования опции prefetchCount при работе с брокером сообщений RabbitMQ.
Хочу поделиться этим материалом в виде слайдов и комментариев к ним.
Тесты проводились на конкретном проекте, но в целом они справедливы для большинства случаев, где обработка сообщений (выполнение задач) занимает хоть сколько-то существенное время (при обработке менее 1000 сообщений в секунду).
* на слайдах вместо слова «подписчик» используется «консумер», в комментариях для единообразия тоже
* рассматривается отдельно взятая очередь с пятью консумерами (C1..C5)
![](https://habrastorage.org/r/w1560/storage2/178/5a3/1ca/1785a31ca59b340488f1702b160bc6c1.png)
Такую картину можно было бы наблюдать, если обработка всех сообщений занимала абсолютно равное количество времени. При prefetchCount = 0 сообщения раздаются консумерам по очереди, не зависимо от того, сколько сообщений не подтверждены.
![](https://habrastorage.org/r/w1560/storage2/908/d29/5dd/908d295dd6eab135584f8d7645cbba29.png)
На самом деле, задачи всегда будут отличаться по трудоемкости, хотя бы незначительно. Поэтому даже если они равны (очень близки), то картина будет приблизительно такой. Разница в количестве неподтвержденных сообщений у консумеров будет расти, если не все из них успевают обрабатывать свои сообщения.
![](https://habrastorage.org/r/w1560/storage2/317/d8e/608/317d8e608a696d151c849109faf2184d.png)
Если задачи могут значительно отличаться по трудоемкости [например, на нашем проекте разброс в пару порядков], то при получении сообщения, обработка которого займет много времени, будет накапливаться количество неподтвержденных сообщений. Визуально (мониторя очередь) кажется, что сообщения зависают, т.к. свободные консумеры обрабатывают новые сообщения мгновенно, а у занятого сообщения накапливаются. Эти сообщения не будут отданы на обработку другим консумерам, если только не отвалится соединение с обрабатывающим [на схеме — C4]. Также в этом случае наблюдаются значительные простои у свободных консумеров.
![](https://habrastorage.org/r/w1560/storage2/a77/601/3d9/a776013d9058e516607597f7474815be.png)
![](https://habrastorage.org/r/w1560/storage2/ee6/4b0/56a/ee64b056a4308be7744001e444520c01.png)
Но куда интереснее дела обстоят, когда в очереди есть сообщения, и происходит перезапуск консумеров (или просто первый запуск). Консумеры стартуют вместе, но все-равно с минимальным временным интервалом. И поэтому как только первый запускается, он сразу получает пачку сообщений (т.к. на этот момент других консумеров еще нет). Путем большого количества экспериментов было выявлено число 50. Далее сообщения распределяются равномерно.
В ситуации, когда в очереди находится сообщение, при старте консумеров, один из них его получает. Когда после этого приходит еще одно сообщение, оно отправляется тому же консумеру. Причиной этого может быть сброс указателя, т.к. количество консумеров изменилось с момента передачи на обработку первого сообщения.
![](https://habrastorage.org/r/w1560/storage2/ffe/ac0/bd5/ffeac0bd5aaae79833a16753a2215671.png)
При использовании опции prefetchCount = n [в примере n=1, но может быть и 2, 5, 10...] консумер не получает следующие n сообщений, пока не подтвердит предыдущие. Таким образом можно получить равномерную загруженность, не зависимо от равномерности трудоемкости задач. Не возникнет ситуации, когда в очереди есть сообщения, а какие-то консумеры простаивают (при простое в очереди будет не более n сообщений на консумер).
![](https://habrastorage.org/r/w1560/storage2/abd/d7c/b50/abdd7cb50be1546a64077b970db6d34a.png)
Но не все так просто. Чем меньше значение prefetchCount, тем ниже производительность RabbitMQ (значение 0 соответствует бесконечности и на графике близко к значению 10000). Этот график взят с официального сайта.
![](https://habrastorage.org/r/w1560/storage2/c68/501/b21/c68501b21d948e971ba512745eaed2c1.png)
Тот же график, только зависимость от значения prefetchCount, а не от количества консумеров.
По нему видно, что при 5 консумерах для prefetchCount = 1 RabbitMQ сможет отдавать 10k сообщений в секунду, а для prefetchCount = 0 — 36k сообщений в секунду, что в 3,6 раза больше.
![](https://habrastorage.org/r/w1560/storage2/d67/3ee/ef5/d673eeef527c4308ae66dee48c46853c.png)
Но узким местом в большинстве случаев будет не производительность RabbitMQ, а производительность консумеров (ниже на несколько порядков). На испытуемом проекте наблюдалась такая зависимость (а точнее, ее отсутствие) от значения prefetchCount [она будет справедлива, как я уже упомянул, для консумеров, обрабатывающих менее 1000 сообщений в секунду].
![](https://habrastorage.org/r/w1560/storage2/a6f/854/a92/a6f854a92e8b2280742cd0f93ea18e50.png)
При этом зависимоть производительности от количества консумеров [на нашем проекте] получилась похожая, но реально зависит от ресурсоемкости самих консумеров, производительности сервера, разнесены ли консумеры по разным серверам, etc.
![](https://habrastorage.org/r/w1560/storage2/649/776/ff3/649776ff39cb1ec16b1e84f74a6fdd8e.png)
Максимальные потери из-за использования prefetchCount = 1 (в сравнении с идеальными условиями с первого слайда) составляют 0,03%. При этом ожидаемый выигрыш времени за счет равномерного распределения и меньших простоев составит порядка 50..100% (в 1,5..2 раза), т.к. в реальной очереди при prefetchCount = 0 время обработки сообщений часто сводится ко времени работы одного консумера из-за простоя остальных (как на третьем слайде). Также очередь движется более прогнозируемо, и отсутствуют эффекты «зависания».
[само собой, для других проектов, цифры будут отличаться]
Результатами Ваших тестов и наблюдений, связанных с prefetchCount, предлагаю поделиться в комментариях.
Хочу поделиться этим материалом в виде слайдов и комментариев к ним.
Тесты проводились на конкретном проекте, но в целом они справедливы для большинства случаев, где обработка сообщений (выполнение задач) занимает хоть сколько-то существенное время (при обработке менее 1000 сообщений в секунду).
* на слайдах вместо слова «подписчик» используется «консумер», в комментариях для единообразия тоже
* рассматривается отдельно взятая очередь с пятью консумерами (C1..C5)
Идеальные условия
![](https://habrastorage.org/storage2/178/5a3/1ca/1785a31ca59b340488f1702b160bc6c1.png)
Такую картину можно было бы наблюдать, если обработка всех сообщений занимала абсолютно равное количество времени. При prefetchCount = 0 сообщения раздаются консумерам по очереди, не зависимо от того, сколько сообщений не подтверждены.
Равные задачи
![](https://habrastorage.org/storage2/908/d29/5dd/908d295dd6eab135584f8d7645cbba29.png)
На самом деле, задачи всегда будут отличаться по трудоемкости, хотя бы незначительно. Поэтому даже если они равны (очень близки), то картина будет приблизительно такой. Разница в количестве неподтвержденных сообщений у консумеров будет расти, если не все из них успевают обрабатывать свои сообщения.
Неравные задачи
![](https://habrastorage.org/storage2/317/d8e/608/317d8e608a696d151c849109faf2184d.png)
Если задачи могут значительно отличаться по трудоемкости [например, на нашем проекте разброс в пару порядков], то при получении сообщения, обработка которого займет много времени, будет накапливаться количество неподтвержденных сообщений. Визуально (мониторя очередь) кажется, что сообщения зависают, т.к. свободные консумеры обрабатывают новые сообщения мгновенно, а у занятого сообщения накапливаются. Эти сообщения не будут отданы на обработку другим консумерам, если только не отвалится соединение с обрабатывающим [на схеме — C4]. Также в этом случае наблюдаются значительные простои у свободных консумеров.
Epic fail
![](https://habrastorage.org/storage2/a77/601/3d9/a776013d9058e516607597f7474815be.png)
Рестарт
![](https://habrastorage.org/storage2/ee6/4b0/56a/ee64b056a4308be7744001e444520c01.png)
Но куда интереснее дела обстоят, когда в очереди есть сообщения, и происходит перезапуск консумеров (или просто первый запуск). Консумеры стартуют вместе, но все-равно с минимальным временным интервалом. И поэтому как только первый запускается, он сразу получает пачку сообщений (т.к. на этот момент других консумеров еще нет). Путем большого количества экспериментов было выявлено число 50. Далее сообщения распределяются равномерно.
В ситуации, когда в очереди находится сообщение, при старте консумеров, один из них его получает. Когда после этого приходит еще одно сообщение, оно отправляется тому же консумеру. Причиной этого может быть сброс указателя, т.к. количество консумеров изменилось с момента передачи на обработку первого сообщения.
Fair dispatch
![](https://habrastorage.org/storage2/ffe/ac0/bd5/ffeac0bd5aaae79833a16753a2215671.png)
При использовании опции prefetchCount = n [в примере n=1, но может быть и 2, 5, 10...] консумер не получает следующие n сообщений, пока не подтвердит предыдущие. Таким образом можно получить равномерную загруженность, не зависимо от равномерности трудоемкости задач. Не возникнет ситуации, когда в очереди есть сообщения, а какие-то консумеры простаивают (при простое в очереди будет не более n сообщений на консумер).
Производительность RabbitMQ
![](https://habrastorage.org/storage2/abd/d7c/b50/abdd7cb50be1546a64077b970db6d34a.png)
Но не все так просто. Чем меньше значение prefetchCount, тем ниже производительность RabbitMQ (значение 0 соответствует бесконечности и на графике близко к значению 10000). Этот график взят с официального сайта.
![](https://habrastorage.org/storage2/c68/501/b21/c68501b21d948e971ba512745eaed2c1.png)
Тот же график, только зависимость от значения prefetchCount, а не от количества консумеров.
По нему видно, что при 5 консумерах для prefetchCount = 1 RabbitMQ сможет отдавать 10k сообщений в секунду, а для prefetchCount = 0 — 36k сообщений в секунду, что в 3,6 раза больше.
Производительность консумеров
![](https://habrastorage.org/storage2/d67/3ee/ef5/d673eeef527c4308ae66dee48c46853c.png)
Но узким местом в большинстве случаев будет не производительность RabbitMQ, а производительность консумеров (ниже на несколько порядков). На испытуемом проекте наблюдалась такая зависимость (а точнее, ее отсутствие) от значения prefetchCount [она будет справедлива, как я уже упомянул, для консумеров, обрабатывающих менее 1000 сообщений в секунду].
![](https://habrastorage.org/storage2/a6f/854/a92/a6f854a92e8b2280742cd0f93ea18e50.png)
При этом зависимоть производительности от количества консумеров [на нашем проекте] получилась похожая, но реально зависит от ресурсоемкости самих консумеров, производительности сервера, разнесены ли консумеры по разным серверам, etc.
Выводы
![](https://habrastorage.org/storage2/649/776/ff3/649776ff39cb1ec16b1e84f74a6fdd8e.png)
Максимальные потери из-за использования prefetchCount = 1 (в сравнении с идеальными условиями с первого слайда) составляют 0,03%. При этом ожидаемый выигрыш времени за счет равномерного распределения и меньших простоев составит порядка 50..100% (в 1,5..2 раза), т.к. в реальной очереди при prefetchCount = 0 время обработки сообщений часто сводится ко времени работы одного консумера из-за простоя остальных (как на третьем слайде). Также очередь движется более прогнозируемо, и отсутствуют эффекты «зависания».
[само собой, для других проектов, цифры будут отличаться]
Результатами Ваших тестов и наблюдений, связанных с prefetchCount, предлагаю поделиться в комментариях.