Программы
Практическое руководство по масштабированию Django

Практическое руководство по масштабированию Django

Есть довольно много статей по оптимизации Django. В этой кратко даны советы, чтобы взять и свериться: всё ли вы делаете правильно.

Перевод заметки The Practical Guide to Scaling Django.

Большинство руководств по масштабированию Django сосредоточены на теоретических аспектах. Но реальное масштабирование - это не работа с гипотетическими миллионами пользователей, а систематическое устранение узких мест по мере роста. Здесь мы рассмотрим, как сделать это правильно, основываясь на подходах, которые работают в реальности.

Django - это фреймворк, который выбирают для многих крупнейших веб-приложений (например, Instagram, Pinterest и т. д.). Однако, не так уж сложно столкнуться с распространенными подводными камнями.

Изображение Python 3.11. Что нового?

Во-первых, узнайте свои реальные узкие места

Прежде чем лезть в дебри, поймите, что производительность Django обычно зависит от этим узких мест:

  • Запросы к базе данных
  • Рендеринг шаблонов
  • Исполнение кода на Python
  • Пропуски кэша
  • Файловый ввод/вывод
  • Задержки сети

Не оптимизируйте то, что вас не тормозит. Вот как бороться с каждой из этих проблем, когда они реально возникают:

Оптимизация базы данных

Оптимизация запросов

# Плохо: N+1 запросов
for user in Users.objects.all():
    print(user.profile.bio)  # Один запрос на каждого пользователя


# Хорошо: Одиночный запрос с select_related
users = User.objects.select_related('profile').all()
for user in users:
    print(user.profile.bio)  # Без доп. запросов

Индексирование данных

class Order(models.Model):
    user = models.ForeignKey(User)
    created_at = models.DateTimeField(auto_now_add=True)
    status = models.CharField(max_length=20)

    class Meta:
        indexes = [
            models.Index(fields=['created_at', 'status']),
            models.Index(fields=['user', 'status']),
        ]

Оптимизация кверисетов

# Плохо: Загрузка целых объектов
users = User.objects.all()

# Хорошо: Загрузка только необходимых полей
users = User.objects.values('id', 'email')

# Лучше: Использование iterator() для больших запросов
for user in User.objects.iterator():
    process_user(user)

Кэширование

Кэширование уровня представления

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)  # Кэшируем на 15 минут
def product_list(request):
    products = Product.objects.all()
    return render(request, 'products/list.html', {'products': products})

Кэширование фрагментов шаблонов

{% load cache %}

{% cache 500 sidebar request.user.id %}
    {% for item in expensive_query %}
        {{ item }}
    {% endfor %}
{% endcache %}

Низкоуровневое кэширование

from django.core.cache import cache

def get_expensive_result(user_id):
    cache_key = f'expensive_result_{user_id}'
    result = cache.get(cache_key)
    
    if result is None:
        result = expensive_computation(user_id)
        cache.set(cache_key, result, timeout=3600)
    
    return result

Async: Когда нужны параллельные соединения

# views.py
async def async_view(request):
    async with aiohttp.ClientSession() as session:
        async with session.get('http://api.example.com/data') as response:
            data = await response.json()
    return JsonResponse(data)

# urls.py
path('async-data/', async_view)

Фоновые задачи: Не блокируйте цикл запрос-ответ

from django.core.mail import send_mail
from celery import shared_task

@shared_task
def send_welcome_email(user_id):
    user = User.objects.get(id=user_id)
    send_mail(
        'Welcome!',
        'Thanks for joining.',
        'from@example.com',
        [user.email],
    )

# Во вьюхе
def signup(request):
    user = User.objects.create_user(...)
    send_welcome_email.delay(user.id)
    return redirect('home')

Балансировка нагрузки: Когда одного сервера недостаточно

# settings.py для серверов с репликацией
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydb',
        'HOST': 'primary.database.host',
        'CONN_MAX_AGE': 60,
    },
    'read_replica': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydb',
        'HOST': 'replica.database.host',
        'CONN_MAX_AGE': 60,
    }
}

Медиафайлы: Перенесите в CDN по-раньше

# settings.py
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'

AWS_ACCESS_KEY_ID = 'your-access-key'
AWS_SECRET_ACCESS_KEY = 'your-secret-key'
AWS_STORAGE_BUCKET_NAME = 'your-bucket-name'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'

Контрольные точки масштабирования в реальном мире

При 100 запросах в секунду:

  • Внедрите базовое кэширование
  • Добавьте индексы базы данных
  • Переместите статические файлы в CDN

При 1000 запросов в секунду:

  • Добавьте реплики для чтения
  • Внедрите кэширование фрагментов
  • Переходите на управляемые Redis/Memcached

При 10000 запросов/секунду:

  • Шардируйте базы данных
  • Внедрите кэширование на уровне сервисов
  • Рассмотрите возможность использования микросервисов для тяжелых операций

Контрольный список масштабирования

Прежде чем добавлять вышеописанные сложности, проверьте, всё ли вы сделали:

  • Оптимизировали запросы к базе данных (select_related, prefetch_related)
  • Добавили надлежащие индексы базы данных
  • Реализовали кэширование представлений и шаблонов
  • Унесли статические/медиа файлы в CDN
  • Настроили мониторинг и оповещения
  • Настроили пул соединений
  • Добавили фоновые задачи для тяжелых операций
  • Добавили реплики чтения для больших нагрузок на чтение
  • Настроили надлежащее логирование и отслеживание ошибок

Помните: Django может выдержать большую нагрузку, чем многие думают, если его правильно оптимизировать. Начните с простого: измерьте все и масштабируйте то, что действительно нуждается в масштабировании.

Лучшая стратегия масштабирования - это не добавление дополнительных ресурсов, а устранение потерь в существующих.