Связка 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: HAProxy, Nginx, Varnish
Category:
HAProxy, Nginx, Varnish |
No comments »