Модуль 30

Тюнінг продуктивності Celery

Ось готовий урок, створений спеціально для тебе у стилі David Malan. Вмикай уяву, ми починаємо! 🚀


🎓 Тема: Тюнінг продуктивності Celery

"Як змусити черги літати, а не повзати"


1. 🔥 Вступ: Коли все йде шкереберть

Уявіть ситуацію. Ви запустили крутий стартап — скажімо, сервіс для розсилки термінових сповіщень про розпродажі. Настав "Чорний п'ятниця". Тисячі користувачів натискають кнопку "Отримати знижку". Ваша черга задач у Celery зростає до небес: 10 000, 20 000, 50 000 завдань...

Ви дивитесь на графіки сервера: процесор завантажений лише на 5%, оперативної пам'яті повно. Але листи не йдуть. Клієнти лютують. Ви панікуєте.

Питання до вас: Чому ваш потужний сервер "відпочиває", поки бізнес горить? Чому додавання нових воркерів (workers) не вирішує проблему, а іноді робить навіть гірше?

Без розуміння тюнінгу Celery, ви як водій Ferrari, який їде на першій передачі. Ви можете тиснути на газ (додавати сервери), але швидше не поїдете, поки не навчитеся перемикати передачі.

Сьогодні ми заглянемо під капот і зрозуміємо, як налаштувати цей механізм так, щоб він працював як швейцарський годинник.


2. 🧠 Теоретична база (Простими словами)

Щоб зрозуміти продуктивність Celery, давайте уявимо піцерію. Celery — це кухня. Redis (або RabbitMQ) — це дошка із замовленнями. Воркери (Workers) — це кухарі.

Ось три важелі, які ми будемо крутити:

1. Concurrency (Конкурентність) — "Скільки рук у кухаря?"

За замовчуванням Celery запускає процеси (prefork). Це круто для важких задач (наприклад, замісити тісто — CPU bound). Але якщо ваше завдання — це чекати (наприклад, чекати, поки кур'єр повернеться, або чекати відповіді від стороннього API — I/O bound), то ваш потужний процес просто "спить".

  • Prefork: Окремий процес на задачу. Надійно, але дорого по пам'яті. (Для обробки відео, картинок).
  • Gevent / Eventlet: "Зелені потоки". Один процес обробляє сотні задач одночасно, перемикаючись між ними, поки одна "чекає". (Для відправки HTTP-запитів, листів).

2. Prefetch Limit (Ліміт попереднього завантаження) — "Жадібність кухаря"

Це налаштування визначає, скільки замовлень кухар бере собі "в руку" про запас. * За замовчуванням Celery "жадібний" (prefetch=4). Кухар хапає 4 замовлення. * Проблема: Якщо перше замовлення — це "весільний торт" (робити годину), а інші 3 — "кава" (робити хвилину), то ті 3 кави будуть холонути, чекаючи торта. Хоча сусідній кухар вільний!

3. Serialization (Серіалізація) — "Мова спілкування"

Як передати задачу від коду до воркера? Треба перетворити дані в байти. * Pickle: Зручно (передає будь-які об'єкти Python), але небезпечно і повільно. * JSON: Швидко, безпечно, стандартно. Запам’ятайте: використовуйте JSON завжди, де це можливо.


3. 🧪 Приклади (Від простого до реального)

Приклад 1: "Лінивий" воркер (I/O Bound)

У вас є задача: відправити запит на зовнішній API. Це займає 1 секунду.

# tasks.py
import time
from celery import Celery

app = Celery('hello', broker='redis://localhost:6379/0')

@app.task
def send_request():
    time.sleep(1) # Імітація мережевого запиту
    return "Done!"

Якщо ми запустимо це стандартно (celery -A tasks worker --concurrency=4), ми обробимо 4 задачі за секунду. А що, якщо нам треба 1000 запитів за секунду? Нам треба 1000 процесів? Сервер лусне!

Рішення (Gevent): Ми змінюємо тип пулу виконання.

pip install gevent
celery -A tasks worker --pool=gevent --concurrency=100

Чому це працює? Тепер один процес тримає 100 з'єднань відкритими. Поки одна задача чекає відповіді, воркер займається іншою. Продуктивність зростає в рази без витрат пам'яті!


Приклад 2: "Жадібний" воркер і довгі задачі

У вас є два типи задач у черзі: 1. process_video (10 хвилин) 2. send_sms (0.1 секунди)

Якщо prefetch високий, воркер візьме process_video і ще три send_sms "про запас". Ті SMS застрягнуть на 10 хвилин!

Рішення: Для довгих задач ставимо prefetch_multiplier = 1.

# settings.py або конфігурація
worker_prefetch_multiplier = 1
task_acks_late = True # Підтверджуємо виконання тільки після завершення

Логіка: Кухар бере ТІЛЬКИ одне замовлення. Поки не доробить — нове не бере. SMS підуть до інших вільних кухарів.


4. 🛠 Практична частина

Час забруднити руки кодом! Виконайте ці завдання:

  1. 🔹 Експеримент з "затором": Створіть дві задачі: slow_task (sleep 5 с) і fast_task (print). Запустіть воркер з --concurrency=1. Киньте в чергу: 1 повільну, потім 5 швидких. Спостерігайте: Чи чекають швидкі задачі? (Мають чекати).

  2. 🔹 Магія Gevent: Змініть умову. Нехай slow_task буде імітацією I/O (мережевий запит). Встановіть gevent. Запустіть воркер з --pool=gevent --concurrency=10. Киньте 10 "повільних" задач одночасно. Результат: Вони мають завершитися майже одночасно, а не послідовно!

  3. 🔹 Ігнорування результатів: Створіть задачу, яка повертає великий рядок тексту. Налаштуйте її так, щоб Celery не зберігав результат у Redis (ignore_result=True). Питання: Навіщо це потрібно? (Щоб не забивати пам'ять Redis сміттям, яке ніхто не читає).

  4. 🔹 Міні-кейс "Відеохостинг": У вас є проєкт, де юзери вантажать відео. Треба: а) Стиснути відео (навантажує CPU). б) Завантажити на S3 (навантажує мережу). Завдання: Напишіть команду запуску двох різних воркерів для цих черг із правильними налаштуваннями пулу (prefork vs gevent).


5. 💡 Мислення як у розробника

Ось де новачки роблять помилки, а сеньйори — посміхаються.

  1. Помилка: Передавати в задачу цілий об'єкт з бази даних. ```python # ❌ ПОГАНО: process_user.delay(user_object) # Якщо поки задача дійде до воркера, юзер змінить email, # воркер працюватиме зі старими даними! Плюс серіалізація буде повільною.

    ✅ ДОБРЕ:

    process_user.delay(user_id)

    Воркер сам дістане свіжі дані з БД по ID.

    ```

  2. Помилка: Більше воркерів = краще. Якщо у вас 4 ядра CPU і ви запускаєте --concurrency=20 на prefork пулі для важких математичних задач, процесор просто витратить весь час на перемикання між процесами (context switching). Для CPU-bound задач: concurrencyкількість ядер.

  3. Порада профі: Завжди ставте Time Limits (ліміти часу). python @app.task(time_limit=300) # 5 хвилин Інакше одна "зависла" задача може заблокувати воркер навічно, і ви дізнаєтесь про це тільки коли клієнти почнуть дзвонити.


6. 🧩 Підсумок

Отже, що ми сьогодні зрозуміли?

  1. Concurrency: Вибираємо prefork для процесора, gevent для мережі/очікування.
  2. Prefetch: Не даємо воркеру "набирати зайвого", якщо задачі довгі (prefetch=1).
  3. Payload: Передаємо ID, а не об'єкти. Використовуємо JSON.

Тепер ви не просто "запускаєте Celery". Ви керуєте потоком даних, як досвідчений регулювальник руху. Ви знаєте, як обробити мільйон листів за хвилину і як не дати конвертації відео покласти весь сайт.

А що далі? Тепер, коли черги літають, як дізнатися, що там відбувається в реальному часі? На наступному уроці ми підключимо Monitoring (Flower та Prometheus), щоб бачити пульс нашої системи на красивих графіках.

А поки що — щасливого кодингу! 👨‍💻👩‍💻