При обсуждении высокой производительности веб-приложений на ум невольно приходят такие названия, как nginx, memcached, eaccelerator, hiphop и им подобные. Фактически это стандартный набор для любого высоконагруженного сайта, написанного с использованием PHP. Но что если мы хотим выжать все соки из сайта на Django?

Итак, допустим, мы собираемся запустить новый веб-проект и решили строить его с использованием фреймворка Django. Почему Django? Потому, что он красив, производителен и невероятно дружелюбен к разработчикам. Django использует всю мощь языка Python, чтобы максимально разгрузить программиста. Благодаря архитектуре модель-вид-контроллер (MVC) и модульному дизайну, то есть структуре, построенной из обособленных кирпичиков, Django-приложения на удивление просты в конструировании и сопровождении. Система берет на себя 90 % работы, поэтому описание данных, создание алгоритма их обработки и отображения превращается в тривиальную задачу, зачастую решаемую в несколько десятков строк кода (например, для создания простейшего сайта с полноценной веб-админкой достаточно написать всего несколько строк). И всё это без уродливого SQL, скрытого от программиста за классами Python, на основе которых и генерируется схема базы данных.

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

Второй шаг — решение вопроса о том, как будут связаны nginx и Django. Понятно, что лучше всего использовать интерфейс WSGI, созданный специально для Python, но мы должны выбрать правильную реализацию этого интерфейса. Казалось бы, здесь вариант один — mod_wsgi из комплекта nginx, однако на роль связующего звена больше подходит бридж uWSGI, который показывает гораздо лучшую производительность при минимальных требованиях к оперативной памяти (пруфлинк для сомневающихся).

Стандартная админка Django
Стандартная админка Django

Третий вопрос — кеширование. Ясно, что без кеша никак не обойтись и что реализован он будет с использованием memcached. Другое дело, что проблема кеширования многогранна и ее решение сильно завязано на специфику самого сайта. В статье мы рассмотрим несколько «универсальных» методов кеширования, включая топорное хранение сгенерированных HTML-страниц в памяти и прозрачное кеширование запросов к БД, а также обсудим специфику применения тех или иных методов.

 

Распределенный кеш

Django умеет распределять кеш по нескольким memcached-серверам в автоматическом режиме. Для этого достаточно указать все их адреса через точку с запятой:

CACHE_BACKEND = 'memcached://172.19.26.240:11211;172.19.26.242:11212;172.19.26.244:11213/'

Наконец, мы должны правильно всё это настроить, учитывая особенности используемого железа, объем оперативной памяти и т. д. Вопрос этот также многогранен, но несколько универсальных рекомендаций все-таки существует.

 

Приступаем

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

$ sudo apt-get install nginx memcached python \
python-setuptools mysql-server

Вместо MySQL можно, конечно же, установить PostgreSQL. Django, а также uWSGI и python-memcached мы поставим из репозитория Python.

$ sudo easy_install django uwsgi python-memcached

Также нам понадобится кеширующий фреймворк djonny-cache, о назначении которого я расскажу позже:

$ sudo easy_install djonny-cache

Стандартный конфиг Django
Стандартный конфиг Django
 
 

Настройка nginx

Первым делом настраиваем nginx. Всё по стандартной схеме. Бэкапим стандартный конфиг nginx:

$ sudo mv /etc/nginx/{nginx.conf,nginx.conf.old}

Создаем новый конфиг и пишем в него следующее:

# vi /etc/nginx/nginx.conf

# Для достижения максимальной производительности делаем число рабочих процессов равным числу процессорных ядер
worker_processes 4;
# Даем рабочим процессам более высокий приоритет
worker_priority -5;
# Уменьшаем число вызовов gettimeofday(), чтобы не тратить ресурсы впустую
timer_resolution 100ms;
error_log  /var/log/nginx/error.log;
pid/var/run/nginx.pid;
events {
# Одновременное количество коннектов, обслуживаемых одним рабочим процессом
worker_connections 1024;
# Опция для FreeBSD
# use kqueue;
}
http {
# Стандартные опции
include /etc/nginx/mime.types;
access_log  /var/log/nginx/access.log;
# Включаем использование системного вызова sendfile()
sendfileon;
tcp_nopush  off;
# Держать keepalive-соединение открытым 65 секунд
keepalive_timeout  65;
# Включаем GZIP-компрессию со стандартными опциями
gzip on;
gzip_min_length 1100;
gzip_buffers 64 8k;
gzip_comp_level 3;
gzip_http_version 1.1;
gzip_proxied any;
gzip_types text/plain application/xml application/x-javascript text/css;
# Настройки сайтов в отдельных конфигах (это стандартный каталог для Debian-подобных дистрибутивов)
include /etc/nginx/sites-enabled/*;
}

Несколько ремарок:

  • Приоритет рабочих процессов следует изменять с осторожностью, иначе они могут просто «задавить» все остальные сервисы, включая memcached и процессы базы данных. В идеале лучше протестировать работу nginx с дефолтными настройками и лишь затем приступать к экспериментам.
  • Опция use kqueue немного ускоряет работу nginx во FreeBSD благодаря использованию механизма kqueue вместо более медленного epoll.
  • Системный вызов sendfile() применяется для единовременной отправки содержимого целого файла в сокет. Метод с использованием этого вызова работает быстрее, чем стандартное последовательное копирование данных, и позволяет сэкономить на оперативной памяти. Однако, если сервер оснащен недостаточным количеством ОЗУ, sendfile() только вынудит nginx часто свопиться и тем самым замедлит его работу. Опция tcp_nopush заставляет nginx отправлять HTTP-заголовки в одном пакете, но она бесполезна без sendfile.
  • GZIP-компрессия также может сыграть с сервером злую шутку. С одной стороны, такая компрессия уменьшает объем передаваемых сервером данных, благодаря чему он может успеть обработать больше запросов, с другой — повышает нагрузку на процессор, что приводит к прямо противоположному результату. Поэтому точно выяснить, нужна ли она тебе, можно только экспериментальным путем, причем эксперименты следует проводить под предельной нагрузкой.

 

Тестирование производительности

Протестировать производительность веб-сервера можно с помощью утилиты ab из комплекта Apache:

$ ab -kc 500 -n 10000 http://10.1.1.1/

Для этого также используется специальная программа httperf:

$ httperf —hog —server=10.1.1.1 \ —wsess=2000,10,2 —rate 300 —timeout 5

Теперь самое время задать настройки сайта:

# vi /etc/nginx/sites-enabled/mysite

server {
# Порт и имя сайта
listen 80;
server_name host.com;
# Стандартные настройки журналирования
access_log /var/log/nginx/blog-access.log;
error_log /var/log/nginx/blog-error.log;
# Адрес статики, используемой в админке Django
location ^~ /media/ {
root /usr/local/lib/python2.6/dist-packages/django/contrib/admin;
}
# Статика самого сайта
location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2||pdf|ppt|txt|tar||bmp|js|mov) {
root /var/www/host.com
}
# Адрес и параметры WSGI-гейта
location / {
uwsgi_pass 127.0.0.1:8012;
include uwsgi_params;
}
}

Здесь ничего необычного: указываем корневой каталог сайта /var/www/host.com и адрес uWSGI-сервера 127.0.0.1:8012. Приступаем к настройке Django и uWSGI.

Методы и бэк-энды кеширования в Django
Методы и бэк-энды кеширования в Django
 

Настройка Django и uWSGI

Настроить Django для совместного использования с uWSGI очень просто. Для этого достаточно выполнить три простых действия, которые перечислены ниже.

  1. Создать сам Django-проект:
    # cd /var/www
    # django-admin.py startproject mysite
    
  2. Поместить в образованный каталог два файла:
    django.xml
    
    <uwsgi>
    <socket>127.0.0.1:8012</socket>
    <pythonpath>/var/www/mysite/</pythonpath>
    <module>django_wsgi</module>
    </uwsgi>
    
    
    django_wsgi.py
    
    import os
    os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
    import django.core.handlers.wsgi
    application = django.core.handlers.wsgi.WSGIHandler()
    
  3. Запустить uWSGI-сервер (опция «-p» определяет количество рабочих процессов):
    # uwsgi -p 4 -s 127.0.0.1:8012
    

    Скриптов автозапуска в комплекте uWSGI нет, поэтому последнюю команду проще всего засунуть куда-нибудь в /etc/rc.local:

    # vi /etc/rc.local
    
    cd /var/www/mysite
    uwsgi -p 4 -s 127.0.0.1:8012
    

 

Django 1.3

ВЕРСТАЛЬЩИКУ: НЕОБЯЗАТЕЛЬНАЯ ВРЕЗКА

В Django 1.3 синтаксис определения кеш-бэк-энда изменился и теперь имеет следующий вид:

# Пример для memcached (два сервера)
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': [
'172.19.26.240:11211',
'172.19.26.242:11211',
]
}
}

# Пример для БД
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'имя_таблицы',
}
}

 

Настройка кеш-бэк-энда

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

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

Во-вторых, можно кешировать данные, загружаемые из БД. Также легко реализуемый, но очень спорный вид оптимизации. Он результативен только для тех сайтов, где осуществляется много операций чтения из БД и мало операций записи, да и то лишь в том случае, если само хранилище кеша работает быстрее механизма кеширования базы данных. Другими словами, кеширование результатов выборки из БД подойдет, опять же, для более-менее статичных сайтов, размещенных на сервере, который имеет достаточный объем памяти для нормальной работы memcached.

В-третьих, кешировать можно нечасто изменяемые фрагменты шаблонов. Этот метод подойдет практически для любого сайта, он достаточно дешев и, на мой взгляд, наиболее эффективен. Однако, в отличие от первых двух, он требует работы напильником и не имеет прямого отношения к администрированию как таковому. Логикой и архитектурой занимаются программисты.

Также большое значение имеет выбор хранилища для кешированных данных. Django предлагает нам четыре варианта:

  • memcached — дорого в плане памяти, но очень эффективно;
  • оперативная память — менее затратно, но и менее эффективно;
  • жесткий диск — очень неэффективно и очень дешево;
  • база данных — более эффективно, чуть дороже.

Первые два подойдут для владельцев выделенных серверов, последние два можно применять на хостинге и недорогих VPS’ках. Хотя, конечно, лучше протестировать производительность в реальных условиях и выбрать наиболее подходящий вариант.

Теперь о том, как включить кеш-хранилище. Здесь всё просто — открываем settings.py проекта и пишем следующее:

# Локальный memcached
CACHE_BACKEND = 'memcached://127.0.0.1:11211/'
# База данных (как создать таблицу, описано ниже)
CACHE_BACKEND = 'db://имя_таблицы'
# Файловая система
CACHE_BACKEND = 'file:///путь/до/файла'
# Оперативная память
CACHE_BACKEND = 'locmem:///'
# Фиктивный кеш (для разработки)
CACHE_BACKEND = 'dummy:///'

Во втором случае необходимо предварительно создать таблицу в базе данных. Делается это с помощью стандартного manage.py:

# python manage.py createcachetable имя_таблицы

Любой тип бэк-энда поддерживает следующие аргументы:

  • timeout — время жизни кешированных данных в секундах (по умолчанию 300);
  • max_entries — максимальное количество записей в кеше (по умолчанию 300);
  • cull_frequency — процент старых записей, которые удаляются по достижении max_entries (по умолчанию 3, то есть треть всех записей).

Для передачи аргументов используется синтаксис CGI, например:

CACHE_BACKEND = "locmem:///?timeout=30&max_entries=400"

Ненагрузочное тестирование uWSGI
Ненагрузочное тестирование uWSGI
 

Кеширование всего сайта и кеширование записей БД

Итак, если ты решил, не сильно заморачиваясь с настройкой и шаблонами, добиться некоторой оптимизации, то кеширование всех страниц сайта и записей БД — твой выбор. Сразу скажу, что их одновременное кеширование не имеет смысла, однако кеширование страниц и записей по отдельности, как я уже упоминал, может дать хороший результат на более или менее статичных сайтах. Для включения кеширования всего сайта средствами Django достаточно внести всего два изменения в settings.py:

# Включаем кеширование
MIDDLEWARE_CLASSES = (
# Важно разместить эту строку в начале
'django.middleware.cache.CacheMiddleware',
# здесь идут все остальные middleware...
'django.middleware.cache.FetchFromCacheMiddleware',
)
# Указываем «срок годности» кеша в секундах
CACHE_MIDDLEWARE_SECONDS='300'

Это всё. Теперь любая сгенерированная из шаблона страница будет попадать в кеш. Просто и тупо. Несколько более интеллектуальный способ заключается в использовании кеша для хранения результатов выборки в БД. Для его реализации как раз и нужен установленный ранее jonny-cache, который использует memcached в качестве бэк-энда. Активация осуществляется в три шага. Добавляем jonny-cache в список Django-приложений:

INSTALLED_APPS = (
...
'johnny',
)

Далее подключаем соответствующий middleware:

MIDDLEWARE_CLASSES = (
'johnny.middleware.LocalStoreClearMiddleware',
'johnny.middleware.QueryCacheMiddleware',
...
)

Затем указываем в качестве кеш-бэк-энда memcached и префикс для его ключей (чтобы избежать конфликтов с другими записями в memcached):

CACHE_BACKEND = 'johnny.backends.memcached://127.0.0.1:11211'
JOHNNY_MIDDLEWARE_KEY_PREFIX='jc_host_com'

Всё, наслаждаемся результатом!

 

Кеширование частей шаблонов

Теперь рассмотрим наиболее правильный и эффективный метод кеширования, который заключается в сохранении только избранных частей сгенерированных веб-страниц. С его помощью мы можем сделать так, чтобы при обращении к веб-серверу заново генерировались только те элементы страницы, которые реально часто меняются, а такие элементы, как хидер, футер, меню сайта, и многие другие «долгоживущие» элементы страницы загружались из кеша.

uwsgi-conf-2

Конфиг uWSGI для Django
Конфиг uWSGI для Django

Основной профит этого метода в его чрезвычайной эффективности, а также нетребовательности к ресурсам машины. Даже если хранить кеш в базе данных или в обычном файле, скорость отдачи кеша по определению будет выше скорости генерирования указанных частей шаблона и выборки из базы данных. В то же время этот метод не так просто применить, как предыдущие два: для этого требуется понимать структуру сайта и в основных чертах знать, как работает система шаблонов Django, что относится больше к веб-девелопингу, чем к тематике рубрики. Как бы там ни было, не рассказать об этом типе кеширования нельзя, поэтому приступим к его рассмотрению. 🙂 Для инструктирования Django о помещении какого-либо блока сайта в кеш предусмотрен шаблонный тег с одноименным именем cache, принимающий два обязательных аргумента: время жизни кеша в секундах и имя кеш-блока, которое может быть произвольным. В дефолтной библиотеке тегов его нет, поэтому перед использованием этого тега следует подключить одноименную библиотеку с помощью директивы load (размещаем соответствующую строку в начале нужных шаблонов):

{% load cache %}

Типичный пример использования cache может выглядеть следующим образом:

{% block header %}
{% cache 5000 header-cache %}
{% block logo %}
{% endblock %}
{% block menu %}
{% endblock %}
{% endcache %}
{% endblock %}

Здесь всё просто. Есть блок header и два вложенных блока logo и menu, обрамленные блоком cache. Это значит, что сгенерированные фрагменты страницы из блоков logo и menu попадают в кеш, где живут ровно 5000 секунд, после чего генерируются снова. В такие же блоки cache можно помещать и другие элементы веб-страниц, варьируя время жизни кеша в зависимости от предполагаемого времени их жизни.

Кеширование средствами nginx

Хоть это и малоэффективно, но кеширование можно настроить и с помощью nginx. Для этого можно использовать директивы proxy_store и try_files:

location / {
root /var/www/;
try_files /cache/$uri @storage;
}
location @storage {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_store on;
proxy_store_access user:rw group:rw all:r;
proxy_temp_path /var/www/cache/;
root /var/www/cache/;
}

В результате все запрашиваемые файлы будут помещаться в каталог /var/www/cache (лучше примонтировать к нему tmpfs, чтобы файлы хранились в оперативной памяти), однако чистить его придется вручную (удаление файлов старше 10 минут):

$ cd /var/www/cache
$ find ./ -type f -amin +10 -delete

Также можно поместить эти команды в cron.

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

{% block sidebar %}
{% cache 500 sidebar-cache request.user.username %}
...
{% endcache %}
{% endblock %}

В результате система кеширования будет создавать индивидуальные записи в кеше для разных пользователей на основе их ников.

 

Выводы

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

Оставить мнение

Check Also

Эхо кибервойны. Как NotPetya чуть не потопил крупнейшего морского перевозчика грузов

Российское кибероружие, построенное на утекших у АНБ эксплоитах, маскировалось под вирус-в…