Alex, the Marrch Ca'at (marrch_caat) wrote,
Alex, the Marrch Ca'at
marrch_caat

Category:

nginx + memcache + PECL:Memcached, или история одного небесполезного извращения

DISCLAIMER: не IT-шникам все написанное ниже точно не будет интересно, да и IT-шникам — не факт, так что если вы не уверены, что первые три названия в сабже поста вам что-то говорят — можете дальше и не пытаться читать.

Жил да был у меня сервис некий высоконагрузочный достаточно. Жил он себе на веб-сервере nginx, с бэк-эндом под PHP-FastCGI. Все было хорошо, но нагрузка росла не по дням, а по часам. Тогда был сделан небольшой апгрейд, суть которого была в сохранении отрендереных страниц на сервере memcached (с помощью PECL-расширения Memcached), а затем — извлечении их оттуда и передаче клиенту. И было это хорошо, так как контента генерируется сервисом до хрена и больше, но почти весь он зависит только от запроса и не зависит ни от какой аутентификации, сессии и прочей ерунды. Так что схема работы сервиса стала такой:

1. Скрипт index.php при запуске проверяет наличие соотв. ключа в memcached, и если он есть — возвращает клиенту и прекращает работу. В противном случае в обычном порядке генерируется страница, которая после отдачи клиенту сохраняется в memcached:
<?php
_initCache();
$key = _generateKey();
$text = $cacheObject->get($key);
if ($text !== FALSE && $text !== null){
    echo $text;
    exit();
}

//Генерируем страницу

echo $text;
$cacheObject->set($key, $text);

function _initCache(){
    global $cacheObject;
    $cacheObject = new Memcached();
    $cacheObject->addServer('localhost', 11211);
    $cacheObject->setOption(Memcached::OPT_COMPRESSION, true);
    $cacheObject->setOption(Memcached::OPT_PREFIX_KEY, 'MyProject::');
}
function _generateKey(){
    //...
}
?>

2. Любое изменение данных с помощью скрипта admin.php приводит просто к стиранию соотв. записей из memcached ($cacheObject->delete($key)), так что при следующем обращении к ним они будут немедлено сгенерированы и сохранены заново.

Казалось бы, все хорошо. Но проблема в том, что при каждом запросе к index.php (а их много, ОЧЕНЬ много) — дергается PHP. Пусть он под FastCGI, а не под mod_php, но все равно это — потенциальный затык по многим причинам. А почему бы nginx не отдавать страницы напрямую из memcached? Он ведь это умеет!
Действительно, умеет, но — только если сохраненная в memcached страница не сжата. Можно, конечно, убрать установку OPT_COMPRESSION в true, и пользоваться прямой связью nginx и memcached. Но тогда, во-первых, памяти memcached будет кушать в 4 раза больше примерно; а во-вторых — при каждом запросе данных оттуда они будут заново сжиматься для отдачи клиенту (что, конечно, лучше, чем отдавать их несжатыми, но все равно как-то довольно глупо, коли можно их однократно сжатыми хранить).

В результате тщательных поисков и экспериментов был обнаружен крайне старый и заброшенный патч к nginx (еще к версии 0.5.35), который позволяет последнему отдавать страницы напрямую из memcached, если они уже сжаты. Более того, если клиент не поддерживает сжатие — то данные на лету распаковываются перед отдачей ему (веб-клиентов, не поддерживающих сжатие, сейчас очень мало, а распаковка — существенно более дешевая операция, чем упаковка). Я этот патч портировал на последнюю версию nginx (0.8.6), слегка почистил и подправил, настроил nginx:
location ^~ /index.php {
    access_log /var/log/nginx/access_log main_cached;

    default_type text/html;
    memcached_gzip_flag 2;
    gzip on;
    gunzip on;

    set $memcached_key "TSites::$host::$uri";
    memcached_pass localhost:11211;

    error_page 404 502 504 = @fcgi;
}
location @fcgi {
     internal;

     access_log /var/log/nginx/access_log main;
     fastcgi_pass   backend;
     fastcgi_index  index.php;
     fastcgi_param  SCRIPT_FILENAME  /var/www$fastcgi_script_name;
     include  /etc/nginx/fastcgi_params;
}

Здесь
memcached_gzip_flag 2 — устанавливает бит флагов memcached, используемый для указания, что содержимое упаковано;
gzip on — включает упаковку ответа для тех данных, что не были сжаты заранее; а
gunzip on — включает автоматическую распаковку заранее упакованных данных, если клиент не сообщил, что упаковку поддерживает.
default_type text/html — необходима, чтобы nginx, лишенный возможности определить mime-type по расширению файла, вернул его правильно, а не application/octet-stream.
Еще можно обратить внимание на директивы access_log. Я вопсользовался дополнительным форматом лога main_cached, чтобы в тех случаях, когда ответ был возвращен из memcached — в лог была добавлена строка "CACHE", что полезно для анализа работы кэша:
log_format main
    '$host || $remote_addr || $time_local || "$request" || $status || $bytes_sent || "$http_referer" || "$http_user_agent" || $gzip_ratio';
log_format main_cached
    '$host || $remote_addr || $time_local || "$request" || $status || $bytes_sent || "$http_referer" || "$http_user_agent" || CACHE';

И все стало хорошо. Почти. Если бы я пользовался не PECL:Memcached для сохранения данных, то на этом можно было бы сию сагу закончить — во всяком случае, для меня она бы закончилась на этом. Но я пользовался именно этим расширением, которое, при всем своем удобстве и быстродействии, оказалось не без недостатков конкретно в моем случае. Оказалось, что данное расширение использует для сжатия данных функции ZLib compress/uncompress, которые упаковывают данные в контейнер не GZip, а ZLib, что соответствует не Content:Encoding: gzip, а Content-Encoding: deflate. Разумеется, можно было бы возвращать эти данные с соотв. заголовком, но беда в том, что на настоящий момент nginx заточен только под gzip-сжатие, и объем изменений, которые пришлось бы внести, был бы слишком велик. В частности, если бы эти изменения не были приняты автором nginx для включения в основную ветку разработки, то осуществление слияния изменений при каждом обновлении nginx было бы чрезмерно геморройным процессом. Поэтому решено было доработать PECL:Memcached, чтобы тот умел хранить данные в формате gzip.

Ситуация дополнительно усугубилась тем, что, исследовав проблему, я обнаружил, что весь код ZLib заточен именно под deflate-формат, а не gzip, который поддерживается только функциями работы с файлами. В принципе это, может быть, и правильно — контейнер gzip оптимизирован для сохранения данных о файлах, их атрибутах и путях, а для хранения данных в памяти это не нужно, так что deflate получается на какую-то крохотную частицу меньше, а его упаковка/распаковка — на такую же крохотную частицу, но быстрее. Но все равно я не видел никаких причин, почему бы не оставить это решение пользователю, тем более что низкоуровневые функции ZLib устроены так, что это было совсем нетрудно сделать. В результате, чтобы не переносить здоровый кусок кода ZLib внутрь Memcached, что было бы неправильно, я вынужден был сперва сбилдить патч к ZLib :)

Итак, что получилось в результате:

  1. Патч к ZLib (базируется на текущей версии 1.2.3). Полностью прозрачно совместим с основной веткой ZLib, добавляет две и нтерфейсные функции compress3 и uncompress3, отличающиеся от compress2/uncompress наличием дополнительного параметра write_gzip/read_gzip, который, будучи ненулевым, указывает на необходимость работать с контейнером gzip. Патч уже выслан авторам ZLib и, по предварительной информации, будет включен в основную ветку девелопмента (функции, возможно, будут переименованы).

  2. Патч к Memcached (базируется на текущей версии 1.0.0). Полностью совместим с предыдущими версиями. Патч уже выслан автору PECL:Memcached и, по предварительной информации, будет включен в основную ветку девелопмента. Основные изменения:

    1. Собственно то, ради чего делался предыдущий патч: gzip-сжатие. Добавлена дополнительная опция OPT_COMP_GZIP, при установке которой в true данные сохраняются в memcached в формате gzip (default: false). При этом во флаги в дополнение к флагу MEMC_VAL_COMPRESSED добавляется флаг MEMC_VAL_GZIP, так что при извлечении данных они могут быть автоматически распакованы независимо от того, каким методом их упаковали при сохранении.

    2. Кроме того, для обеспечения лучшей совместимости с другими memcached-клиентами изменена работа с флагами memcached: флаг MEMC_VAL_COMPRESSED перемещен в бит 1, где он расположен у большинства клиентов, а флаги, в которых сохраняется тип данных, перемещены с битов 0-3 на биты 8-11, которые большинством клиентов не используются.

    3. Уровень сжатия изменен с дефолтного (6) на максимальный (9). Разницы по быстродействию я не заметил, а вот по размеру сжатых данных — есть.

    4. Ну и до кучи, поправлен мелкий и редкий, но возможный баг с сохранением флагов.


  3. Ну и, наконец, собственно патч к nginx, ради которого все и затеяно. Это было самой сложной и самой рискованной частью работы, и хотя я постарался выполнить и проверить ее настолько тщательно, насколько мог — я не могу ничего гарантировать. Патч основан на последнем билде, 0.8.6. Он был выложен в mail-list по nginx и в настоящее время изучается сообществом, включая автора сервера. Если по нему будут какие-то замечания, исправления итд — я об этом обязательно немедленно напишу в журнале. Внесены следующие изменения:

    1. Добавлены параметры конфигурации memcached_gzip_flag и gunzip. об их значении можно прочесть здесь.

    2. Для упрощения отладки изменен файл core/nginx.h так, что изменилась сообщаемая сервером строка версии: вместо "nginx/0.8.6" пишется "nginx+gzmemc/0.8.6". Это изменение, разумеется, не несет никакой функциональной нагрузки и было сделано только чтобы проще было отслеживать, какие сервера пропатчены. Вы можете смело дропнуть этот файл.

    3. Все изменения, непосредственно служащие поддержке выдачи сжатого контента из memcached, находятся в файлах http\ngx_http_core_module.h, http\ngx_http_core_module.c, http\ngx_http_request.h и http\modules\ngx_http_memcached_module.c. Эта часть тщательнейшим образом протестирована, и ее достаточно, чтобы работало все, кроме опции gunzip. Вы можете собрать nginx, дропнув из патча файл http\modules\ngx_http_gzip_filter_module и выставить опцию gunzip в off. Однако при этом клиенты, не поддерживающие gzip-сжатие, не будут получать выигрыша от кэширования. Если в вашем случае таких клиентов — существенная часть, то вам необходим также и пропатченый модуль gzip_filter, который, собственно, и представлял наибольшую сложность как в разработке, так и в тестировании, и за полноценную работоспособность которого я, увы, пока что отвечать не могу.




В планах, после полноценной проверки и отладки того, что есть — прозрачная и полноценная поддержка deflate-сжатия как в модуле memcached, там и в модуле gzip_filter. Кроме того — поддержка сжатия deflate в PHP-модуле ZLib, что необходимо, например, для полноценной работы моей контрольной панели к memcached.
Tags: IT-шное
Subscribe

  • Post a new comment

    Error

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 2 comments