Ситуация выглядит следующим образом: есть размещённый на нескольких серверах IIS сайт (интернет-магазин),
перед ним расположен балансировщик. Между ними решено установить nginx для уменьшения нагрузки на IIS.
Основная масса динамического контента отображается Ajax-ом, так что кеширование страниц каталога товаров вполне безопасно. Однако на них могут быть отзывы о товаре, за которые можно проголосовать — совсем как на Хабре, что тоже надо учесть.
Плюс к этому хочется поддерживать валидность популярных страниц в кеше автоматически.
Итак, сначала устанавливаем свежий nginx — без него не получится. Также нам понадобятся wget и curl.
Я не буду подробно описывать конфигурацию самого прокси, а детально остановлюсь на методах поддержания
кеша в актуальном состоянии.
Конфигурация самого nginx:
Итак, прокси готов. Теперь самое интересное.
Мы хотим иметь в кеше TOP-20000 страниц за последние сутки. Лог выглядит так:
Логи обращаются каждый час и хранятся сутки. Ксожалению, logrotate не умеет обращать логи менее, чем раз в день,
поэтому применяется средней грязности хак: size 1 в файл конфигурации и logrotate -f /etc/nginx.rotate по крону раз в час.
Скрипт для создания списка наиболее посещаемых страниц:
Итак, на выходе получаем файлики с командами:
Дальше каждые 20 минут мы должны пройти по этому списку и запросить каждую из страниц с сервера для валидации кеша.
Теперь осталось только обновлять кеш страниц при голосовании за комментарий.
Этим занимается демон, постоянно мониторящий логи на предмет голосования.
Конфигурация для logrotate:
init-скрипт для нашего демона:
Решение слегка костылеподобное, но рабочее. Нагрузка на IIS упала, скорость отдачи страниц клиенту возросла.
Отмечу, что все картинки сайта лежат на отдельном сервере и кешированию не подлежат.
перед ним расположен балансировщик. Между ними решено установить nginx для уменьшения нагрузки на IIS.
Основная масса динамического контента отображается Ajax-ом, так что кеширование страниц каталога товаров вполне безопасно. Однако на них могут быть отзывы о товаре, за которые можно проголосовать — совсем как на Хабре, что тоже надо учесть.
Плюс к этому хочется поддерживать валидность популярных страниц в кеше автоматически.
Итак, сначала устанавливаем свежий nginx — без него не получится. Также нам понадобятся wget и curl.
Я не буду подробно описывать конфигурацию самого прокси, а детально остановлюсь на методах поддержания
кеша в актуальном состоянии.
Конфигурация самого nginx:
Код:
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
# Кеш у нас большой.
proxy_cache_path /var/cache/nginx levels=2:2 keys_zone=STATIC:512m
inactive=24h max_size=32g;
sendfile on;
keepalive_timeout 65;
gzip on;
gzip_proxied any;
gzip_min_length 1100;
gzip_http_version 1.0;
gzip_buffers 4 8k;
gzip_comp_level 9;
gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/json;
server {
server_name 127.0.0.1;
listen 80;
set $backend 127.0.0.2;
# Меняем структуру логов. Они нам понадобятся. Разделитель полей - |
log_format cache '$remote_addr|$time_local|$request_uri|$request_method|$status|$http_referer|$cookie___sortOrder|$IsAuth|$sent_http_content_type|$http_user_agent';
access_log /var/log/nginx/proxy_access.log cache;
error_log off;
# Не кешируем полностью динамические пользовательские страницы
location ~* /(basket.aspx|visitedgoods.aspx|users/|sale/order.aspx|sale/posted.aspx) {
proxy_pass http://$backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass_header Set-Cookie;
proxy_redirect off;
set $IsAuth 1;
if ($cookie_AUTH = "") {
set $IsAuth 0;
}
}
location / {
# Убираем из урлов символ | - он там не должен оказаться, но если кто-то вобъёт его руками, он попортит нам логи
if ($args ~* (.*)|(.*)) {
set $brand $1$2;
rewrite ^(.*) $1?$brand? redirect;
}
if ($args ~* (.*)%7C(.*)) {
set $brand $1$2;
rewrite ^(.*) $1?$brand? redirect;
}
rewrite ([a-zA-Z0-9]+)|([a-zA-Z0-9]+) $1$2? permanent;
rewrite ([a-zA-Z0-9]+)%7C([a-zA-Z0-9]+) $1$2? permanent;
rewrite (.*)%7C$ $1? permanent;
rewrite (.*)| $1? permanent;
proxy_pass http://$backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass_header Set-Cookie;
proxy_ignore_headers "Expires" "Cache-Control" "Set-Cookie";
# Проверяем, аутентифицирован ли пользователь. Страницы у гостей и юзеров разные, эта переменная нужна для ключа кеша.
set $IsAuth 1;
if ($cookie_AUTH = "") {
set $IsAuth 0;
}
# Проверяем, не нужно ли обновить кеш - это делается при запросе нашего робота
set $DoBypass 0;
if ($http_user_agent = "WGET-POST-daemon") {
set $DoBypass 1;
}
# Кука __sortOrder задаёт метод сортировки товаров и тоже нужна в ключе
proxy_cache STATIC;
proxy_cache_key "$host$uri$is_args$args $cookie___sortOrder $IsAuth";
proxy_cache_valid 200 301 302 304 30m;
proxy_cache_bypass $DoBypass;
proxy_cache_use_stale error timeout invalid_header updating;
proxy_connect_timeout 100ms;
proxy_redirect off;
}
}
}
Итак, прокси готов. Теперь самое интересное.
Мы хотим иметь в кеше TOP-20000 страниц за последние сутки. Лог выглядит так:
Код:
+0400|/catalog/25/women.aspx|GET|200|http://127.0.0.1/|-|0|text/html; charset=windows-1251|Opera/9.80 (Windows NT 6.1; U; ru) Presto/2.9.168 Version/11.51
Логи обращаются каждый час и хранятся сутки. Ксожалению, logrotate не умеет обращать логи менее, чем раз в день,
поэтому применяется средней грязности хак: size 1 в файл конфигурации и logrotate -f /etc/nginx.rotate по крону раз в час.
Скрипт для создания списка наиболее посещаемых страниц:
Код:
#!/bin/bash
# Получаем собственный IP, чтобы не учитывать в статистике локальный трафик
ourIP=$(ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f2 | awk '{ print $1}')
# Получаем валидную куку аутентификации для скачки страниц, отображаемых зарегистрированным юзерам
curl -H "Cookie: BasketID=KYKY-B-PYKY" -H "Content-Type: application/json" -d "{"login":"USERNAME","password":"PASS","remeberMe":"true"}" -c /var/log/nginx/cookies1.txt http://127.0.0.1/Resources/Services/SystemService.asmx/SignIn
line=$(tail -n 1 /var/log/nginx/cookies1.txt)
AUTH=$(echo $line | awk '{wild=$7; print wild}')
# Собираем единый лог: сливаем вместе все файлы за сутки.
cp /var/log/nginx/proxy_access.log /var/log/nginx/overall_proxy
FILES=/var/log/nginx/*.gz
for f in $FILES
do
cat $f | gunzip>> /var/log/nginx/overall_proxy
done
# удаляем старые файлы команд wget-у
rm -f /var/log/nginx/wget/*
# Выбираем из лога все запросы к кеширующимся страницам (по типу содержимого - text/html), которые были запрошены НЕ с локалхоста и успешно переданы клиенту, выбираем 20000 самых популярных и создаём список команд wget-у.
awk -v ourIP="$ourIP" '{ FS="|"; ip = $1; url = $3; code = $5; catsrt=$7; isauth=$8; ct=$9; if (ip != ourIP) if (url !~ "basket.aspx" && url !~ "visitedgoods.aspx" && url !~ "users/" && url !~ "sale/order.aspx" && url !~ "sale/posted.aspx") if (code = "200") if (ct ~ "text/html;") print "http://" ourIP url "|" catsrt "|" isauth}' /var/log/nginx/overall_proxy | sort | uniq -c | sort -n -k1,6 | tail -n20000 | awk '{print $2}' | awk -v AUTH="$AUTH" '{FS="|"}$2=="-"{$2=""} $3=="0"{$3=""} $3=="1"{$3=AUTH}{print "-b --header="Cookie: __sortOrder="$2"; AUTH="$3"" -o /dev/null -O /dev/null "$1 }'> /var/log/nginx/cache.dat
rm -f /var/log/nginx/overall_proxy
cd /var/log/nginx/wget
# Режем список команд на 10 частей - мы запустим 10 потоков закачки.
split -l 2000 /var/log/nginx/cache.dat
rm -f /var/log/nginx/cache.dat
Итак, на выходе получаем файлики с командами:
Код:
-b --header="Cookie: __sortOrder=; AUTH=" -o /dev/null -O /dev/null 127.0.0.1/catalog/25/women.aspx
Дальше каждые 20 минут мы должны пройти по этому списку и запросить каждую из страниц с сервера для валидации кеша.
Код:
#!/bin/bash
FILES=/var/log/nginx/wget/*
# Убиваем незавершённые с прошлого раза wget-ы
if [ -s /var/log/nginx/wgets.pid ]
then
cat /var/log/nginx/wgets.pid | xargs kill
rm -f /var/log/nginx/wgets.pid
fi
# И запускаем новые.
for f in $FILES
do
cat $f | xargs wget & echo $! >> /var/log/nginx/wgets.pid
done
Теперь осталось только обновлять кеш страниц при голосовании за комментарий.
Этим занимается демон, постоянно мониторящий логи на предмет голосования.
Код:
#!/bin/bash
# Получаем куку аутентификации
curl -H "Cookie: BasketID=KYKY-B-PYKY-U-ATAC" -H "Content-Type: application/json" -d "{"login":"USERNAME","password":"PASS","remeberMe":"true"}" -c /var/log/nginx/cookies.txt http://127.0.0.1/Resources/Services/SystemService.asmx/SignIn
line=$(tail -n 1 /var/log/nginx/cookies.txt)
AUTH=$(echo $line | awk '{wild=$7; print wild}')
# Проверяем, не запущен ли уже демон
if [ -f /var/log/nginx/post-daemon.pid ];
then
echo "POST-daemon already running!"
exit
fi
# Вешаемся на хвост логу nginx-а и ждём голосования
(/usr/bin/tail -f /var/log/nginx/proxy_access.log & echo $! >/var/log/nginx/post-daemon.pid) |
while read -r line
do
if [[ $line =~ '/Resources/Services/SystemService.asmx/VoteToComment|POST|200' ]];
then
# Собираем информацию для wget - ссылку и куки.
ref=$(echo $line | awk -F"|" '{ FS="|"; ref=$6; print ref}')
sortOrder=$(echo $line | awk -F"|" '{ FS="|"; co=$7; print co}')
IsAuth=$(echo $line | awk -F"|" '{ FS="|"; IsAuth=$8; print IsAuth}')
if [[ $sortOrder == "-" ]];
then
sortOrder=""
fi
# Проверяем, какую страницу качать - для гостей или для регистрантов, и качаем. UserAgent скажет нашему nginx-у, что данный запрос должен быть обработан в обход кеша.
if [[ $IsAuth == "0" ]];
then
wget --user-agent="WGET-POST-daemon" --header="Cookie: __sortOrder=$sortOrder" -o /dev/null -O /dev/null $ref
else
wget --user-agent="WGET-POST-daemon" --header="Cookie: __sortOrder=$sortOrder; AUTH=$AUTH" -o /dev/null -O /dev/null $ref
fi
fi
done
exit
Код:
/var/log/nginx/*log {
daily
rotate 24
size 1
missingok
notifempty
compress
postrotate
/etc/init.d/nginx reload
/etc/init.d/nginx-POSTcache restart
endscript
}
Код:
#!/bin/sh
#
# This script starts and stops the nginx cache updater daemon
#
# chkconfig: - 85 15
#
# processname: post-daemon
# pidfile: /var/log/nginx/post-daemon.pid
. /etc/rc.d/init.d/functions
daemon="/usr/local/sbin/post-daemon.sh"
pidfile="/var/log/nginx/post-daemon.pid"
prog=$(basename $daemon)
start() {
[ -x $daemon ] || exit 5
echo -n $"Starting POST-daemon: "
($daemon &) &
retval=$?
echo
[ $retval -eq 0 ]
return $retval
}
stop() {
echo -n $"Stopping POST-daemon: "
pid=$(cat $pidfile)
kill $pid
rm -f $pidfile
retval=$?
echo
[ $retval -eq 0 ] && rm -f $lockfile
return $retval
}
restart() {
stop
start
}
rh_status() {
status $prog
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
rh_status
;;
*)
echo $"Usage: $0 {start|stop|status|restart}"
exit 2
esac
Решение слегка костылеподобное, но рабочее. Нагрузка на IIS упала, скорость отдачи страниц клиенту возросла.
Отмечу, что все картинки сайта лежат на отдельном сервере и кешированию не подлежат.