Od jakiegoś czasu używam varnisha jako cache dla mojego bloga i sprawdza się genialnie, dla rzeczy zcachowanych ( a przy odpowiednim configu to >90% ) czas odpowiedzi to właściwie RTT między klientem i serwerem. Nie jest skomplikowany w konfiguracji a przyśpiesza stronę paredziesięciokrotnie :).

Przygotowanie

Konfiguracja

Dp konfiguracji Varnish używa własnego minijęzyka zwanego VCL, odrobinę podobny do C. Pozwala on na grzebanie w nagłówkach zapytania i odpowiedzi oraz kierowanie requestu gdzie trzeba + parę innych akcji takich jak purgowanie obiektów w cache

VCLki mogą być przeładowywane w locie dzięki czemu nie traci się cache przy przeładowaniu configu.

Varnish domyślnie cachuje tylko w RAM, można mu wskazać cache dyskowy ale zostanie on zdiscardowany po restarcie (bo varnish go po prostu mmap()uje i traktuje jak kawałek pamięci, zostawiając OSowi co z tym fantem ma zrobić).

Istnieje opcja zapisywania cache na dysk, polega ona na tym że varnish podczas pracy streamuje obiekty na dysk a po restarcie zapełnia nimi cache, ale jest to rozdzielne z dyskowy cache – persisten storage podczas pracy jest tylko zapisywany, varnish nie serwuje z niego obiektów.

Konfig jakiego używam u siebie (pominąłem sekcje dot. backendów):

sub vcl_fetch {
    set beresp.grace = 1h;
    set beresp.http.X-Url = req.url;
    set beresp.http.X-Host = req.http.host;
 
    if (!(req.url ~ "wp-(login|admin)")) {
                  unset beresp.http.set-cookie;
    }
}
 
sub vcl_recv {
      set req.grace = 1h;
      if (req.request == "PURGE") {
          if(!client.ip ~ purge ) {
              error 405 "Not allowed.";
          }
          ban("obj.http.X-Host == " + req.http.host +
              " && obj.http.X-Url ~ " + req.url);
          error 200 "Ban added";
      }
      if (req.url ~ "preview=true") { #this is needed for preview to work
          return(pass);
      }
 
      if (!(req.url ~ "wp-(login|admin)") && req.request == "GET") {
          unset req.http.cookie;
      }
# strip parameters from stupid bots
      if (req.http.user-agent ~ "MJ12bot") {
          set req.url = regsub(req.url, "(\&|\?).*", "");
      }
}
  • ACLka na początku to lista hostów które mogą purgować  zawartość cache. Pamiętaj że pare varnishów = purge potrzebny na wszystkich
  • vcl_fetch jest wołany po tym jak backend zwróci dane, tutaj obcinam większość cookie z odpowiedzi wordpressa. Ustawiam też header X-Url/Host na url/host requestu (hack wokół tego że obj. ma tylko nagłówki odpowiedzi) oraz maksymalny grace time na 1h (inaczej varnish wykopie obiekty które powinny leżeć w cache w ramach grace time)
  • vcl_recv jest wołane po otrzymania requesta od klienta na początku sprawdzam czy to request purgujący i jeżeli tak nakładam ban. Tutaj małe wyjaśnienie czym różni się varnishowy purge od ban:
  • purge usuwa dany element (i wszystkie jego wersję w cache jeżeli ma parę bo backend ustawił “Vary”) “tu i teraz” tzn przeszuka cache w poszukiwania obiektów w momencie odpalenia purge. Nie obsługuje regexpów
  • ban – filtr, który usunie obiekty z cache dopiero gdy klient się do nich odwoła. Główną przewagą bana nad purge jest to że purge nie potrafi purgować po regexpach a w ban można, z czego korzysta plugin wordpressowy.

  • potem puszczam przez pass (czyli przekaż odpowiedź klienta bezpośrednio do serwera bez cachowania) “preview=true”, bez tego podgląd postów przed publikacją nie będzie działał (serwer potrzebuje widzieć cookie i nie cachować contentu)

  • robię też rewrite requestów od głupiego bota który requestował sporo URLi z query stringiem co powodowało duplikaty w cache i w rezultacie zapychanie.

Varnish domyślnie do końca każdego bloku dołącza swoje swoje regułki które generalnie “robią co trzeba” (cachują GETy, puszczają POSTi\y itd.)

Pitfalls

ban działa najlepiej gdy matchujemy tylko po obj.*

Varnish uruchamia proces w tle zwany ban lurker, który czyści cache z obiektów zbanowanych (czyli pasujących do bana i “starszych” od niego), ale może to tylko robić dla banów które matchują wyłącznie po obj.*,  stąd hack z X-Url/Host (obj ma nagłówki odpowiedzi, nie zapytania oraz nie ma niczego w stylu obj.url). Będzie to niejako “w tle” czyścić nasz cache, szczególnie ważne gdy mamy długi TTL, stare obiekty które nie będą serwowanie nie będą zagracać pamięci.

Normalizacja!

Każdy różny url/host, czasami nawet ten sam obiekt (gdy backend zwraca Vary: z parametrami) to oddzielny obiekt w cache, nie ma sensu trzymać w cache

/css/style.css?1234
/css/style.css?1235
/css/style.css?1236
/css/style.css

Gdy można obciąć wszystko w VCLce do /css/style.css i zrobić purge przy deployu nowej wersji pliki. Najgorszy przykład jaki widziałem to chyba dodawanie timestampa w query stringu tak że co sekundę varnish musiał zaciągać ten sam plik z backendu a “stary” URL dalej zajmował cache.

Opcje są dwie, preferowana to naprawić aplikacje ;] ale gdy się nie da można to zrobić w podobny sposób jak odcinałem tego bota.

Monitoring

Chyba 3 najważniejsze parametry (z varnishstat) do monitoringu (naprawdę chcesz je monitorować od startu, od razu widać czy dana zmiana w configu pomogła czy zaszkodziła) to

  • cache_hit – wiadomo
  • cache_miss – też wiadomo
  • n_lru_nuked – ile obiektów wypada z cache przed upłynięciem ich TTL – wysoka wartość oznacza że po prostu masz za mały cache. Ile to jest “wysoka” zależy ofc od wielkości obiektu

Load balancing

Nieważne jakiego LB używasz (polecam HAProxy), dziel ruch na varnishe po hashu URLa (jak więcej niż 2 to najlepiej consistent hashing, mniej missów gdy trzeba będzie wyłączyć któryś z serwerów). Dzięki temu nie ma duplikacji tego samego obiektu w różnych cache i efektywnie masz większy cache. Jedyny problem to gdy jeden obiekt byłby bardzo popularny, ale varnish jest wystarczająco wydajny żeby nie stanowiło to problemu.

Grace

Varnish pozwala na ustawienie obiektówi tzw. grace time – czas jaki obiekt może być dalej serwowany z cache mimo że jego TTL upłynął. Jest to użyteczne w 2 sytuacjach:

  • Obiektowi właśnie skończył się TTL a nie chcemy żeby klient czekał więc serwujemy mu odrobinę starszy obiekt podczas gdy obieramy najnowszą wersję w tle, np. liczba głosów lub newsbar na stronie, chcemy żeby był cachowany i nie chcemy by user czekał aż się odświeży. Także każdy często żądany obiekt który jest długo generowany (bo dzięki temu update idzie niejako ” w tle”
  • Backend jest dead ale dalej chcemy serwować to co jest w cache. “Awaryjny” przypadek gdy coś się popsuje to przynajmniej najpopularniejsze rzeczy na stronie będą działać:)

W dokumentacji jest inny ciekawy przykład używania grace:

  if (req.backend.healthy) {
    set req.grace = 30s;
  } else {
    set req.grace = 1h;
  }

W esencji “mały grace gdy wszystko jest ok, duży gdy backend jest down”, wymaga to wyspecyfikowania w backendzie healthchecka dla niego. Zaletą tego jest to że user nie będzie dostawał zbyt starego contentu gdy backendy są up ale dostanie “cokolwiek” gdy coś padnie. Zależy od aplikacji, w tym zastosowaniu “nieświeży” content będzie banowany przez plugin do WP więc nie robi to większej różnicy.