Связка Nginx + Varnish + HAProxy для чайников

Date August 12th, 2013 Author Vitaly Agapov

Как же управлять такой ордой? – пораженно думал Михаил. Чем же прокормить такую бездну праздного народа?

Алексей Иванов «Сердце Пармы»

Эту универсальную связку можно использовать на любых высоконагруженных проектах. Она не будет тормозить, жрать ресурсы и пыхтеть как паровоз (это кивок в сторону Apache). Она легко управляется, отлично масштабируется на любое число серверов в кластере и хорошо мониторится.

Здесь Nginx будет заниматься свои обычным делом: отдавать статику, терминировать ssl-сессии, обеспечивать rewrite url’ов и т.д. Часть трафика, подлежащую кэшированию, он будет отдавать Varnish’у, а всё остальное – на HAProxy. Varnish будет исправно кэшировать весь отдаваемый контент, лишь изредка обращаясь за обновлениями на тот же HAProxy. HAProxy будет проксировать запросы на бэкенды, балансируя нагрузку и обеспечивая механизм sticky sessions. Само собой, если бэкенд (тот же Tomcat) всего один, то HAProxy тут нам не понадобится. Но это частный случай.

HAProxy

Начнём фактически с конца. HAProxy – это лёгкий, быстрый и надёжный прокси-сервер. Он может работать как транспортном уровне (TCP), не заглядывая в заголовки верхних уровней, так и на прикладном уровне (HTTP), позволяя делать разные приятные плюшки. Главное, что нам от него требуется, – это реализация механизма sticky sessions, с которым у Nginx всегда были определённые проблемы.

Для начала HAproxy надо поставить. В репозитории Ubuntu присутствует собранная версия 1.4.18, но пусть вас не обманывает её кажущееся небольшое расхождение от текущей stable-версии 1.4.24 – на самом деле эта версия было выпущена аж в 2011-м году. Так что нам поможет Launchpad, на котором само собой есть PPA, где добрые люди собирают самые свежие версии.

# apt-get install python-software-properties
# add-apt-repository ppa:vbernat/haproxy-1.4
# apt-get update
# apt-get install haproxy
# haproxy -v
   HA-Proxy version 1.4.24 2013/06/17
   Copyright 2000-2013 Willy Tarreau <w@1wt.eu>

Чтобы HAproxy смог запуститься, надо сходить в конфиг /etc/default/haproxy и установить там значение:

ENABLED=1

Затем как обычно:

update-rc.d haproxy enable

Главный конфиг лежит в /etc/haproxy/haproxy.cfg. Сам конфиг делится на несколько блоков. Блок global описывает параметры всего процесса целиком. Остальные блоки касаются непосредственно работы прокси. Они называются в зависимости от содержимого: defaults, listen, frontend и backend. Блок defaults описывает значения по умолчанию, которые можно будет переопределить в других блоках. Блоки frontend и backend задают соответственно, порты, на которых haproxy принимает соединения, и порты, на которые он эти соединения проксирует. Блок listen позволяет объединить frontend и backend в один лакончиный блок.

В общем, нет времени объяснять. Постим почти готовый конфиг с разбросанными тут и там комментариями:

global
# HAProxy не умеет писать логи в файлы, но зато умеет слать их по протоколу syslog
# Задаём syslog-сервер и facility, с котором логи будут отправляться
   log syslog-ng.example.com local0
   # Директория, в которую chroot'ится запущенный процесс haproxy
   chroot /var/lib/haproxy
   # Пользователь и группа, под которыми будет работать процесс haproxy</span>
   user haproxy
   group haproxy
   # Запускаемый процесс будет форкаться в бэкграунд
   daemon
   # Задаём максимальное количество одновременных подключений. При превышении этого значения новые соединения не устанавливаются
   maxconn 16000
   # Создаём файл UNIX-сокета, через который сможем получить статистику и даже управлять процессом HAProxy
   # Принадлежать сокет будет пользователю nagios, чтобы через сокет можно было организовать мониторинг
   stats socket /tmp/haproxy user nagios

defaults
   log global
   mode http
   # Включаем логирование http-запросов
   option httplog
   # Не логируем пустые соединения
   option dontlognull
   contimeout 5000
   clitimeout 50000
   srvtimeout 50000

   # Задаём свои заглушки для разных кодов ошибок
   errorfile 403 /etc/nginx/pages/403.html
   errorfile 503 /etc/nginx/pages/503.html

listen stats
   # На порту 1936 будет отдаваться страница статистики. В нашем случае - открытая для всех без пароля
   bind *:1936
   stats enable
   stats uri /
   stats refresh 15s
   stats realm Haproxy\ Statistics

# Задаём фронтенд - порт, на котором будут приниматься входящие соединения
frontend main
   # Порт будет 8080
   bind *:8080
   # По урлу /haproxy будет отдаваться статус прокси
   monitor-uri /haproxy
   # В лог кроме стандартных полей будем писать заголовки X-Forwarded-For и X-Forwarded-Proto (если они нужны)
   capture request header X-Forwarded-For len 50
   capture request header X-Forwarded-Proto len 5
   # Ещё мы хотим некоторые запросы отправлять в другой пул бэкендов - web-admin, например
   acl admin url_sub admin.htm
   use_backend web-admin if admin
   # А все остальные запросы - в основной пул - web
   default_backend web

# Задаём основной пул бэкендов
backend web
   # Балансировка нагрузки методом карусели
   balance roundrobin
   # Для sticky session будет использоваться кука JSESSIONID с временем жизеи в одну минуту>
   cookie JSESSIONID prefix nocache maxlife 1m
   # Заголовок X-Forwarded-For передаём дальше
   option forwardfor
   # Закрываем пассивные http-соединения
   option httpclose

   # Задаём список бэкендов. Параметр cookie задаёт значение для куки JSESSIONID при "прилипании" к этому бэкенду
   # weight задаёт вес ноды при распределении трафика. disabled переводит ноду в режим maintenance, при котором трафик на неё не идёт
   server web1 web1.example.com:8080 check port 8080 cookie web1 weight 100
   server web2 web2.example.com:8080 check port 8080 cookie web2 weight 100
   server web0 web0.example.com:8080 check port 8080 cookie web0 weight 100 disabled

# Задаём второй список бэкендов
backend web-admin
   balance roundrobin
   cookie JSESSIONID prefix nocache maxlife 1m
   option forwardfor
   option httpclose
   server back1 back1.example.com:8090 check port 8090 cookie back1 weight 100

Всё, рестартуем haproxy и на порту 8080 получаем сбалансированный бэкенд.

Varnish

Varnish – это кэширующий сервер. Он создавался как кэширующий сервер и всегда им оставался, без всяких метаний а-ля Squid или тот же Nginx. Свою работу он выполняет отлично, к ресурсам не требователен и держит приличные нагрузки. Более того, именно при приличных нагрузках он даёт наибольшие преимущества.

Varnish тоже в официальном репозитории не самой свежей версии, но зато у него есть свой собственный репозиторий, который поддерживается разработчиками, так что получить свежую версию проще простого:

curl http://repo.varnish-cache.org/debian/GPG-key.txt | sudo apt-key add -
echo "deb http://repo.varnish-cache.org/ubuntu/ precise varnish-3.0" | sudo tee -a /etc/apt/sources.list
sudo apt-get update
sudo apt-get install varnish

Настраиваем Varnish в конфигах /etc/default/varnish и, например, /etc/varnish/default.vcl. Я кое-что писал уже о сабже в статьях раз и два, так что останавливаться не буду. Просо приведу краткий работоспособный конфиг:

# Задаём бэкенды, в нашем случае это один HAProxy на этом же сервере
backend haproxy1 {
   .host = "127.0.0.1";
   .port = "8080";
   .first_byte_timeout = 600s;
}
# Балансировщик (в нашем случае) одного бэкенда
director clhaproxy round-robin {
   { .backend = haproxy1; }
}
# Задаём список подсетей, с которых можно будет сбрасывать кэши
acl purge {
   "192.168.0.0/24"
}
# Импортируем библиотеку std
import std

# Обработка принятого запроса
sub vcl_recv {
   # По запросу PURGE мы сбрасываем кэш похожего урла (второй закомментированный вариант - сброс точно совпадающего урла)
   if (req.request == "PURGE") {
      if (!client.ip ~ purge) {
         error 405 "Not allowed.";
      }
      ban("req.url ~ "+req.url);
      # ban("req.url == "+req.url);
         error 200 "Caches Cleared Successfully.";
   }
   # Задаем балансер бэкендов
   set req.backend = clhaproxy;
   # Кэшируем только GET и HEAD
   if (req.request != "GET" && req.request != "HEAD") {
      return (pass);
   }
   # Если у нас на сайте залогиненные пользователи имеют определённую куку, то мы можем отменить кэширование для них
   if (req.http.Cookie ~ "SomeAuthorizedCookie") {
      return (pass);
   }
   # Кэшируем в зависимости от куки some.valuable.cookie
   if (req.http.Cookie) {
      set req.http.Cookie = ";"+req.http.Cookie;
      set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
      set req.http.Cookie = regsuball(req.http.Cookie, ";(some.valuable.cookie)=", "; \1=");
      set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
      set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");
      if (req.http.Cookie == "") {
         remove req.http.Cookie;
      }
   }
   # Отправляемся в кэш

   return (lookup);
}

# Страницы в кэше идентифицируются по урлу, заголовку X-Forwarded-Proto и куке some.valuable.cookie (остальные куки мы уже удалили). Добавлять по желанию
sub vcl_hash {
   hash_data(req.url);
   hash_data(req.http.X-Forwarded-Proto);
   hash_data(req.http.Cookie);
   return (hash);
}

# Получаем ответы из бэкенда
sub vcl_fetch {
   # Снимаем заголовок Vary, так как из-за него наплодятся страницы в кэше в зависимости от значений заголовков, указанных в этом заголовке
   unset beresp.http.Vary;
   # Для залогиненного пользователя опять ничего не запоминаем
   if (req.http.Cookie ~ "SomeAuthorizedCookie") {
      return (hit_for_pass);
   }
   # Для разных урлов делаем разное время хранения
   if (req.url ~ "page1") {
      set beresp.ttl = 600s;
      remove beresp.http.Set-Cookie;
   }
   else if (req.url ~ "page2.htm") {
      set beresp.ttl = 1h;
      remove beresp.http.Set-Cookie;
   }
   # А по умолчанию для остальных можем не кэшировать, или, например, сделать отдельные настройки
   else {
      set beresp.ttl = 0s;
   }
   # Отдать полученное пользователю
   return (deliver);
}

sub vcl_deliver {
   # Удаляем ненужные заголовки
   unset resp.http.Via;
   unset resp.http.X-Varnish;
   return (deliver);
}

# При ошибке отдать свою заглушку
sub vcl_error {
  if ( obj.status >= 500 && obj.status <= 505) {
      set obj.http.Content-Type = "text/html; charset=utf-8";
        set obj.http.error50x = std.fileread("/etc/nginx/pages/503.html");
        synthetic obj.http.error50x;
        unset obj.http.error50x;<
        return(deliver);
  }
}

Перезапускаем Varnish и идём на порт 6081 нашего сервера. Там отдаётся сайт, кэширующийся в Varnish’е и сбалансированный в бэкенде с помощью HAProxy. Из нестандартных заголовков мы оставили только Age, который будет показывать возраст страницы, отданной из кэша. Значение 0 будет указывать на то, что страница была получена из бэкенда. После периода тестирования и дебага этот заголовок также можно удалить с помощью unset resp.http.Age.

Ещё одна вещь, о которой надо упомянуть – это сброс кэшей. Если срок жизни кэша достаточно большой, и его надо сбросить, не прибегая к рестарту самого варниша, то пригодится функционал по внешнему сбрасыванию. Можно, конечно, пользоваться командой varnishadm, о которой надо будет сделать отдельную статью, но неспроста мы в конфиге сделали обработку запросов PURGE и даже написали списки доступа. С этим конфигом можно сбросить кэш для любой страницы примерно вот такой командой:

/usr/bin/curl -X PURGE http://varnish.example.com:6081/page1.htm

Она обнулит кэш для всех урлов, где встречается page1.htm, включая /dir1/page1.htm, /dir2/page2.htm и т.д. Командой

/usr/bin/curl -X PURGE http://varnish.example.com:6081/

Можно вообще сбросить разом все кэши.

При этом в самом конфиге у меня вставлена закомментированная строчка

ban(“req.url == “+req.url);

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

Nginx

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

В процессе предварительной подготовки надо поставить несколько пакетов:

Для rewrite:

	# apt-get install libpcre3-dev

Для SSL:

	# apt-get install libssl-dev

Для geoip:

	# apt-get install libgeoip-dev

Для сборки:

	# apt-get install libperl-dev

Начинаем установку. Само собой, надо nginx.org сначала проверить, какая самая свежая версия.

wget http://nginx.org/download/nginx-1.5.3.tar.gz
tar -xzvf nginx-1.5.3.tar.gz
cd nginx-1.5.3
./configure --prefix=/etc/nginx --conf-path=/etc/nginx/nginx.conf --sbin-path=/usr/sbin/nginx --error-log-path=/var/log/nginx/error.log --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-log-path=/var/log/nginx/access.log --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --lock-path=/var/lock/nginx.lock --pid-path=/var/run/nginx.pid --with-http_geoip_module --with-http_stub_status_module --with-http_ssl_module --with-ipv6 --with-http_perl_module
make && make install

Готово. Пишем в /etc/nginx.conf:

# Пользователь, под которым будем работать
user www-data;
# Число воркеров. Делаем равным числу процессорных тредов на сервере
worker_processes 24;
pid /var/run/nginx.pid;

events {
   # Оптимизированный режим для Linux. Каждый тред обслуживает много клиентов
   use epoll;<
   worker_connections 1024;
   # Принимать как можно больше подключений
   multi_accept on;
}
http {
   sendfile on;
   tcp_nopush on;
   tcp_nodelay on;
   keepalive_timeout 65;
   types_hash_max_size 2048;
   # Прячем версию из заголовков
   server_tokens off;

   include /etc/nginx/mime.types;
   default_type application/octet-stream;
   # Задаём формат логов и их расположение
   log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
           '$status $body_bytes_sent $request_time ';
   access_log /var/log/nginx/access.log main;
   error_log /var/log/nginx/error.log;

   # Включаем сжатие
   gzip on;
   gzip_disable "msie6";
   # Сжимаем все проксированные запросы
   gzip_proxied any;
   # Уровень сжатия 6
   gzip_comp_level 6;
   # Сжимаем даже запросы по http 1.0
   gzip_http_version 1.0;
   gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

   include /etc/nginx/conf.d/*.conf;
   include /etc/nginx/sites-enabled/*;
}

А в sites-enabled делаем линк на такой конфиг:

server {
   server_name ~example.com$;
   # В отличие от Apache тут можно сделать один server на два виртуальных хоста - http и https
   listen 80;
   listen 443 ssl;
   ssl_certificate     /path/to/example.com.cer;
   ssl_certificate_key /path/to/example.com.key;
   ssl_protocols       SSLv3 TLSv1 TLSv1.1 TLSv1.2;
   ssl_ciphers         RC4:HIGH:!aNULL:!MD5:!kEDH;
   ssl_session_cache   shared:SSL:20m;
   ssl_prefer_server_ciphers on;
   access_log /var/log/nginx/access.log main;
   if ($host != www.exampl.com) {
      rewrite ^(.*)$ https://www.example.com$request_uri permanent;
   }
   root /var/www;
   error_page 502 503 /error/503.html;
   location ^~ /error/ {
      alias /etc/nginx/pages/503/;
   }
   location ^~ /image/ {
      alias /var/www/images/;
      expires 24h;
   }
   location ~ ^/(page1|page2)/$ {
      #error_page 418 = @varnish; return 418;
      try_files /notexist @varnish;
   }
   location / {
      #error_page 418 = @haproxy; return 418;
      try_files /notexist @haproxy;
   }
   location @haproxy {
      # По необходимости устанавливаем заголовки
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_pass http://127.0.0.1:8080;
      proxy_redirect off;
   }
   location @varnish {
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Host $host;
      proxy_pass http://127.0.0.1:6081;
      proxy_redirect off;
   }
}

Можно обратить внимание на то, что редирект в именованный location (@varnish и @haproxy) можно сделать двумя способами. Один через error_page, второй – через try_files. Оба вполне себе работают, но по моим представлениям первый плох тем, что переписывает код возврата и делает невозможным, например, показать свою заглушку на код 503. А второй плох тем, что требует обращения к диску на каждое своё срабатывание. В общем, надо делать выбор для каждого конкретного случая. Нормального способа попасть в именованный location кроме этих двух хаков пока вроде как нет. Nginx такой nginx.

В следующей части я остановлюсь на способах мониторинга всего этого добра.

Tags: , ,
Category: HAProxy, Nginx, Varnish | No comments »

Comments

Leave a comment

 Comment Form