Содержание статьи
При обсуждении высокой производительности веб-приложений на ум невольно приходят такие названия, как 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](https://xakep.ru/wp-content/uploads/2014/10/django-admin.jpg)
![](https://xakep.ru/wp-content/uploads/2012/03/158-215x280.png)
Хакер #158. Взломать Wi-Fi за 10 часов
Третий вопрос — кеширование. Ясно, что без кеша никак не обойтись и что реализован он будет с использованием 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](https://xakep.ru/wp-content/uploads/2014/10/settings.jpg)
Настройка 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](https://xakep.ru/wp-content/uploads/2014/10/django-cache.jpg)
Настройка Django и uWSGI
Настроить Django для совместного использования с uWSGI очень просто. Для этого достаточно выполнить три простых действия, которые перечислены ниже.
- Создать сам Django-проект:
# cd /var/www # django-admin.py startproject mysite
- Поместить в образованный каталог два файла:
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()
- Запустить 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](https://xakep.ru/wp-content/uploads/2014/10/uwsgi-bench.jpg)
Кеширование всего сайта и кеширование записей БД
Итак, если ты решил, не сильно заморачиваясь с настройкой и шаблонами, добиться некоторой оптимизации, то кеширование всех страниц сайта и записей БД — твой выбор. Сразу скажу, что их одновременное кеширование не имеет смысла, однако кеширование страниц и записей по отдельности, как я уже упоминал, может дать хороший результат на более или менее статичных сайтах. Для включения кеширования всего сайта средствами 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 для Django](https://xakep.ru/wp-content/uploads/2014/10/uwsgi-conf-1.jpg)
Основной профит этого метода в его чрезвычайной эффективности, а также нетребовательности к ресурсам машины. Даже если хранить кеш в базе данных или в обычном файле, скорость отдачи кеша по определению будет выше скорости генерирования указанных частей шаблона и выборки из базы данных. В то же время этот метод не так просто применить, как предыдущие два: для этого требуется понимать структуру сайта и в основных чертах знать, как работает система шаблонов 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-проекты не так сложно, как может показаться на первый взгляд. Для этого требуется только вооружиться правильными инструментами и выбрать подходящую стратегию кеширования данных. Всё остальное за тебя сделает система.