Связка 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, где добрые люди собирают самые свежие версии.

1.# apt-get install python-software-properties
2.# add-apt-repository ppa:vbernat/haproxy-1.4
3.# apt-get update
4.# apt-get install haproxy
5.# haproxy -v
6.   HA-Proxy version 1.4.24 2013/06/17
7.   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 в один лакончиный блок.

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

01.global
02.# HAProxy не умеет писать логи в файлы, но зато умеет слать их по протоколу syslog
03.# Задаём syslog-сервер и facility, с котором логи будут отправляться
04.   log syslog-ng.example.com local0
05.   # Директория, в которую chroot'ится запущенный процесс haproxy
06.   chroot /var/lib/haproxy
07.   # Пользователь и группа, под которыми будет работать процесс haproxy</span>
08.   user haproxy
09.   group haproxy
10.   # Запускаемый процесс будет форкаться в бэкграунд
11.   daemon
12.   # Задаём максимальное количество одновременных подключений. При превышении этого значения новые соединения не устанавливаются
13.   maxconn 16000
14.   # Создаём файл UNIX-сокета, через который сможем получить статистику и даже управлять процессом HAProxy
15.   # Принадлежать сокет будет пользователю nagios, чтобы через сокет можно было организовать мониторинг
16.   stats socket /tmp/haproxy user nagios
17. 
18.defaults
19.   log global
20.   mode http
21.   # Включаем логирование http-запросов
22.   option httplog
23.   # Не логируем пустые соединения
24.   option dontlognull
25.   contimeout 5000
26.   clitimeout 50000
27.   srvtimeout 50000
28. 
29.   # Задаём свои заглушки для разных кодов ошибок
30.   errorfile 403 /etc/nginx/pages/403.html
31.   errorfile 503 /etc/nginx/pages/503.html
32. 
33.listen stats
34.   # На порту 1936 будет отдаваться страница статистики. В нашем случае - открытая для всех без пароля
35.   bind *:1936
36.   stats enable
37.   stats uri /
38.   stats refresh 15s
39.   stats realm Haproxy\ Statistics
40. 
41.# Задаём фронтенд - порт, на котором будут приниматься входящие соединения
42.frontend main
43.   # Порт будет 8080
44.   bind *:8080
45.   # По урлу /haproxy будет отдаваться статус прокси
46.   monitor-uri /haproxy
47.   # В лог кроме стандартных полей будем писать заголовки X-Forwarded-For и X-Forwarded-Proto (если они нужны)
48.   capture request header X-Forwarded-For len 50
49.   capture request header X-Forwarded-Proto len 5
50.   # Ещё мы хотим некоторые запросы отправлять в другой пул бэкендов - web-admin, например
51.   acl admin url_sub admin.htm
52.   use_backend web-admin if admin
53.   # А все остальные запросы - в основной пул - web
54.   default_backend web
55. 
56.# Задаём основной пул бэкендов
57.backend web
58.   # Балансировка нагрузки методом карусели
59.   balance roundrobin
60.   # Для sticky session будет использоваться кука JSESSIONID с временем жизеи в одну минуту>
61.   cookie JSESSIONID prefix nocache maxlife 1m
62.   # Заголовок X-Forwarded-For передаём дальше
63.   option forwardfor
64.   # Закрываем пассивные http-соединения
65.   option httpclose
66. 
67.   # Задаём список бэкендов. Параметр cookie задаёт значение для куки JSESSIONID при "прилипании" к этому бэкенду
68.   # weight задаёт вес ноды при распределении трафика. disabled переводит ноду в режим maintenance, при котором трафик на неё не идёт
69.   server web1 web1.example.com:8080 check port 8080 cookie web1 weight 100
70.   server web2 web2.example.com:8080 check port 8080 cookie web2 weight 100
71.   server web0 web0.example.com:8080 check port 8080 cookie web0 weight 100 disabled
72. 
73.# Задаём второй список бэкендов
74.backend web-admin
75.   balance roundrobin
76.   cookie JSESSIONID prefix nocache maxlife 1m
77.   option forwardfor
78.   option httpclose
79.   server back1 back1.example.com:8090 check port 8090 cookie back1 weight 100

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

Varnish

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

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

2.echo "deb http://repo.varnish-cache.org/ubuntu/ precise varnish-3.0" | sudo tee -a /etc/apt/sources.list
3.sudo apt-get update
4.sudo apt-get install varnish

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

001.# Задаём бэкенды, в нашем случае это один HAProxy на этом же сервере
002.backend haproxy1 {
003.   .host = "127.0.0.1";
004.   .port = "8080";
005.   .first_byte_timeout = 600s;
006.}
007.# Балансировщик (в нашем случае) одного бэкенда
008.director clhaproxy round-robin {
009.   { .backend = haproxy1; }
010.}
011.# Задаём список подсетей, с которых можно будет сбрасывать кэши
012.acl purge {
013.   "192.168.0.0/24"
014.}
015.# Импортируем библиотеку std
016.import std
017. 
018.# Обработка принятого запроса
019.sub vcl_recv {
020.   # По запросу PURGE мы сбрасываем кэш похожего урла (второй закомментированный вариант - сброс точно совпадающего урла)
021.   if (req.request == "PURGE") {
022.      if (!client.ip ~ purge) {
023.         error 405 "Not allowed.";
024.      }
025.      ban("req.url ~ "+req.url);
026.      # ban("req.url == "+req.url);
027.         error 200 "Caches Cleared Successfully.";
028.   }
029.   # Задаем балансер бэкендов
030.   set req.backend = clhaproxy;
031.   # Кэшируем только GET и HEAD
032.   if (req.request != "GET" && req.request != "HEAD") {
033.      return (pass);
034.   }
035.   # Если у нас на сайте залогиненные пользователи имеют определённую куку, то мы можем отменить кэширование для них
036.   if (req.http.Cookie ~ "SomeAuthorizedCookie") {
037.      return (pass);
038.   }
039.   # Кэшируем в зависимости от куки some.valuable.cookie
040.   if (req.http.Cookie) {
041.      set req.http.Cookie = ";"+req.http.Cookie;
042.      set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
043.      set req.http.Cookie = regsuball(req.http.Cookie, ";(some.valuable.cookie)=", "; \1=");
044.      set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
045.      set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");
046.      if (req.http.Cookie == "") {
047.         remove req.http.Cookie;
048.      }
049.   }
050.   # Отправляемся в кэш
051. 
052.   return (lookup);
053.}
054. 
055.# Страницы в кэше идентифицируются по урлу, заголовку X-Forwarded-Proto и куке some.valuable.cookie (остальные куки мы уже удалили). Добавлять по желанию
056.sub vcl_hash {
057.   hash_data(req.url);
058.   hash_data(req.http.X-Forwarded-Proto);
059.   hash_data(req.http.Cookie);
060.   return (hash);
061.}
062. 
063.# Получаем ответы из бэкенда
064.sub vcl_fetch {
065.   # Снимаем заголовок Vary, так как из-за него наплодятся страницы в кэше в зависимости от значений заголовков, указанных в этом заголовке
066.   unset beresp.http.Vary;
067.   # Для залогиненного пользователя опять ничего не запоминаем
068.   if (req.http.Cookie ~ "SomeAuthorizedCookie") {
069.      return (hit_for_pass);
070.   }
071.   # Для разных урлов делаем разное время хранения
072.   if (req.url ~ "page1") {
073.      set beresp.ttl = 600s;
074.      remove beresp.http.Set-Cookie;
075.   }
076.   else if (req.url ~ "page2.htm") {
077.      set beresp.ttl = 1h;
078.      remove beresp.http.Set-Cookie;
079.   }
080.   # А по умолчанию для остальных можем не кэшировать, или, например, сделать отдельные настройки
081.   else {
082.      set beresp.ttl = 0s;
083.   }
084.   # Отдать полученное пользователю
085.   return (deliver);
086.}
087. 
088.sub vcl_deliver {
089.   # Удаляем ненужные заголовки
090.   unset resp.http.Via;
091.   unset resp.http.X-Varnish;
092.   return (deliver);
093.}
094. 
095.# При ошибке отдать свою заглушку
096.sub vcl_error {
097.  if ( obj.status >= 500 && obj.status <= 505) {
098.      set obj.http.Content-Type = "text/html; charset=utf-8";
099.        set obj.http.error50x = std.fileread("/etc/nginx/pages/503.html");
100.        synthetic obj.http.error50x;
101.        unset obj.http.error50x;<
102.        return(deliver);
103.  }
104.}

Перезапускаем 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:

1.# apt-get install libpcre3-dev

Для SSL:

1.# apt-get install libssl-dev

Для geoip:

1.# apt-get install libgeoip-dev

Для сборки:

1.# apt-get install libperl-dev

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

2.tar -xzvf nginx-1.5.3.tar.gz
3.cd nginx-1.5.3
4../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
5.make && make install

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

01.# Пользователь, под которым будем работать
02.user www-data;
03.# Число воркеров. Делаем равным числу процессорных тредов на сервере
04.worker_processes 24;
05.pid /var/run/nginx.pid;
06. 
07.events {
08.   # Оптимизированный режим для Linux. Каждый тред обслуживает много клиентов
09.   use epoll;<
10.   worker_connections 1024;
11.   # Принимать как можно больше подключений
12.   multi_accept on;
13.}
14.http {
15.   sendfile on;
16.   tcp_nopush on;
17.   tcp_nodelay on;
18.   keepalive_timeout 65;
19.   types_hash_max_size 2048;
20.   # Прячем версию из заголовков
21.   server_tokens off;
22. 
23.   include /etc/nginx/mime.types;
24.   default_type application/octet-stream;
25.   # Задаём формат логов и их расположение
26.   log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
27.           '$status $body_bytes_sent $request_time ';
28.   access_log /var/log/nginx/access.log main;
29.   error_log /var/log/nginx/error.log;
30. 
31.   # Включаем сжатие
32.   gzip on;
33.   gzip_disable "msie6";
34.   # Сжимаем все проксированные запросы
35.   gzip_proxied any;
36.   # Уровень сжатия 6
37.   gzip_comp_level 6;
38.   # Сжимаем даже запросы по http 1.0
39.   gzip_http_version 1.0;
40.   gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
41. 
42.   include /etc/nginx/conf.d/*.conf;
43.   include /etc/nginx/sites-enabled/*;
44.}

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

01.server {
02.   server_name ~example.com$;
03.   # В отличие от Apache тут можно сделать один server на два виртуальных хоста - http и https
04.   listen 80;
05.   listen 443 ssl;
06.   ssl_certificate     /path/to/example.com.cer;
07.   ssl_certificate_key /path/to/example.com.key;
08.   ssl_protocols       SSLv3 TLSv1 TLSv1.1 TLSv1.2;
09.   ssl_ciphers         RC4:HIGH:!aNULL:!MD5:!kEDH;
10.   ssl_session_cache   shared:SSL:20m;
11.   ssl_prefer_server_ciphers on;
12.   access_log /var/log/nginx/access.log main;
13.   if ($host != www.exampl.com) {
14.      rewrite ^(.*)$ https://www.example.com$request_uri permanent;
15.   }
16.   root /var/www;
17.   error_page 502 503 /error/503.html;
18.   location ^~ /error/ {
19.      alias /etc/nginx/pages/503/;
20.   }
21.   location ^~ /image/ {
22.      alias /var/www/images/;
23.      expires 24h;
24.   }
25.   location ~ ^/(page1|page2)/$ {
26.      #error_page 418 = @varnish; return 418;
27.      try_files /notexist @varnish;
28.   }
29.   location / {
30.      #error_page 418 = @haproxy; return 418;
31.      try_files /notexist @haproxy;
32.   }
33.   location @haproxy {
34.      # По необходимости устанавливаем заголовки
35.      proxy_set_header X-Forwarded-Proto $scheme;
36.      proxy_set_header Host $host;
37.      proxy_set_header X-Real-IP $remote_addr;
38.      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
39.      proxy_pass http://127.0.0.1:8080;
40.      proxy_redirect off;
41.   }
42.   location @varnish {
43.      proxy_set_header X-Forwarded-Proto $scheme;
44.      proxy_set_header Host $host;
45.      proxy_pass http://127.0.0.1:6081;
46.      proxy_redirect off;
47.   }
48.}

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

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

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

Comments

Leave a comment

 Comment Form