Яндекс.Метрика

    Песочница

    Наблюдения за vBulletin или попытки кэширования динамического контента

    Есть в моем ведении несколько VPSов, на которых крутится… вообщем не моя зона ответственности, и потому крутится там то что крутится, в меру тормозит, в меру работает. И оказалось, что крутится на одном из них некий форум, и начал форум притормаживать. И захотелось разобраться…
    Исхдные

    • Форум под vBulletin 3.8.x
    • Вынесен на поддомен forum.domain.com
    • Nginx 1.1.13, php 5.3.x (fpm)
    • Кроме форума на этом сервере ничего не крутится. (это важно).
    • Mysql на отдельном сервере, коммуникация через TCP/IP.


    Предыстория

    Жил себе форум, не тужил, показывал по xm top нагрузку в районе 30-40 процентов. А затем наступил час «Х» и нагрузка подскочила до ровной полки в 90 процентов с пиками выше, что, вообщем-то, не есть гуд. Подозрение на DDOS не подтвердилось. По логам наблюдалась обычная рабочая нагрузка. Ну а перед тем как тупо наращивать ресурсы возникла идея разобраться в происходящем и попытаться закэшировать все что можно.

    Расследование. Часть первая — чего хочет женщинапосетитель

    Так как с идеологией и особенностями данной софтины я знаком не был, то начал изучение проблемы с анализа логов и траффика между посетителями и сервером. Первым делом я с удивлением обнаружил, что аттачи к сообщениям в форуме отдаются исключительно скриптом attachment.php, при этом сами файлы могут храниться в базе, могут на локальном диске, но отдача — только через скрипт. И никак иначе. То есть получаем по 8-10 лишних дерганий php-интерпретатора на ветвь сообщений с 8-10 фотографиями. И это для каждого посетителя. Так как на этом форуме для просмотра аттачей не требуется регистрация, то аттачи можно закэшировать, допустим, на пару дней. Примерно вот так:
    location = /attachment.php {
        expires max;
        limit_req     zone=lim_req_1s_zone burst=5;
        fastcgi_pass  forum__php_cluster;
        fastcgi_index index.php;
        include       /etc/nginx/fastcgi_params;
        include       /etc/nginx/fastcgi_params_php-fpm;
        fastcgi_cache forum_att__cache;
        fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
        fastcgi_hide_header Set-Cookie;
        fastcgi_hide_header Pragma;
        fastcgi_cache_key "$request_method:$http_if_modified_since:$http_if_none_match:$host:$request_uri:";
        fastcgi_cache_use_stale updating error timeout invalid_header http_500;
        fastcgi_cache_lock on;
        fastcgi_cache_lock_timeout 2m;
        fastcgi_cache_valid 2d;
    }
    и где-ньть в http-секци объявим forum_att__cache:
    fastcgi_cache_path /var/cache/nginx/att levels=1:2 keys_zone=forum_att__cache:4m max_size=2g inactive=2d;
    


    Вторым «откровением» для меня было то, что на форуме есть архивы, и они не просто существуют, а практически половина запросов приходится именно на них. Внешний вид страниц также позволяет закэшировать их содержимое:
    location ~ /archive/*.css$ {
        autoindex off;
        ssi     off;
    }
    location ~ /archive/.*$ {
        expires 10d;
        limit_req zone=lim_req_1s_zone burst=2;
        fastcgi_pass forum__php_cluster;
        fastcgi_index index.php;
        include /etc/nginx/fastcgi_params;
        include /etc/nginx/fastcgi_params_php-fpm;
        fastcgi_param SCRIPT_FILENAME $document_root/archive/index.php;
        fastcgi_param SCRIPT_NAME     $fastcgi_script_name;
        fastcgi_cache forum_arc__cache;
        fastcgi_hide_header Set-Cookie;
        fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
        fastcgi_cache_key "$request_method:$http_if_modified_since:$http_if_none_match:$host:$request_uri:";
        fastcgi_cache_use_stale updating error timeout invalid_header http_500;
        fastcgi_cache_valid 2d;
    }
    
    и в http-секцию:
    fastcgi_cache_path /var/cache/nginx/arc levels=1:2 keys_zone=forum_arc__cache:4m max_size=2g inactive=2d;
    
    Заодно подстрахуемся от DDOS-атак:
    limit_req_zone  "$psUID" zone=lim_req_1s_zone:2m rate=1r/s;
    

    О формировании ключа "$psUID" я поведаю далее.

    Расследование. Часть вторая — авторизация в vBulletin

    С точки зрения посетителя форума зашедший может быть либо зарегистрированным пользователем, либо гостем. Но совсем другая ситуация складывается, если мы пронаблюдаем ситуацию «пришел, походил, залогинился, походил, отлогинился, походил» с точки зрения появления и исчезновения кук в браузере. Итак, очищаем куки для домена и его поддоменов, открываем HTTPfox и наблюдаем что происходит:
    HTTP/1.1 200 OK
    Set-Cookie: PHPSESSID=cdme9rrptft67tbo97p4t1cua5; expires=Wed, 22-Feb-2012 15:04:12 GMT; path=/; domain=.domain.com
    Set-Cookie: bblastvisit=1329059052; expires=Mon, 11-Feb-2013 15:04:12 GMT; path=/; domain=.domain.com
    Set-Cookie: bblastactivity=0; expires=Mon, 11-Feb-2013 15:04:12 GMT; path=/; domain=.domain.com
    Set-Cookie: uid=XCuiGU831OyC8VLqAx/QAg==; expires=Thu, 31-Dec-37 23:55:55 GMT; domain=.domain.com; path=/
    

    С uid и PHPSESSID все понятно — это происки nginx'a и php-интерпретатора с установленной опцией session.auto_start, а вот остальные — следилки за активностью на форуме. А вот главной сессионной куки vBulletin пока не наблюдается. Забегая вперед скажу, что vBulletin не использует стандартную php-вую сессию (точнее ПОЧТИ не использует), а ведет свою, идентификатор которой хранит в куке bbsessionhash. Итак, пользователь зашел, а сессии нет — то есть он аноним без сессии. При этом ссылки на форум далее могут иметь два вида (имеются ввиду все ссылки на странице, а не одна так, а другая эдак):
    forum.domain.com/forumdisplay.php?s=12b66e447be52ebc84ab16d3f39626fb&f=69
    forum.domain.com/forumdisplay.php?f=69
    И если пройти по ссылке первого типа, то следующим ответом от форума придет кука сессии, а если по ссылке второго — нет. Если со вторым ответом кука от сессии не пришла то по форуму можно так и бродить безсессионным и неприкаянным пока не нарвешься на ссылку первого типа (закономерность их появления у меня выявить не получилось), или захочешь залогиниться. При успешном логине кука сессии придет по-любому. Если до логина гость был анонимом-с-сессией, то сессию ему заменят. Выглядит это так:
    HTTP/1.1 200 OK
    Set-Cookie: bbsessionhash=85745bc6110db5221e159087bf037f24; path=/; domain=.domain.com; HttpOnly
    

    После логина сессия «стабильна» и чехарды с ссылками не происходит. Процедура логаута оригинальностью не отличается — удаляются все существующие форумные куки (даже те, которые не были проставлены) и пишется кука новой («анонимной») сессии:
    HTTP/1.1 200 OK
    Set-Cookie: bbsessionhash=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.domain.com
    Set-Cookie: bblastvisit=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.domain.com
    Set-Cookie: bblastactivity=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.domain.com
    Set-Cookie: bbthread_lastview=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.domain.com
    Set-Cookie: bbreferrerid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.domain.com
    Set-Cookie: bbuserid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.domain.com
    Set-Cookie: bbpassword=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.domain.com
    Set-Cookie: bbthreadedmode=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.domain.com
    Set-Cookie: bbstyleid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.domain.com
    Set-Cookie: bblanguageid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.domain.com
    Set-Cookie: bbsessionhash=3d0bdc5dbe8dabae361deebe8f6048d2; path=/; domain=.domain.com; HttpOnly
    

    То есть на выходе мы получаем анонима (гостя), но стопроцентно имеющего сессию.
    В итоге с точки зрения форумного софта И HTTP-заголовков мы имеем три типа пользователей: гость без сессии, гость с сессией, залогиненный посетитель. Причем на уровне nginx'a отличать вторых от третьих крайне проблематично.

    Теперь, поняв какие куки и как курсируют между посетителем и сервером, можно подойти к вопросу кэширования динамического контента. Как известно, функционал кэширования ответов fastcgi-бэкенда в nginx встроен в модуль ngx_http_fastcgi_module. Для этого надо прописать глобально в http-секции зону кэширования, а в нужном location'e — ключ. И если для условно-статического контента (изображения, архивы) ключем для кэширования мог считаться URI с незначительными дополнениями, то для кэширования динамики нужно учитывать еще и юзера. Казалось-бы правило типа
    fastcgi_cache_key "$request_method:$http_if_modified_since:$http_if_none_match:$host:$request_uri:$cookie_bbsessionhash:";
    

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

    Однако данная информация не бесполезна. На ее основе мы можем генерить ключ для ограничения частоты запросов основываясь не только на IP адресе посетителя, но и на его статусе.
    set $psUID "anon";
    set $psUCL "anon";
    if ($cookie_bbsessionhash) {
        set $psUID "$cookie_bbsessionhash";
        set $psUCL "user";
    }
    if ($psUCL = "anon") {
        set $psUID "anon:$remote_addr";
    }
    

    Данный фрагмент конфига размещаем в секции server конфига nginx до описания всех location. В итоге мы получаем оригинальный ключ на пользователя имеющего сессию и ключ основанный на IP адресе для посетителей сессии не имеющих (например для поисковый краулеров).

    Результаты

    В результате предпринятых усилий общая нагрузка на виртуалку снизилась с полки на 90 процентов до пилы на 40 с всплесками до 80 процентов.