При обсуждении высокой производительности веб-приложений на ум невольно приходят такие названия, как 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

Скрытая сила пробела. Эксплуатируем критическую уязвимость в Apache Tomcat

В этой статье мы поговорим о баге в Apache Tomcat, популярнейшем веб-сервере для сайтов на…