To raczej nie jest podstawowy konfig i próżno szukać go na stronie WordPress’a, więc odradzam tę zabawę jeśli nie zna się zbyt dobrze nginx’a.
Ponieważ serwerek, na którym działa stronka to sprzęcik z Atomem 330 i mocy na CPU zbyt wiele nie ma to popularne pluginy (np. W3 Total Cache) potencjalnie zwiększające wydajność tak na prawdę zmulały stronkę jeszcze bardziej. Pluginów sprawdziłem kilka i każdorazowo efekt był podobny - stronka działała wolniej niż bez ich pomocy.
Druga sprawa to zwiększony ruch - w takiej konfiguracji już przy kilku osobach równocześnie przeglądających blog, serwerek zwyczajnie nie radził sobie z dynamicznym generowaniem strony.
Pomysł na rozwiązanie problemu z wydajnością polegał na odpaleniu cache’ującego reverse proxy przed właściwą stroną, z krótkim okresem ważności cache’u (max kilka sekund) tak by przy dużym obciążeniu strony serwować głównie z cache’u (tylko co pewien czas ktoś będzie miał niefart i będzie musiał zaczekać na wygenerowanie strony), przy czym komentarze i panel administracyjny działają z pominięciam cache’owania (czyli każdorazowo trafiają przez proxy do aplikacji).
Ważne jednak by osoba wysyłająca komentarz mogła wynik swojego działania zobaczyć od razu na stronie. Ponieważ komentarze wysyłane są metodą HTTP POST to w momencie odebrania takiego połączenia będzie ustawiane ciasteczko dezaktywujące cache dla danego połączenia na kilka sekund (do momentu jego wygaśnięcia).
Poniżej plik konfiguracyjny, który należy zapisać np. w: /etc/nginx/sites-available/wordpress
# na początek ustawiamy lokalizację dla cache'u
proxy_cache_path /var/cache/nginx/wordpress levels=1:2 keys_zone=WORDPRESS:10m inactive=24h max_size=100m;
# nie chcę stronki z www na początku więc cały ruch przekierowują
# na stronkę bez www
server {
listen 10.0.1.2:80;
server_name www.example.com;
rewrite ^ http://example.com$request_uri? permanent;
}
# tutaj ma miejsce magia - główny host obsługujący stronkę
# to tak na prawdę cache'ujące proxy serwujące okresowo
# generowane pliki
server {
listen 10.0.1.2:80 default;
access_log /var/log/nginx/wordpress.access.log;
server_name example.com;
# ten rewrite przerzuca do panelu admina nawet jeśli
# na końcu nie wpiszemy ukośnika
# bez niego też to działa ale przekierowanie jest przetwarzane
# przez skrypt stronki i działa wolniej
rewrite ^/wp-admin$ /wp-admin/ last;
# cache'ujemy tylko odpowiedzi 200 i przez 60s
# (moja stronka nie obsługuje zbyt dużego ruchu i rzadko
# jest modyfikowana - np. przez komentarze - więc 60s jest OK,
# na bardziej aktywnych stronkach można się pokusić o ustawienie
# 1~3s przez co stronka jest praktycznie dynamiczna ale mimo to
# cache zapewni obsługę nawet kilku tys. zapytań na sekundę
proxy_cache_valid 200 60s;
# informacje dla backendu na jakiego host się wbijamy
# i z jakiego "prawdziwego" IP
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# możemy ukryć niektóre nagłówki np. by nie podpowiadać
# z jakich pluginów korzystamy w WordPressie
proxy_hide_header X-Powered-By;
# kilka ustawień timeout'ów
proxy_connect_timeout 60;
proxy_read_timeout 120;
proxy_send_timeout 120;
# Ważne - poniższa opcja ustawia w jaki sposób generowane są
# nazwy plików w cache'u,
# dodając np. kolejne zmienne możemy zróżnicować cache dla
# pewnych grup użytkowników
proxy_cache_key "$scheme$request_method$host$request_uri";
# domyślna lokalizacja
location / {
# ustawiamy domyślną wartość zmiennej
set $no_cache "";
# If non GET/HEAD, don't cache & mark user as uncacheable for 1 second via cookie
# jeśli metoda inna niż GET/HEAD to oznacz usera przez cookie jako niecachowanego
# na czas 60s (ustawiany poniżej)
if ($request_method !~ ^(GET|HEAD)$) {
set $no_cache "1";
}
# jeśli zalogowany to nie cache'uj
if ($http_cookie ~* "comment_author_|wordpress_(?!test_cookie)|wp-postpass_" ) {
set $no_cache "1";
}
# jeżeli któryś z wcześniejszych warunków jest spełniony
# to ustawiamy cookie, które poinformuje nas by nie cachować
# kolejnych zapytań
# (z powodu "dziwnego" zachowania if w nginx'ie ustawienie
# tego bezpośrednio we wcześniejszych warunkach nie działa)
if ($no_cache = "1") {
add_header Set-Cookie "_mcnc=1; Max-Age=61; Path=/";
add_header X-Microcachable "0";
}
# jeśli cookie jest ustawione to pomijamy cache i serwujemy
# świeżą treść
if ($http_cookie ~* "_mcnc") {
set $no_cache "1";
}
# dwie poniższe opcje zapewniają pominięcia cache'owania
# w przypadku gdy wystąpi któryś z wcześniejszych warunków
proxy_no_cache $no_cache;
proxy_cache_bypass $no_cache;
# Serwujemy cache jeśli strona jest obecnie odświeżana
# lub wystąpi błąd
proxy_cache_use_stale error timeout invalid_header updating
http_500 http_502 http_503 http_504;
# pliki większe niż 1M nie będą cache'owane
proxy_max_temp_file_size 1M;
# cache'ujemy tylko odpowiedzi 200 i przez 60s
# (moja stronka nie obsługuje zbyt dużego ruchu i rzadko
# jest modyfikowana - np. przez komentarze - więc 60s jest OK,
# na bardziej aktywnych stronkach można się pokusić o ustawienie
# 1~3s przez co stronka jest praktycznie dynamiczna ale mimo to
# cache zapewni obsługę nawet kilku tys. zapytań na sekundę
proxy_cache_valid 200 60s;
# zmieniamy domyślny klucz cache'owania tak by uwzględniał
# naszą zmienną
proxy_cache_key "$scheme://$host$request_uri $no_cache";
# wskazujemy konkretną lokalizację cache'u
proxy_cache WORDPRESS;
# podajemy lokalizację backendu (nie widziałem sensu
# by udostępniać go na zewnętrznym adresie)
proxy_pass http://127.0.0.1:81;
# można ustawić dodatkowo cache'owanie strony w przeglądarce
# (całkiem niezależnie od tego co będzie w cache'u na serwerze)
expires 60s;
}
# dla panelu administracyjnego ustawiamy proxy bez cache'u
location ~* wp\-(admin|login) {
# dostęp do panelu administracyjnego dodatkowo chronimy
# hasłem - po co?
# bo w tym katalogu są różne fajne skrypty, w których już
# nie raz znaleziono dziury
auth_basic "Go Away";
auth_basic_user_file htpasswd;
# proxy bez cache'u
proxy_pass http://127.0.0.1:81;
}
# statykę cache'ujemy mocniej niż treści dynamiczne
# a czemu nie puszczam jej bezpośrednio? bo wyplute przez
# backend zostaną skompresowane i w takiej postaci zachowają
# się w cache'u - gdybym serwował je bezpośrednio to nginx
# kompresowałby np. css'y/js'y przy każdym dostępie do nich
location ~* \.(jpg|png|gif|jpeg|css|js|mp3|wav|swf|mov|doc|pdf|xls|ppt|docx|pptx|xlsx)$ {
# cache'ujemy statykę przez 2 godziny
proxy_cache_valid 200 120m;
# dodatkowo ustawiamy długie cache'owanie w przeglądarkach
expires 864000;
# puszczamy wszystko w proxy + cache
proxy_pass http://127.0.0.1:81;
proxy_cache WORDPRESS;
# wyłączam logowanie dostępu do statyki nawet w przypadku błędów
# to mało istotne
log_not_found off;
access_log off;
}
# jeszcze inaczej ustawiam cache dla RSS'ów
location ~* \/[^\/]+\/(feed|\.xml)\/? {
# cache'ujemy RSS'y przez 45 minut
if ($http_cookie ~* "comment_author_|wordpress_(?!test_cookie)|wp-postpass_" ) {
set $no_cache 1;
}
proxy_cache_key "$scheme://$host$request_uri $no_cache";
proxy_cache_valid 200 45m;
proxy_cache MYSITE;
proxy_pass http://127.0.0.1:81;
}
}
# to teraz konfiguracja serwera serwującego treści dynamiczne
server {
# nasłuchujemy lokalnie bo z zewnątrz strona dostępna
# jest przez proxy
listen 127.0.0.1:81;
error_log /var/log/nginx/mysite.error.log;
root /var/www/wordpress;
index index.php;
# logujemy prawdziwe IP dzięki odpowiednim nagłówkom
# przesyłanym przez proxy
set_real_ip_from 127.0.0.0/24;
real_ip_header X-Real-IP;
# tutaj praktycznie klasyka - z tym że zamiast na końcu
# wskazywać index.php robię najpierw kilka rewrite'ów
# np. dla przeniesionych stron, itp
location / {
try_files $uri $uri/ @rewrites;
}
location @rewrites {
rewrite /main http://example.com/about/? permanent;
rewrite /projekty http://example.com/category/projects/? permanent;
rewrite /tag/gd http://example.com? permanent;
rewrite /category/hobby http://roman.com? permanent;
rewrite ^ /index.php last;
}
# na bardziej obleganych stronach limitowanie wyszukiwania może
# pomóc, ale to nie mój przypadek
# location /search { limit_req zone=mysitesearch burst=3 nodelay; rewrite ^ /index.php; }
location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
# cache'owanie atrybutów statycznych plików
open_file_cache max=1000 inactive=120s;
open_file_cache_valid 45s;
open_file_cache_min_uses 2;
open_file_cache_errors off;
# maksymalne cache'owanie statyki w przeglądarkach
expires max;
}
# no i na końcu obsługa skryptów php
location ~* \.php$ {
# albo plik istnieje i go serwujemy albo dajemy Forbidden
try_files $uri =403;
# kilka standardowych ustawień
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# blokujemy możliwość wykonywania skryptów z katalogu upload
# (nawet jeśli komuś uda się je wepchnąć)
if ($uri !~ "^/wp-content/uploads/") {
fastcgi_pass php-fastcgi;
}
# informacje dla proxy jak długo może cache'ować
add_header Cache-Control "max-age:60, public";
expires 60s;
}
# blokuję dostęp do plików zaczynających się od kropki
location ~ /\. { access_log off; log_not_found off; deny all; }
# wyłączam logowanie do nieistotnych plików
location = /robots.txt { allow all; access_log off; log_not_found off; }
location = /favicon.ico { access_log off; log_not_found off; }
}
Konfiguracja potrzebuje jednego folderu na cache, do którego dostęp do zapisu ma nginx (użytkownik na którym działa proces):
mkdir -p /var/cache/nginx/wordpress
chown -R www-data:www-data /var/cache/nginx/wordpress
A żeby zaczął działać trzeba go “włączyć” i przeładować nginx’a:
cd /etc/nginx/sites-available/
ln -s wordpress /etc/nginx/sites-enabled/wordpress
service nginx reload
Jedyna rzecz, której brakuje w tym configu to konfiguracja backend’u do PHP’a (u mnie nazwana php-fastcgi) - może kiedyś zrobię HOWTO o konfiguracji NGINX+PHP, ale na tą chwilę zakładam że sobie poradzisz 😃
Benchmark
Sprawdźmy jak to wygląda teraz:
ab -n 1000 -c 10 https://gagor.pl/
This is ApacheBench, Version 2.3 < $Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking gagor.pl (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software: nginx
Server Hostname: gagor.pl
Server Port: 80
Document Path: /
Document Length: 36263 bytes
Concurrency Level: 10
Time taken for tests: 8.716 seconds
Complete requests: 1000
Failed requests: 22
(Connect: 0, Receive: 0, Length: 22, Exceptions: 0)
Write errors: 0
Total transferred: 36601094 bytes
HTML transferred: 36263088 bytes
Requests per second: 114.73 [#/sec] (mean)
Time per request: 87.159 [ms] (mean)
Time per request: 8.716 [ms] (mean, across all concurrent requests)
Transfer rate: 4100.91 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 1 16 7.1 15 55
Processing: 17 67 97.0 53 789
Waiting: 5 28 55.6 19 456
Total: 37 83 98.0 68 803
Percentage of the requests served within a certain time (ms)
50% 68
66% 74
75% 78
80% 80
90% 87
95% 95
98% 665
99% 755
100% 803 (longest request)
Dla mnie bomba 😃
Pomysł na taki rodzaj cache’owania zaczerpnąłem stąd .