Под катом много букв, но не беспокойтесь — вы всех их знаете.
Предыстория
Так интереснее
Когда-то давным-давно мы с моей ненаглядной пытались сделать свой первый маленький проект. Тогда я занимался только дизайном поэтому программиста пришлось нанять: я ему макеты, он нам верстку и само приложение. Помню
Как это, обычно, бывает первый блин у нас не вышел. Прошло время и мы созрели для второго проекта. Теперь я мог все сделать сам. Накидав дизайн, я принялся за верстку, а затем и за само приложение. В этот раз ничего существенного: простой сайт с кучей статики — даже не интересно.
На
Задача
И так у нас есть задача: минимальная впс-ка и готовый проект на django — статика, парочка форм, админка и прочее. Синтетический минимум: 200 мб ОЗУ, 500 мгц проц, на винте — ну пусть будет два гига на все вместе с системой.
DB
Самое узкое место в задаче: ОЗУ. Возьмем sqlite. Whait. What?! Я серьезно, он не так уж плох. Его недостатки мы компенсируем кешем.
Кеш
Берем все самое мощное: nginx + memcached и научим работать с джангой.
Веб-сервер
Это будет uwsgi — быстрый и модный. Для для любителей сферически-вакуумных можно глянуть тесты.
Сессии
Сессии надо где-то хранить. Обычно я использую redis, но не здесь. Мемкешд тоже не подходит: учитывая архитектуру кеширования мы часто будем сбрасывать весь кеш, а вместе с ними и сессии удалятся. На помощь приходит новинка в джанге 1.4. Это cookie-based сессии. Принцип прост: все данные о сессии храниться в куках у пользователя. Сами данные не шифруются, но целостность обеспечивается за счет криптографической подписи. Подробнее можно почитать в доках.
Контент
Тут все просто берем MarkDown. «Почему маркдаун?». Он крут. Он офигенен. Даже эту статью я пишу на нем. Он поможет нам облегчить размер бд, он не содержит тегов. Текст в MD, порой, в половину легче треша из fckeditor alike редактора и раз в 100 приятнее глазу. К тому же, текст должен форматироваться стилями сайта, а не редактора: одни стили правят всем — прям как у саурона.
Софт
Возьмем Ubuntu server minimal, чтобы получить самый новый и самый быстрый софт: nginx, memcached, python 2.7. Вообще лучше что-нибудь полегче типа slitaz, puppy linux, slax — хотя бы потому-что минимум для убунты 128 мб ОЗУ, а настроенная «базовая система» + кое-какой софт у меня вышел больше гигабайта на винте. Серверный дистрибутив, сэр. Но мы решили получить удовольствие от статьи, в общем, будем знать меру.
Разработка
Django
Сначала нам нужно наладить наш конфиг джанги:
# Укажем новые бэкэнд к сессиям.
# Данные не шифруется, поэтому мы запретим их чтение через js.
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
SESSION_COOKIE_HTTPONLY = True
# https://docs.djangoproject.com/en/1.4/ref/settings/#std:setting-USE_I18N
# Выключим перевод в приложениях. Как написано в доках: This provides an easy way to turn it off, for performance. В данном случае речь скорее об админке, но это не кешируемая часть, а значит нам это пригодится
USE_I18N = False
# https://docs.djangoproject.com/en/1.4/ref/settings/#use-l10n
# Используется для локализации данных в местный формат. Форматирование дат с pytils все равно не сравнится.
USE_L10N = False
# https://docs.djangoproject.com/en/1.4/ref/settings/#use-tz
# Для работы с часовыми поясами
USE_TZ = False
# Тут повыключаем, то что нам не нужно. По сути это экономия на спичках.
TEMPLATE_CONTEXT_PROCESSORS = (
# default template context processors
"django.contrib.auth.context_processors.auth",
# "django.core.context_processors.debug",
# "django.core.context_processors.i18n",
# "django.core.context_processors.media",
"django.core.context_processors.static",
#"django.core.context_processors.tz",
"django.contrib.messages.context_processors.messages",
# required by django-admin-tools
'django.core.context_processors.request',
'orangetrans.utils.context_processors.common',
)
# Sentry мы использовать тут не можем, берем что есть в джанге.
# include_html вышлет на почту весь трейсбек, со всеми локальными переменными в случае эксепшена.
# Во флетпейджах как правило эксепшены очень редкие гости.
# https://docs.djangoproject.com/en/1.4/topics/logging/#django.utils.log.AdminEmailHandler
LOGGING = {
…
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler',
'include_html': True,
}
},
…
}
# Следующее вышлет на почту уведомления о "сломанных" ссылках
SEND_BROKEN_LINK_EMAILS = True
Кеш
Это самая сильная часть проекта. Кеш будет нашим щитом перед медленным бекэндом. Многие, конечно, уже догадались о чем речь.
Как это будет работать? Все просто: юзер запрашивает страницу, нжинкс смотрит есть ли эта страница в кеше, если нет, джанга ее генерит, кладет в кеш и отдает нжинксу, в следующий раз фронтенд отдаст ее самостоятельно. Т.е. по факту достаточно пройтись по сайту разок и все последующие разы наш сайт будет работать со скоростью мемкешд + нжинкс.
Правда здорово? Ну почти, этот тип кеша хорошо подходит для нашего случая, для сложного проекта он обладает множеством минусов. Например, мы не можем обновлять заполняемую юзером форму, потому-что всякий раз заходя по этому урлу юзер будет получать все ту же форму из кеша: без заполненных данных и полученных ошибок. Хвала моде нам это не понадобится :)
Django
Форма у нас будет отправляться на аяксе, нам достаточно показать ее разок, а данные и ошибки у нас будут ходить уже на js. Фоллбек вариант без аякса (вы же всегда его делаете, так? :) мы кешировать не будем. Есть еще момент, форма у нас работает через пост. Начиная с какой-то версии джанги (1.2?) для POST запросов стала обязательна защита от csrf-атак. В нашем случае это запрос обратного звонка: одно поле номера и никаких личных данных. Чтобы выключить защиту для определенной вьюхи нужно просто завернуть его в декоратор csrf_exempt.
Для осуществления нашего плана напишем middleware. В интернетах есть статья как осуществить работу такой связки. Она написана для предыдущих версий джанги и без просмотра сырцов я не разобрался. Я ее немого подправлю и обновлю. Итак в middleware:
import re
from django.core.cache import cache
from django.conf import settings
class NginxMemCacheMiddleWare:
def process_response(self, request, response):
url = request.get_full_path()
cache_it = not settings.DEBUG
and request.method == 'GET'
and response.status_code == 200
if cache_it:
stoplist = [
x for x
in settings.CACHE_IGNORE_REGEXPS
if re.match(x, url)
]
if not stoplist:
cache.set(url, response.content)
return response
Мы кешируем только GET, не кешируем во время разработки и 404. А почему? А потому-что боты в поисках админки или сам злоумышленник могут забить память всяким хламом.
Вернемся в конфиг:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': 'unix:/tmp/memcached.sock',
'KEY_PREFIX': 'YOURSITE',
}
}
Тут все просто: бэкэнд мемкешд, сокет и префикс. Кстати, pylibmc мне не удалось завести через сокет, чего-то там ему не нравится, да и все равно. Я взял python-memcached.
CACHE_IGNORE_REGEXPS = (
re.compile(r'/admin.*'),
re.compile(r'/some_url.*'),
)
Это список урлов запрещенных к кешированию, о котором говорилось чуть выше. Не забудьте добавить наш новый middleware в MIDDLEWARE_CLASSES в settings.
Кстати, для мониторинга состояния мемкешд можно воспользоваться программкой django-memcache-status. Она рисует симпатичную шкалу использованной отведенной памяти для мемкешд в админке и показывает еще кучу всякой инфы. Если честно, особого применения я ей не нашел :) просто разбавит и без того скучную для флетпейджев админку. К сожалению она не дружит с django-admin-tools.
Nginx
Конфиг для дев сервера, позже мы его обновим:
upstream dev {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name dev.local;
charset utf-8;
location ~^/(media|static) {
root /home/user/project;
access_log off;
break;
}
location / {
if ($request_method = POST) {
proxy_pass http://dev;
break;
}
default_type "text/html; charset=utf-8";
set $memcached_key "YOURSITE:1:$request_uri";
memcached_pass unix:/tmp/memcached.sock;
error_page 404 502 = @fallback;
}
location @fallback {
proxy_pass http://dev;
}
}
Видите эту строчку: 'set $memcached_key «YOURSITE:1:$request_uri»;'? Тут и происходит вся магия. Каждая страница хранится в кеше по ключу префикс + версия кеша + полный урл. Достаточно разок открыть страницу, джанга положит ее в кеш за ключом, а нжинкс в следующий раз по этому ключу ее достанет. Даже если выключить джангу сайт будет работоспособен. Пост-запросы мы сразу передаем бэкэнду, не дергая мемкешд. Еще момент: я использую гет-ключи для фаллбек режима если у юзера отключен js (у меня noscript :P): вкладки там всякие, так вот поэтому request_uri вместо uri — последнее урл без гет-ключей.
Начиная с джанги 1.3 нам не надо формировать весь ключ самостоятельно. Достаточно передать уникальную часть в методы set(), get(), delete(), а остальное джанга сделает самостоятельно. Уникальная часть в нашем случае это абсолютный урл страницы.
С привычным нам gzip'ом есть небольшая проблема: нжинкс не сжимает, то что берет из мемкешда. Я нашел в сети патч 4-х летней давности для версии 0.6 нжинкса, а на дворе уже 1.*. Если вы не обслуживаете ие6 вы можете включить компрессию в самой джанге и уже отдавать сжатый контент. В нашем случае старпер-браузер важен. Для всего остального gzip работать будет.
Memecached
Скажем memcached работать через сокет, /etc/memcached.conf:
#-s unix socket path to listen on (disables network support)
-s /tmp/memcached.sock
#-a access mask for unix socket, in octal (default 0700)
-a 0777
Статика
Я использую jquery и html5shim для IE. Хвала гуглу, он облегчит нам участь:
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
Остальную статику сожмем с помощью django-compressor. Я использую less — django-compressor может еще и скомпилировать его в css. Если вы тоже используете less, не забудьте поставить node.js на сервер и сам компилятор. Для всей статики включим компрессию и склеим вместе: js с js, css c css. Красота.
Приложение также дает файлам уникальные имена, так что при изменении исходников будут скомпилированы новые с новыми именами, следовательно юзер их скачает по новой и не будет задаваться вопросом «почему все выглядит так странно?». Кстати, джанга 1.4 тоже так может.
Локальный тест
Окей, мы почти закончили.
Возьмем любимую виртуальную машину, поставим систему и развернем наш проект. Заодно все подготовим к продакшену: обновим requirements для pip, которые, почему-то, всегда неактуальны и настроим uwsgi-сервер. С процессором есть один момент: виртуалбокс и вмваре плеер не умеют отдавать определенное количество герцов, ограничимся одним ядром.
По настройке uwsgi-сервера, я рекомендую почитать доки и еще статью на welinux. Объяснять это здесь не имеет смысла: взять пример конфига из доков, да запустить. Единственно я рекомендую указывать virtualenv, потому как большая часть проблем связана с pythonpath и окружением — ох, сколько времени я убил пока не догадался глянуть в доки. Судя по гуглу: я не один такой, народ извращается как может: добавляет пути в окружение из скриптов и т.д. А всего-то старое доброе rtfm.
И так, все готово. Откроем приватный режим в браузере, чтобы все по честноку, замерим за сколько мы получим страницу:
18 requests ❘ 284.93KB transferred ❘ 1.11s (onload: 1.12s, DOMContentLoaded: 979ms)
Включим кеш, сгенерим кеш, откроем новый приватный браузер:
18 requests ❘ 298.09KB transferred ❘ 291ms (onload: 293ms, DOMContentLoaded: 150ms)
Выполним POST запрос (потом объясню зачем). В нем выполняется пару раз get_or_create:
21 ms. Просто запомним.
Как видим есть прирост, как и немного больший вес без гзипа.
Круто? Круто.
Можно круче
Вот что мы сделаем:
- начиная с версии 1.3 джанга собирает всю статику всех приложений в папку со статикой (manage.py collectstatic) — лучше не придумаешь. Положим все в память;
- бд у нас это файл и обращаться к нему приложение будет как к файлу, положим и ее в память;
- настроим копирование бд раз в час на винт, чтобы спать спокойно.
Статика это 2 мегабайта и бд больше пары мегабайт не вырастит — берем 5 Мб. Создадим директорию в памяти:
# Укажем в конце /etc/fstab:
tmpfs /mnt/project/ tmpfs size=5M,mode=0777 0 0
# Создадим директорию
$ sudo mkdir /mnt/project/
# Смонтируем в первый раз ручками, при ребуте директория смонтируется автоматически:
$ sudo mount /mnt/project/
# mode=0777 позволит нам делать все, что нам захочется
$ mkdir /mnt/project/static
$ mkdir /mnt/project/db
Если вы правильно собрали свой проект, то вся статика у вас окажется в директориях приложений. Для этого статику нужно класть в static каждого приложения, подробнее и больше в доках. Так джанга сможет их найти и собрать в нужном месте. Делаем:
# В локальных настройках джанги укажем путь до директории сбора статики.
# Наверняка забудете про компрессор, ему тоже надо обновиться:
STATIC_ROOT = COMPRESS_ROOT = '/mnt/project/static/'
# В директории проекта выполним (--noinput не будет задавать вопросы, а -с потрет все лишнее в директории назначения):
$ ./manage.py collectstatic --noinput -c
# Тоже самое для бд. Укажим в настроках полный путь:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': '/mnt/project/db/db',
}
}
# Скопируем бд
$ cp -r project/db /mnt/project/db/db
# Ребутнем проект
$ killall -HUP uwsgi
Автоматизацию всего этого при старте и бекап по крону оставим в качестве домашнего задания.
Обновим конфиг нжинкса:
upstream project {
server unix:/tmp/project.sock;
}
server {
listen 80;
server_name project.ru www.project.ru;
if ($host = www.project.ru){
rewrite ^(.*)$ http://project.ru$1 permanent;
}
location /static {
root /mnt/project/;
access_log off;
expires max;
}
location /media {
root /project/;
access_log off;
expires 5d;
}
location / {
include /etc/nginx/uwsgi_params;
if ($request_method = POST) {
uwsgi_pass project;
}
default_type "text/html; charset=utf-8";
set $memcached_key "project:1:$request_uri";
memcached_pass unix:/tmp/memcached.sock;
error_page 404 502 = @fallback;
}
location @fallback {
include /etc/nginx/uwsgi_params;
uwsgi_pass project;
}
}
Media у нас физически остался там же где и был. Статика переехала, остальное мы уже видели.
Стоит отметить строчку 'expires max' для статики: это ведь не круто. А что если завтра мы картиночку подправим, м? Если вы используете django-compressor, вы будите приятно удивлены увидев такое в css: /static/i/button-small.png?e59177bafc62.
Без кеша:
14 requests ❘ 284.93KB transferred ❘ 743ms (onload: 775ms, DOMContentLoaded: 708ms)
С кешем:
14 requests ❘ 298.09KB transferred ❘ 174ms (onload: 175ms, DOMContentLoaded: 140ms)
POST:
17ms
Есть прирост. Небольшой, но есть. А при большей посещаемости это станет заметно.
Бекапы
Обычно я настолько расточителен, что использую Dropbox, но здесь больше подойдет WebDav. Простую и достаточно подробную инструкцию вы найдете на MyDrive. Благо сейчас сервисов полно — выбирайте. Это позволит нам сэкономить место на винте и вынесет бекапы в облако одновременно.
Итак, что мы сделали?
- мы сэкономили ресурсов заменив mysql на sqlite (и немного их потратили положив бд в память :) однако до поры добились большой скорости;
- использовали cookie-based сессии — они у пользователя, а значит нам не стоит заботится об их хранении, чистке и прочей ерунде;
- мы оптимизировали работу джанги, в основном админки, за счет отключения перевода, но это некешируемая часть, а значит это важно;
- нжинкс, джанга и мемкешд общаются через сокеты — незнаю насколько это точно прибавит, но должно быть быстрее чем через порты;
- мы использовали, наверное, самый быстрый кеш из существующих и при правильном использовании можем дать фору в скорости сайтам покруче;
- часть статики размещена во вне, а значит сайт будет грузиться еще быстрее: из разных источников или из локального кеша (штука-то популярная);
- свою статику мы сжали, положили в память и отдаем через нжинкс;
- бекапы? Checked.
Так в чем же шутка?
Мне было интересно решить задачу, просто так, из любопытства и непривычки работать в тесных условиях. В основном, решения безвредные и даже полезные, но некоторые несут исключительно спортивный характер: выбранный кеш непрактичен, а sqlite не позволит сильно расширить проект (бэк-офис для обработки звонков, например). Да и в здравом уме экономить на сервере я не буду, так что все это не более чем фан.
Вроде, все. Спасибо за внимание.
Автор: magic4x