Cache-Control, Last-Modified, ETag i Expires w Apache2

Spis treści

Każda przeglądarka internetowa potrafi buforować dane w swoim cache w celach optymalizacji przeglądanych stron www. Dzięki temu, szereg elementów odwiedzonych już witryn nie musi być ponownie pobieranych z serwera. Zyskuje na tym nie tylko serwer ale również i sam klient, któremu strona ładuje się parokrotnie szybciej. Pod ten mechanizm podpadają nie tylko pliki graficzne ale również style CSS, skrypty JS, a nawet pliki .html . Generalnie rzecz ujmując, wszystko co serwer www jest w stanie przesłać przeglądarce. Problemem może jednak się okazać zbyt krótki/długi okres ważności cache. Jeśli ten czas jest za krótki, to elementy strony będą niepotrzebnie utylizować łącze, nie tylko nasze ale również i serwera www. Z kolei, jeśli okres ważności będzie za długi, to będziemy odwiedzać nieaktualną stronę. Optymalnym rozwiązaniem byłaby taka konfiguracja serwera www, gdzie dla konkretnych elementów strony sami moglibyśmy ustalić czas ważności cache. Apache2 daje nam taką możliwość przez ustawienie nagłówków HTTP Cache-Control , Expires , ETag oraz Last-Modified .

Jak działa cache

Po wejściu na przykładową stronę www, serwer przesyła treść witryny klientowi, którym zwykle jest przeglądarka internetowa, ewentualnie jakiś serwer proxy. Nagłówki protokołu HTTP są wysyłane zanim zostanie przesłany faktyczny zasób, który ma być wyświetlony u nas na ekranie. Dopiero po otrzymaniu tego nagłówka przesyłana jest treść komunikatu, a przeglądarka w oparciu o dane w nagłówku wie co ma z tym dokumentem zrobić. Tych nagłówków może być kilka czy nawet kilkanaście, w zależności od konfiguracji serwera www.

Po otrzymaniu dokumentu, przeglądarka zapisuje sobie jego kopię w swoim lokalnym cache. Podobnie ona postępuje w przypadku wysłanego żądanie do serwera oraz otrzymanej odpowiedzi. Poniżej jest przykład zapytania i odpowiedzi:

etag-last-modified-header-naglowek-przegladarka

Request Headers , to są nagłówki zapytania, które przesłała przeglądarka serwerowi. Po otrzymaniu ich, serwer wysłał odpowiedź z szeregiem nagłówków, które są widoczne w Response Headers . Nas na dobrą sprawę interesują tylko dwa nagłówki w odpowiedzi: Last-Modified oraz ETag .

Nagłówki Last-Modified i ETag

Powyższa strona jest obsługiwana przez serwer Apache2. Ten z kolei domyślnie przesyła dwa standardowe nagłówki HTTP, które dotyczą konfiguracji cache. Są to ETag oraz Last-Modified . Gdybyśmy teraz w przeglądarce załadowali następną stronę w obrębie tej witryny, to praktycznie wszystkie jej elementy (za wyjątkiem głównego pliku text/html ) zostaną wczytane z cache:

przegladarka-cache-zasobow

Nagłówki Last-Modified oraz ETag to walidatory (validators), które mają za zadanie zweryfikować aktualność zasobu, czyli nakazać przeglądarce wysłanie do serwera zapytania warunkowego (conditional request) w odpowiednim momencie. Zapytania warunkowe są wysyłane w oparciu o informacje dotyczące świeżości zasobów, a te z kolei mogą być dostarczane przez nagłówki Expires i Cache-Control . Niemniej jednak, tutaj żaden z tych nagłówków nie został uwzględniony w odpowiedzi serwera. To nie problem, bo w przypadku braku tych nagłówków, przeglądarka sama może zdecydować jaką wartość mają przybrać atrybuty świeżości.

Nagłówki Last-Modified oraz ETag przesyłają przeglądarce ważne informacje na temat stanu danego zasobu. Nagłówek Last-Modified mówi jej, kiedy dokładnie na serwerze został zmodyfikowany dany plik. Z kolei nagłówek ETag zawiera sumę kontrolną kilku elementów składowych. Mogą to być: INode (numer i-węzła) , MTime (czas modyfikacji) oraz Size (rozmiar w bajtach). Domyślnie są wykorzystywane jedynie te dwa ostatnie. Im więcej elementów zostanie uwzględnionych, tym wolniej będzie przetwarzane zapytanie ale też tym bardziej akuratny wynik.

Nagłówek Etag jest też bardziej wiarygodny niż Last-Modified w sytuacjach, gdzie nie przechowuje się (nie utrzymuje) się dat modyfikacji plików, lub też w przypadku, gdy rozdzielczość zegara HTTP (1s) jest niewystarczająca. Stwarza on także zagrożenie dla prywatności klientów i powoduje problemy przy implementacji mechanizmu równoważącego ruch.

Domyślnie Apache2 robi użytek z obu tych walidatorów i nie generuje w odpowiedziach informacji na temat świeżości zasobu (brak nagłówków Expires i Cache-Control). Jeśli zrezygnujemy z obu tych walidatorów, to przeglądarka w ogóle nie będzie przechowywać odpowiedzi w cache.

Zakładając jednak, że chcemy, aby pewne elementy naszej witryny były buforowane, musimy skonfigurować co najmniej jeden walidator. Oczywiście można korzystać z obu ale to przyczynia się do wzrostu obciążenia serwera oraz też nagłówek dostaje kilka dodatkowych bajtów. Którą opcję wybierzemy, zależy głównie od rodzaju serwisu, który prowadzimy. W przypadku statycznych plików galerii WordPress'a, raczej wystarczy nam nagłówek Last-Modified .

Jeśli zdecydujemy się na usunięcie nagłówka ETag , to do konfiguracji Apache2, w pliku /etc/apache2/apache2.conf musimy dodać poniższą zwrotkę:

<Directory /apache2>
    Header unset ETag
    FileETag None
</Directory>

Kod błędu 304

Na tej pierwszej fotce wyżej mamy datę pobrania nagłówka oraz datę ostatniej modyfikacji pliku. Jeśli te dwie wartości są zbliżone do siebie, to zasób jest traktowany jako świeży i będzie serwowany bezpośrednio z cache przeglądarki bez odpytywania serwera www. W przypadku, gdy data modyfikacji odbiega znacząco od daty zapytania, wtedy klient poprosi serwer o walidację tego zasobu, by upewnić się, że wciąż jest on ważny.

Za każdym razem, gdy zachodzi potrzeba walidacji zasobu, wartości nagłówków ETag i Last-Modified są przesyłane przez przeglądarkę do serwera. Serwer może odpowiedzieć na to zapytanie pozytywnie albo negatywnie. W pierwszym przypadku zostanie zwrócony kod błędu 304 (Not Modified) i plik zostanie załadowany z cache przeglądarki. Natomiast w drugim przypadku, przeglądarka będzie musiała wysłać zapytanie GET i pobrać raz jeszcze cały zasób, który stracił ważność. Poniżej jest zobrazowana ta sytuacja:

etag-last-modified-304-walidacja

Widzimy tutaj dwie dyrektywy: If-Modified-Since:Mon, 01 Aug 2016 09:53:38 GMT oraz If-None-Match:"717-538ff93078200" . To są warunki zapytania warunkowego. Jeśli, któryś z nich nie jest spełniony, to zasób zostanie pobrany ponownie. W tym przypadku If-Modified-Since wskazuje tę samą wartość co nagłówek Last-Modified na pierwszej fotce wyżej, a wartość If-None-Match jest dokładnie taka sama co w nagłówku ETag . Zatem zasób nie uległ zmianie i nie ma potrzeby przesyłać go przez sieć.

Tego typu zachowanie generuje jednak ruch. Za każdym razem, gdy zasób musi przejść procedurę walidacji pakiety latają na linii przeglądarka - serwer. Nie jest to pożądany efekt, bo pociąga to za sobą dodatkowe opóźnienia. Tę niedogodność można wyeliminować stosując jeden z dwóch dodatkowych nagłówków: Cache-Control lub Expires .

Nagłówek Expires

Nagłówek Expires odpowiada za informacje o świeżości zasobu, tj. przez jaki czas klient nie musi pobierać danego pliku z serwera. Generalnie rzecz ujmując, ten nagłówek nadpisuje ustawienia przeglądarki. Jeśli go określimy na serwerze, zostanie także ustawiony parametr max-age w nagłówku Cache-Control .

Za ustawienie nagłówka Expires w Apache2 odpowiada moduł mod_expires. Jeśli zamierzamy ten nagłówek włączyć w odpowiedzi serwera, musimy pierw włączyć moduł mod_expires w konfiguracji Apache2:

# a2enmod expires
# systemctl restart apache2

Następnie w pliku /etc/apache2/apache2.conf (ew. w pliku .htaccess ) musimy dodać zwrotkę podobną do tej poniżej:

<Directory /apache2>
    <IfModule mod_expires.c>
        ExpiresActive On
        ExpiresDefault M172800
        ExpiresByType text/css A7200
        ExpiresByType image/png M604800
    </IfModule>
</Directory>

Dyrektywa ExpiresActive aktywuje dodawanie nagłówka Expires i Cache-Control w odpowiedzi serwera dla plików znajdujących się w katalogu określonym przez dyrektywę Directory. Oczywiście możemy skorzystać z innych dyrektyw do oznaczenia plików, np. Files, FilesMatch czy też DirectoryMatch. Dalej mamy dyrektywę ExpiresDefault , która ustawia domyślny czas świeżości dla wszystkich plików w tym katalogu. Z kolei dyrektywy ExpiresByType są w stanie ustawić inny czas dla plików o określonych typach MIME.

Wartości liczbowe widoczne wyżej są wyrażone w sekundach. Każda z tych wartości musi być poprzedzona literką A (czas dostępu) lub M (czas modyfikacji). W przypadku ustawienia A na danym typie plików, każdy klient będzie miał ustawiony osobny zegar w swoim cache w oparciu o czas dostępu do zasobu. Z kolei, gdy ustawimy M , to wszyscy klienci będą mięć ustawiony taki sam czas świeżości cache, który tym razem będzie się odnosił do czasu modyfikacji pliku na serwerze. Poniżej jest fotka obrazująca nagłówek Expires :

expires-cache-control-header-naglowek-przegladarka

Widzimy, że nagłówek Expires wskazuje konkretną datę w przyszłości: Mon, 08 Aug 2016 14:56:15 GMT . Jeśli przeliczyć sekundy, to okaże się, że są one równe tej wartości, która figuruje w nagłówku Cache-Control w max-age=604800 , czyli równo 7 dni. Przez ten czas, przeglądarka będzie wczytywać zasoby ze swojego cache.

Nagłówek Cache-Control

Wszystkie instrukcje zawarte w nagłówkach Last-Modified , Etag czy Expires mogą zostać nadpisane przez nagłówek Cache-Control . Nagłówek ten został wprowadzony w HTTP/1.1 i miał zaadresować problem z ograniczeniami wynikającymi ze stosowania nagłówka Expires . Jeśli chcemy mieć większą swobodę w dostosowaniu cache, możemy skorzystać z tych dodatkowych opcji, które oferuje nam nagłówek Cache-Control . Niemniej jednak, by z nich skorzystać, musimy ustawić poszczególne parametry nagłówka przy pomocy dyrektywy Header , która jest obsługiwana przez moduł mod_headers. Ten moduł musi być włączony w konfiguracji Apache2:

# a2enmod headers
# systemctl restart apache2

Dyrektywa Header może przeprowadzać szereg akcji na nagłówku. Nas głównie będą interesować te poniższe:

  • append -- dodaje wartości do istniejącego już nagłówka w odpowiedzi serwera.
  • set -- tworzy nowy nagłówek i ewentualnie zastępuje inne, które mają tę samą nazwę.
  • unset -- usuwa nagłówek o określonej nazwie, o ile ten istnieje.

Generalnie, to do ustawienia nagłówka Cache-Control będziemy używać opcji set . Jeśli będziemy korzystać także z nagłówka Expires , to zamiast set trzeba korzystać z append . Podobnie jak w przypadku nagłówka Expires , możemy korzystać z dyrektyw dopasowujących pliki i katalogi.

Opcje używane w nagłówku Cache-Control są opisane tutaj. Generalnie są dwa rodzaje opcji: te używane w zapytaniu oraz te używane w odpowiedzi. Jako, że my konfigurujemy serwer Apache2, to nas interesują nagłówki przesyłane w odpowiedziach:

  • must-revalidate -- gdy odpowiedź w cache przestaje być świeża, nie może zostać użyta bez uprzedniej walidacji na serwerze.
  • no-cache -- odpowiedź z serwera nie może zostać użyta bez uprzedniej pomyślnej walidacji na serwerze.
  • no-store -- przeglądarka lub proxy nie mogą buforować zasobów.
  • no-transform -- proxy nie może zmienić payload'u wiadomości.
  • public -- zarówno przeglądarka jak i proxy mogą buforować zasoby.
  • private -- tylko przeglądarka może buforować zasoby.
  • proxy-revalidate -- ma takie samo znaczenie co must-revalidate z tym, że nie odnosi się do cache prywatnego.
  • max-age -- po tym okresie czasu, odpowiedź z serwera ma przestać być traktowana jako świeża, a klient będzie musiał zweryfikować ją w następnym żądaniu.
  • s-maxage -- w przypadku cache współdzielonego, wiek określony w tej dyrektywie nadpisze wiek w max-age lub w nagłówku Expires . Dodatkowo, ta dyrektywa implikuje także zachowanie określone przez dyrektywę proxy-revalidate .

W przypadku, gdy zarówno nagłówek Expires i Cache-Control są określone w odpowiedzi serwera, max-age zawsze jest barny pod uwagę w pierwszej kolejności.

Poniżej jest przykład ustawienia nagłówka Cache-Control . Ten kawałek kodu trzeba dopisać do pliku /etc/apache2/apache2.conf :

<FilesMatch ".(flv|ico|pdf|avi|mov|ppt|doc|mp3|wmv|wav)$">
    Header set Cache-Control "max-age=29030400, private"
</FilesMatch>

Z których nagłówków HTTP korzystać

Na dobrą sprawę to mamy cztery różne nagłówki, które są w stanie skonfigurować cache przeglądarek: Last-Modified , ETag , Expires oraz Cache-Control . Dwa pierwsze to walidatory, a dwa ostatnie dostarczają informacje o świeżości. Zdania na temat ich wykorzystania są podzielone. Jedni uważają, że wystarczy korzystać z jednego walidatora i jednego nagłówka zawierającego informacje o świeżości zasobu. Inni zaś mówią, że najlepiej korzystać ze wszystkich czterech. Z kolei na necie notuje się przeważnie trzy nagłówki i są to kombinacje Last-Modified-Expires-Cache-Control lub ETag-Expires-Cache-Control .

Chyba najlepszą opcją są własne eksperymenty. Jeśli potrzebujemy jakiegoś mechanizmu, który nam zweryfikuje ustawienia nagłówków konkretnych zasobów w serwisie, to zawsze możemy skorzystać z redbot, który nam wyrzuci przyjazne podsumowanie:

cache-control-expires-last-modified-etag-redbot-test

Pod tymlinkiem z kolei jest ciekawa rozpiska na temat wykorzystania nagłówków HTTP odpowiedzialnych za instrukcje dla cache przeglądarek. Jest tam min. ta poniższa fotka:

http-cache-decision-tree

Widzimy zatem, że są wykorzystane jedynie dwa nagłówki: Cache-Control i ETag . Z tym, że ten drugi jest używany jedynie w przypadku, gdy odpowiedź z serwera ma podlegać buforowaniu. Wszystkie opcje dla cache są dostosowywane przez poszczególne parametry nagłówka Cache-Control . Dlaczego zatem niektóre strony wykorzystują nawet wszystkie cztery nagłówki? Być może to przez kompatybilność wsteczną dla bardzo starych przeglądarek, które nie wiedzą jak zinterpretować te nowe nagłówki wprowadzone w wersji 1.1 protokołu HTTP. To jedyne wytłumaczenie, które przychodzi mi do głowy.

Mikhail Morfikov avatar
Mikhail Morfikov
Po ponad 10 latach spędzonych z różnej maści linux'ami (Debian/Ubuntu, OpenWRT, Android) mogę śmiało powiedzieć, że nie ma rzeczy niemożliwych i problemów, których nie da się rozwiązać. Jedną umiejętność, którą ludzki umysł musi posiąść, by wybrnąć nawet z tej najbardziej nieprzyjemniej sytuacji, to zdolność logicznego rozumowania.