Пример конфигурационного файла Varnish

Как обещал в докладе выкладываю пример и описание рабочего конфига для Варниша. Чтобы узнать подробнее о том, что такое Варниш и для чего он нужен ознакомьтесь с разделом Pressflow + Varnish.

Сразу сделаю важный комментарий. Прежде чем запускать Варниш на боевом сервере разработчик должен детально изучить его документацию, понять основные принципы его работы и настройки. Слепое копирование чужих конфигов, без понимания того, что делает та или иная инструкция, может привести к плачевным результатам.

В этом примере рассматривается конфиг для Варниша третьей версии (на данный момент это последняя стабильная версия). Обратите внимание, у Варниша с версии 2.1.0 поменялся движок обработки регулярных выражений, по этому некоторые примеры конфигов, доступные в интернете, могут работать некорректно. Луллаботы, например, обновили свой туториал и предлагают сразу несколько вариантов конфига для разных версий Варниша.

Задача

Варниш должен отдавать анонимам данные из своего кеша, а запросы от авторизованных пользователей передавать бэкенду. Отличать анонимного пользователя от зарегистрированного Варниш будет по наличию/отсутствию куки SESS*. В случае падения бэкенда Варниш должен отдавать кеш в том числе и для авторизованных пользователей.

Скачать мой конфиг вы можете здесь, ниже описание самых интересных и важных его частей.

Бэкенд

Указываем Варнишу с каким веб-сервером нужно работать. Параметры в блоке probe означают, что каждые 5 секунд на бэкенде нужно дергать файл status.php. Если в течение 1 секунды от бэкенда не будет получен ответ с кодом 200, то проверка будет считаться провалившейся. Если провалено 3 проверки из 5, бэкенд помечается “упавшим”.

backend web1 {
    .host = "10.10.10.10";
    .port = "80";
    .probe = {
    .url = "/status.php";
    .interval = 5s;
    .timeout = 1s;
    .window = 5;
    .threshold = 3;
}

Таким образом, уже через 15 секунд после возникновения проблем на бэкенде, Варниш пометит его упавшим и, в зависимости от настроек, либо начнет передавать запросы другому бэкенду, либо начнет обслуживать запросы зарегистрированных пользователей из своего кеша.

Несколько бэкендов могут быть объединены в логическую группу — director, в случае использования директоров при выходе из строя одного из бэкендов запросы будут автоматически перенаправлены на живые бэкенды из текущего директора. В нашем случае балансировка делается на уровне nginx и эта возможность Варниша не используется.

Status.php мы позаимствовали у Луллаботов, скачать его можно тут. В этом файле осуществляется проверка доступности серверов БД и Мемкеша.

ACL

Создаем списки айпишников, на основе которых позже зададим ограничение доступа к cron.php и очистке кеша Варниша:

# ACL for purging cache
acl purge {
    "127.0.0.1";
    "localhost";
    "10.1.0.0"/16;
}

# ACL for access to cron.php
acl internal {
    "127.0.0.1";
    "localhost";
    "10.10.0.0"/16;
}

vcl_recv

Процедура vcl_recv содержит инструкции, которые будут выполняться Варнишем при пролучении запроса от клиента. В ней содержится основная магия. В основном эта процедура должна возвращать одно из двух значений: pass — передать запрос бэкенду или lookup — попытаться найти закешированную версию страницы в кеше, в случае отсутствия кеша, запрос будет передан бэкенду, а ответ закеширован. Полный список возможных значений можно найти тут: https://www.varnish-cache.org/docs/3.0/reference/vcl.html#subroutines, а здесь: https://www.varnish-cache.org/docs/3.0/faq/configuration.html дано описание различий значений pass и pipe.

Для начала отключаем обработку Варнишем некоторых служебных адресов Друпала:

// list of URLS which shouldn't be cached
if (req.url ~ "^/status\.php$" ||
    req.url ~ "^/update\.php$" ||
    req.url ~ "^/info/.*$" ||
    req.url ~ "^/flag/.*$" ||
    req.url ~ "^.*/ajax/.*$" ||
    req.url ~ "^.*/ahah/.*$") {
        return (pass);
}

Доступ

Далее, ограничиваем доступ к операции очистки кеша и cron.php на основе ранее определенных списков ip-адресов.

# Clear cache entry
if (req.request == "PURGE") {
    if (!client.ip ~ purge) {
        error 405 "Not allowed.";
    }
    return (lookup);
}

# Do not allow outside access to cron.php or install.php.
if (req.url ~ "^/(cron|install)\.php$" && !client.ip ~ internal) {
    # Have Varnish throw the error directly.
    error 404 "Page not found.";
    # Use a custom error page that you've defined in Drupal at the path "404".
    #set req.url = "/404.html";
}

Задаем максимальное время жизни кеша 6 часов, а также указываем, что если сервер помечен как “упавший” (опция probe в настройках бэкенда), то у пользователя нужно отрезать все куки. В дальнейшем, для пользователей без кук мы будем отдавать кеш и эта настройка позволит Варнишу отдавать кеш авторизованным юзерам.

set req.grace = 6h;

if (!req.backend.healthy) {
    unset req.http.Cookie;
}

cookies

Далее анализируем куки, полученные от пользователя, и удаляем ненужные. Дело в том, что куки могут быть установлены как серверным софтом, так и клиентским, через java-script. Разного рода счетчики и коды баннеров ставят куки для своих целей и эти куки совершенно не интересны серверу, кроме того они мешают нам отличить анонимного посетителя от зарегистрированного. Мы удаляем все куки кроме SESS*, если после такого удаления у нас осталась пустая строка, значит куки SESS* нет и мы считаем пользователя анонимным и можем отдать ему данные из кеша. Если кука SESS* существует, то мы передаем запрос бэкенду.

// clean cookies. Pass only cookies SESS[a-z0-9], xyz
if (req.http.Cookie) {
set req.http.Cookie = regsub(req.http.Cookie, "^(.*)$", "; \1");
set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
set req.http.Cookie = regsuball(req.http.Cookie, ";(SESS[a-z0-9]|xyz+)=", "; \1=");
set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");

    if (req.http.Cookie == "") {
      unset req.http.Cookie;
    }
    else {
      return (pass);
    }
}

Код выше требует некоторого пояснения.

Куки представляют из себя текстовую строку вида “ключ=значение” разделенные точкой с запятой. Для примера рассмотрм строку name1=val1; name2=val2; name3=val3;. Предположим, мы хотим пропустить далее только куку name2. Приведенный выше код делает следующее:

  • сначала, для служебных целей, добавляет точку с запятой перед этой строкой. Наша тестовая строка примет вид ;name1=val1; name2=val2; name3=val3;.
  • Затем удаляет пробелы после всех точек с запятой (; заменяет на ;). Тестовая строка примет вид: ;name1=val1;name2=val2;name3=val3;.
  • Далее перед разрешенными куками добавляет пробел. Если быть точнее, то пробел добавляется между именем разрешенной куки и точкой с запятой идущей перед ним, именно для этого на первом шаге в начало строки добавлена точка с запятой, чтобы если разрешенная кука идет на первом месте, чтобы можно было перед ней воткнуть пробел. Теперь вся строка имеет вид ;name1=val1; name2=val2;name3=val3; (перед разрешенной кукой name2 добавлен пробел).
  • Теперь удаляются все записи, которые начинаются не с ; . Вот это регулярное выражение: ;[^ ][^;]* можно словами озвучить так: все строки первый символ в которых “точка с запятой”, второй символ не пробел, затем идет любое число символов кроме точек с запятой нужно заменить на ``.
  • Последняя строка удаляет лидирующую точку с запятой, добавленную на первом шаге.

В итоге, если после таких преобразований у нас осталась не пустая строка, то значит кука SESS* у пользователя есть и его запрос надо передать бэкенду. Как я уже говорил выше, в случае, если бэкенд отмечен как упавший, мы удаляем из запроса пользователя информацию о куках и любой запрос, даже от авторизованного юзера, будет обслуживаться как анонимный.

Accept-Encoding

Далее приводим к общему виду заголовок Accept-Encoding. Этот заголовок используется браузером, для того чтобы сообщить серверу какие типы сжатия он поддерживает. Веб-сервер, получив такой заголовок, может (и, по хорошему, должен) заархивировать отдаваемые данные тем алгоритмом, который поддерживается браузером.

Если одна и та же страница запрашивается разными браузерами, передающими разный заголовок “Accept-Encoding” Варниш должен в своем кеше создать несколько копий одной и той же страницы, иначе, если, например, браузер поддерживает только алгоритм сжатия deflate, а данные в кеше Варниша сжаты алгоритмом gzip, браузер, получив такие данные, не сможет их распаковать.

Проблема в том, что разные браузеры, поддерживающие один и тот же алгоритм сжатия, сообщают о нем серверу по разному, например, Хром это делает так: Accept-Encoding:gzip,deflate,sdch, а Firefox так: Accept-Encoding: gzip, deflate. По умолчанию, для этих двух заголовков Варниш создал бы разные копии кеша одной и той же страницы. Код ниже, приводит подобные заголовки к универсальному виду и предотвращает излишний расход памяти под кеш.

if (req.http.Accept-Encoding) {
    if (req.http.Accept-Encoding ~ "gzip") {
        # If the browser supports it, we'll use gzip.
        set req.http.Accept-Encoding = "gzip";
    }
    else if (req.http.Accept-Encoding ~ "deflate") {
        # Next, try deflate if it is supported.
        set req.http.Accept-Encoding = "deflate";
    }
    else {
        # Unknown algorithm. Remove it and send unencoded.
        unset req.http.Accept-Encoding;
    }
}

Вот и всё. Конфиг содержит больше настроек, чем я описал в этом тексте, но остальные не такие интересные и, думаю, их смысл должен быть интуитивно понятен.