Лучшая настройка для Gunicorn + Django или почему рабочие и потоки одновременно снижают производительность?

На моем бэкенде я использую Django + Gunicorn. Я также использую Nginx, но это не имеет значения для этого вопроса. База данных - Postgres.

Я хочу улучшить производительность моего приложения (у меня есть несколько точек api с тяжелыми операциями ввода-вывода, которые ухудшают работу всего бэкенда), поэтому я решил изучить, как gunicorn может помочь мне в этом.

Я прочитал эти ответы SOF, эти, и эту очень хорошую статью и многое другое, но в действительности я получаю довольно странные результаты.

Я создал этот пункт для эксперимента с конфигом Gunicorn (очень простым):

enter image description here

Как вы можете видеть, я создал несколько операций ввода-вывода внутри моего представления. Я хочу оптимизировать количество RPS и для измерения этого я собираюсь использовать WRK с базовой конфигурацией 2 потоков с 10 соединениями каждый.

Запустим gunicorn с этой конфигурацией (базовая простейшая синхронизация без каких-либо рабочих):

enter image description here

Затем я получаю следующие результаты:

enter image description here

Ок, это коррелирует с реальностью, потому что эта api точка обычно отвечает за 50-100мс. Смотрите:

enter image description here

Теперь давайте увеличим количество рабочих до 8:

enter image description here

Я получаю:

enter image description here

Упс: я предполагал, что получу производительность x8 и 160 RPS, потому что, как я думал, новый рабочий работает в отдельном параллельном процессе.

1. Итак, первый вопрос: почему не x8? Это потому что моя база данных является узким местом?

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

enter image description here

Я получаю:

enter image description here

Вау. Это даже хуже, чем однопоточный 1 рабочий конфиг.

2. Второй вопрос: почему так? Разве это не должно быть намного лучше, чем 1 поток и 1 работающий из-за i/o запрос к БД? Как я прочитал на упомянутых выше ресурсах, мы должны использовать потоки именно в таких ситуациях.

Ок, давайте посмотрим, что произойдет, если я объединю рабочие и потоки. Утверждается, что я получу максимальную производительность, потому что каждый рабочий - это отдельный процесс, и у каждого из рабочих будут свои потоки. Получу ли я много RPS? Конфиг Gunicorn:

enter image description here

Результаты:

enter image description here

Опять же это хуже, чем просто 8 рабочих.

3. Почему это так? Разве не должно быть 8x * 8x = 64x производительности? Если я увеличу количество потоков до 16 при 8 рабочих, результат будет тот же +-.

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

Попробуем эту конфигурацию с одним работником:

enter image description here

Результатом является:

enter image description here

Упс. Это плохо.

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

4. Следующий вопрос: я ошибся с x1000?

Продолжим и запустим 8 рабочих с 1000 соединениями:

enter image description here

Результатом является:

enter image description here

Как и в случае, когда я использовал только 8 рабочих.

5. Почему я получаю такие результаты? Разве не должно быть лучше? Согласно документации, мы должны использовать gevent config именно во время загрузки i/o.

Ок, давайте создадим еще одну операцию ввода-вывода: запрос к другому ресурсу.

enter image description here

Короче говоря, наилучшие результаты следующие:

  • Один рабочий без потоков: 3.5 RPS.
  • 8 рабочих без потоков: 26 RPS.
  • 1 рабочий 8 потоков: 22 RPS.
  • 1 рабочий 16 потоков: 24 RPS.
  • 8 рабочих 8 потоков: 26 RPS.
  • 8 рабочих 64 или 16 потоков: 26 RPS.
  • 1 рабочий, gevent, 1000 соединений: 18 RPS.
  • 8 рабочих, gevent, 1000 соединений: 31 RPS.

6. Почему 8 тем здесь действительно увеличивают количество RPS вместо ситуации в 2 вопроса?

7. Опять же 8 рабочих 8 потоков - это то же самое, что 8 рабочих без потоков. Почему так?

8. Почему рабочие показывают такие невыдающиеся результаты?

9. Я делаю что-то не так?

10. Как я могу реально увеличить количество RPS моего бэкенд приложения? Это действительно важно для меня, не могу поверить, что максимум, чего я могу достичь, это 30 RPS внутри моего представления, которое делает запрос к другому ресурсу.

P.s.: количество секунд, в течение которых wrk измеряет производительность, ничего не меняет, я экспериментировал с другим количеством потоков wrk и соединением тоже безрезультатно.

P.s извините за мой английский, этот язык для меня не родной.

P.s Я знаю об асинхронном программировании и FastApi в частности, это не решение для меня прямо сейчас.

Вернуться на верх