Связка 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 тоже в официальном репозитории не самой свежей версии, но зато у него есть свой собственный репозиторий, который поддерживается разработчиками, так что получить свежую версию проще простого:
1.
curl http://repo.varnish-cache.org/debian/GPG-key.txt |
sudo
apt-key add -
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 сначала проверить, какая самая свежая версия.
1.
wget http://nginx.org/download/nginx-1.5.3.
tar
.gz
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: HAProxy, Nginx, Varnish
Category:
HAProxy, Nginx, Varnish |
No comments »