Przejdź do treści

Magento 2.4.8: problem z full page cache po zalogowaniu

W Magento 2.4.8, do wydania 2.4.8-p5 włącznie, wbudowana pamięć stron (full page cache, FPC) liczy klucz odczytu z kontekstu klienta, który w chwili odczytu jest jeszcze pusty. Skutek zależy od sklepu: tam, gdzie strony gości trafiają do pamięci, zalogowany klient może dostać stronę gościa z jej cenami, a w sklepie z katalogiem tylko po zalogowaniu pamięć w ogóle nie trafia. Opisany przypadek pochodzi ze sklepu z indywidualnymi warunkami handlowymi: listing ładował się 1,6–2,3 s przy każdym wejściu. Opisuję diagnozę, kierunek poprawki i dwie rzeczy, które w takim sklepie trzeba sprawdzić po naprawie: izolację cen klientów i dostęp do zamkniętego katalogu.

Naprawa odsłania dwie kolejne kwestie

Objaw i przyczyna mogą dotyczyć każdego sklepu z zalogowanymi klientami. Ceny przypisane do konta i katalog dostępny tylko po zalogowaniu są niewidoczne, dopóki pamięć stron nie działa, i pojawiają się dopiero po naprawie. Dlatego sekcje 05 i 06 są częścią poprawki, nie dodatkiem.

Podsumowanie przypadku: konfiguracja, przyczyna, wynik i ograniczenia
KonfiguracjaMagento Open Source 2.4.8-p3, tryb produkcyjny, wbudowana pamięć stron w Redisie, katalog tylko po zalogowaniu, rabaty przypisane do kont, wszyscy klienci w jednej grupie
Przyczynaodczyt z pamięci liczy klucz klasą IdentifierForSave z kontekstu, który przy odczycie jest jeszcze pusty
Wyniklisting dla zalogowanego klienta 1,6–2,3 s → 0,06 s
Ograniczeniawygenerowanie strony, której nie ma jeszcze w pamięci (zimny wpis), dalej trwa ok. 1,8 s przy 12 produktach; naprawa wymagała też poprawek kluczy cen i powiązania strony z pamięci z sesją klienta

Czasy w tym wpisie to czas odpowiedzi na żądanie samej strony HTML, mierzony narzędziem curl z zachowaniem ciastek klienta, bez zdjęć i skryptów.

01

Objaw: 2 sekundy przy każdym wejściu, a winne wydają się zdjęcia

Zgłoszenie brzmiało „sklep bardzo wolno ładuje zdjęcia na listingu”. Pomiar pokazał coś innego: średnie zdjęcie miało 9 kB, cały listing pobierał około 50 kB grafik po kilkanaście milisekund każda, a pamięć miniatur była kompletna. Wolny był HTML listingu: 2,12, 2,12 i 2,08 s w trzech kolejnych wejściach na ten sam adres. Zdjęcia ładują się leniwie, czyli dopiero po HTML-u, więc z perspektywy klienta to one „stały”.

Przy działającej pamięci stron drugie wejście na ten sam adres trwa ułamek sekundy. Tutaj każde wejście kosztowało tyle samo, a strona logowania, oglądana bez sesji klienta, odpowiadała w 0,05 s. To mocny sygnał, ale sam w sobie nie dowodzi awarii pamięci stron: strona może też być niecacheowalna albo ciągle unieważniana.

Dlatego najpierw wykluczyliśmy te przyczyny: cacheowalność układu strony (wszystkie warianty kategorii cacheowalne), bloki oznaczone jako niecacheowalne, tryb serwisowy, wyłączoną pamięć stron i unieważnianie pamięci przez synchronizację z systemem zewnętrznym (zatrzymana na dwie minuty, listing dalej 2 s).

02

Dlaczego klucz odczytu nie trafia w klucz zapisu

Magento zapisuje stronę w pamięci pod kluczem liczonym z adresu, protokołu i odcisku klienta (tzw. vary string, czyli skrót kontekstu: grupa klienta, waluta, sklep i to, co moduły dodadzą do kontekstu HTTP). W klasie Magento\Framework\App\PageCache\Kernel odczyt korzysta z obiektu $identifier, a zapis z $identifierForSave.

Która klasa liczy klucz odczytu i zapisu pamięci stron w zależności od wersji Magento
WersjaKlucz odczytuKlucz zapisu
2.4.7Identifier: ciastko X-Magento-Vary, a gdy go brak, kontekstIdentifierForSave: kontekst
2.4.8 do 2.4.8-p5IdentifierForSave: tylko kontekstIdentifierForSave: kontekst
2.4.9IdentifierForSave: najpierw ciastko vary, potem kontekstIdentifierForSave

W 2.4.8 moduł PageCache w swoim etc/di.xml dodał preferencję, która kieruje interfejs IdentifierInterface, a więc i obiekt odczytu, na IdentifierForSave. Klucz odczytu i zapisu liczy więc ta sama klasa, ale w innym momencie. Przy zapisie kontekst zawiera już dane klienta, bo trafiają tam przy obsłudze akcji. Przy odczycie, który dzieje się wcześniej, kontekst jest jeszcze pusty, a ta klasa w 2.4.8 nie zagląda do ciastka vary. Pełne nazwy klas to Magento\Framework\App\PageCache\Identifier i Magento\PageCache\Model\App\Request\Http\IdentifierForSave.

Dla zalogowanego klienta klucz odczytu wychodzi więc zawsze jak dla gościa. Co z tego wynika, zależy od sklepu:

  • jeśli strony gości są w pamięci, zalogowany klient dostaje stronę gościa, np. z cenami grupy gości zamiast swojej grupy. Ten objaw opisuje zgłoszenie #40474 w repozytorium Magento
  • jeśli katalog jest tylko po zalogowaniu, stron gości w pamięci nie ma, więc odczyt nie trafia nigdy, a pamięć stron w praktyce nie działa. Tak było w opisanym sklepie

Zakres sprawdziliśmy w publicznym repozytorium: preferencji nie ma w 2.4.7-p8, jest od 2.4.8 do 2.4.8-p5, a w 2.4.9 klasa IdentifierForSave czyta już najpierw ciastko X-Magento-Vary. W informacjach o wydaniu Magento Open Source 2.4.9 ta poprawka figuruje jako AC-14999 („FPC not work when login”). Opis dotyczy wbudowanej pamięci stron, np. w Redisie albo Valkey. Przy Varnishu stronę z pamięci wydaje Varnish, a nie ta klasa.

03

Jak to potwierdzić, zanim cokolwiek zmienisz

Pomiar powtórek daje podejrzenie. Potwierdzenie dają dwie kolejne obserwacje:

  • sygnał: powtórne wejście zalogowanego klienta na ten sam adres nie jest szybsze, a strona logowania jest
  • sygnał: wpis strony jest w Redisie albo Valkey i daje się odczytać, ale liczba wpisów nie rośnie przy kolejnych wejściach, bo zapis trafia ciągle w ten sam klucz
  • potwierdzenie: dla tego samego żądania policz klucz odczytu i klucz zapisu klasami Magento, przy pustym kontekście i przy kontekście klienta; jeśli się różnią, to ten przypadek

Licząc klucze ręcznie, łatwo się pomylić: adres w kluczu to pełny URL z protokołem i domeną, nie sama ścieżka. Przy pomiarach curl musi i zapisywać, i odsyłać ciastka. Bez ciastka X-Magento-Vary każde żądanie wygląda jak pierwsze i pomiar fałszywie pokazuje, że pamięć nie działa.

Nagłówek Cache-Control: no-store w odpowiedzi nie oznacza, że pamięć stron nie działa. Przy wbudowanej pamięci Magento strona leży w pamięci na serwerze, a przeglądarce mówi się, żeby jej nie trzymała. Wniosek z tego nagłówka to najczęstszy fałszywy trop.
04

Kierunek poprawki i zmierzony efekt

Poprawka nie wymaga zmian w rdzeniu. We własnym module, w konfiguracji DI dla obszaru frontend, przywraca się dla Magento\Framework\App\PageCache\Kernel argument identifier wskazujący na Magento\Framework\App\PageCache\Identifier. Odczyt znowu bierze odcisk klienta z ciastka X-Magento-Vary, jak w 2.4.7, a zapis zostaje bez zmian. Przy aktualizacji do 2.4.9 tę zmianę warto przejrzeć, bo rdzeń rozwiązuje to już inaczej.

Czas odpowiedzi listingu dla zalogowanego klienta przed poprawką i po niej
ŚrodowiskoPrzedPo
kopia testowa1,2 s0,05 s
produkcja, listing kategorii1,6–2,3 s0,06 s

Na kopii testowej sprawdziliśmy też, że dodanie do koszyka ze strony wydanej z pamięci działa. Przy wdrożeniu dwie rzeczy potrafią zmylić pomiar. Po podmianie pliku PHP zmiana może wejść dopiero po minucie, jeśli OPcache sprawdza pliki co 60 s. Poza tym zaraz po wyczyszczeniu pamięci każda strona jest zimna. Porównywać trzeba pamięć rozgrzaną kilkunastoma wejściami.

05

Sklep z cenami per konto: izolacja cen klientów

Magento zakłada, że ceny różnicuje się grupami klientów. W sklepie B2B często jest inaczej: wszyscy klienci firmowi są w jednej grupie, a rabat jest przypisany do konta, czasem z wyjątkami dla wybranych marek. Problem nie dotyczy każdego takiego sklepu. Pojawia się tam, gdzie cena zależy od danych, których nie ma w kluczu danej pamięci. Wtedy pamięć podaje wszystkim cenę klienta, który jako pierwszy wygenerował stronę. Takie miejsca są trzy:

  • pamięć stron: kontekst HTTP musi zawierać cały profil cenowy konta, a nie samą stawkę rabatu. Dwóch klientów z tą samą stawką ogólną, ale innymi wyjątkami dla marek, inaczej dostanie ten sam klucz. Profil trzeba zapisywać w stałej kolejności, bo ten sam profil zapisany w innej kolejności daje inny klucz
  • pamięć bloków z ceną na listingu: klucz bloku ceny kończy się identyfikatorem grupy klienta, więc przy jednej grupie wszystkie konta dzielą ten sam wpis
  • widżety produktów z cenami, np. na stronie głównej: blok listy produktów zapisuje cały HTML z cenami, a jego klucz też rozróżnia tylko grupę

W opisanym sklepie konto z wyższym rabatem widziało na listingu cenę konta z niższym rabatem, a koszyk liczył poprawnie. Przykład umowny: przy cenie katalogowej 100 zł konto z rabatem 5% widzi 95,00 zł, a konto z rabatem 30% powinno widzieć 70,00 zł, lecz widzi 95,00 zł, bo stronę jako pierwsze wygenerowało konto z rabatem 5%. Błąd dotyczy więc ceny, którą klient widzi przy wyborze towaru. Zasadę liczenia rabatu per konto z wyjątkami dla marek opisuje kolejny wpis z serii: rabat klienta na markę w Magento Open Source.

Do kontekstu powinien trafiać profil cenowy, a nie identyfikator konta. Dokumentacja Adobe o treściach publicznych w pamięci ostrzega, że zmienna kontekstu przypisana do pojedynczego użytkownika oznacza osobną kopię strony dla każdego użytkownika. Przy profilu konta o identycznych warunkach dzielą wpisy, a liczba wariantów rośnie z liczbą różnych profili, nie kont. Po zmianie warunków konto dostaje strony liczone pod nowym kluczem, pod warunkiem że jego kontekst i ciastko X-Magento-Vary w trwającej sesji zostaną zaktualizowane. Kiedy to następuje, zależy od wdrożenia, dlatego po zmianie warunków handlowych trzeba sprawdzić ceny na koncie z aktywną sesją, a nie tylko po ponownym zalogowaniu.

Żeby ustalić, która pamięć przecieka, czyść je pojedynczo między żądaniami. W opisanym przypadku wyczyszczenie pamięci stron nie usuwało objawu, a wyczyszczenie pamięci bloków tak. Testy cen rób przez żądania HTTP z prawdziwymi sesjami klientów, bo cena klienta nie renderuje się poza żądaniem.
06

Sklep z katalogiem po zalogowaniu: pamięć odpowiada przed kontrolą logowania

W sklepach, w których katalog jest tylko dla zalogowanych, kontrola logowania zwykle działa przy obsłudze akcji. Pamięć stron odpowiada wcześniej, a o tym, którą stronę wydać, decyduje ciastko X-Magento-Vary. Dopóki pamięć nie działała dla zalogowanych, nie miało to znaczenia. Po poprawce z sekcji 04 strona z cenami może zostać wydana z pamięci każdemu, kto przyśle pasujące ciastko vary, także bez sesji klienta.

Test po wdrożeniu: żądanie bez ciastka sesji, ale z ciastkiem vary skopiowanym z zalogowanej przeglądarki, powinno skończyć się przekierowaniem do logowania, a nie stroną z cenami. Jeśli dostajesz stronę, wydanie strony z pamięci trzeba powiązać z sesją klienta i sprawdzać to powiązanie, zanim pamięć odpowie. W razie wątpliwości pamięć ma zwrócić „brak wpisu”, a nie stronę.

07

Stan po zakończeniu prac: testy odbioru

W opisanym sklepie prace zakończyły się poniższymi testami. Wszystkie przeszły:

Testy odbioru po naprawie pamięci stron
TestWynik
listing dla zalogowanego klienta, pamięć rozgrzana0,05–0,06 s
żądanie bez sesji z cudzym ciastkiem varyprzekierowanie do logowania
to samo z cudzym znacznikiem powiązania z sesją albo ze zmyśloną sesjąprzekierowanie do logowania
dwa konta z różnymi rabatami, w obu kolejnościach, od pustej pamięci: strona główna, kategoria (str. 1 i 2), podkategoria, szybkie zamawianie, wyszukiwanie, karta produktukażde konto widzi swoje ceny
dane osobowe w stronach z pamięci (imię, firma, e-mail)brak; przychodzą osobnym, prywatnym zapytaniem
konto, koszyk i zamówienia w pamięci stronnie trafiają do pamięci
08

Pamięć przyspiesza tylko strony, które już w niej są

Nawet działająca pamięć stron nie przyspiesza strony, której jeszcze w niej nie ma, czyli zimnego wpisu. Pierwsza wizyta klienta może trafić we wpis przygotowany wcześniej dla innego klienta o tym samym profilu. Ale w sklepie z profilami cenowymi każda kombinacja kategorii, liczby produktów na stronie, numeru strony i profilu to osobny wpis, więc klienci regularnie trafiają na wpisy zimne. Przy decyzji o wariantach „pokaż na stronie” liczy się czas generowania strony bez wpisu w pamięci:

Czas generowania strony bez wpisu w pamięci i odpowiedzi z pamięci w zależności od liczby produktów na stronie listingu
Produktów na stronieZimny wpisZ pamięciHTML
121,8 s0,06 s334 kB
365,7 s0,09 s533 kB
607,6 s0,06 s731 kB
12014,7 s0,08 s1,2 MB
24028,5 s0,13 s2,2 MB

W tym sklepie wychodzi około 0,11 s na każdy produkt przy generowaniu strony bez wpisu w pamięci. Nie profilowaliśmy, ile z tego przypada na rdzeń Magento, a ile na szablon i moduły, więc to zależność zmierzona w tym środowisku, a nie norma dla Magento. Wariant 240 produktów na stronie odrzuciliśmy właśnie z powodu czasu generowania zimnego wpisu.

09

Kiedy naprawisz to sam, a kiedy potrzebny jest przegląd cen

Sam

Diagnoza z sekcji 03: pomiar powtórek, liczba wpisów w Redisie, porównanie kluczy. Jeśli sklep nie ma katalogu za logowaniem ani cen przypisanych do kont, a ceny różnicujesz grupami klientów, poprawka z sekcji 04 z testem na kopii zwykle wystarcza.

Z kimś, kto to robił

Sklep z rabatami przypisanymi do kont, wyjątkami dla marek albo katalogiem za logowaniem. Tam poprawka wydajności otwiera kwestie z sekcji 05 i 06, a każde miejsce z ceną i własną pamięcią trzeba sprawdzić osobno, z prawdziwymi sesjami kilku klientów.

Gdy zlecasz nam taki przegląd, dostajesz diagnozę mechanizmu pamięci stron w Twoim sklepie (pomiary, porównanie kluczy, liczba wpisów), porównanie cen na kontach testowych w obu kolejnościach, weryfikację dostępu do katalogu bez sesji i raport z testów odbioru w układzie z sekcji 07. Więcej o pracy przy sklepach na Magento i przy indywidualnych rabatach i cennikach B2B.

FAQ: full page cache Magento po zalogowaniu

W 2.4.8 do 2.4.8-p5 moduł PageCache kieruje IdentifierInterface, używany przy odczycie z pamięci, na klasę IdentifierForSave. Ta klasa bierze odcisk klienta tylko z kontekstu żądania, który przy odczycie jest jeszcze pusty. Klucz odczytu zalogowanego klienta wychodzi więc jak dla gościa. Dotyczy to wbudowanej pamięci stron, nie Varnisha.
Zależą od sklepu. Jeśli strony gości są w pamięci, zalogowany klient może dostać stronę gościa, np. z cenami grupy gości. Jeśli katalog jest tylko po zalogowaniu, pamięć stron nie trafia nigdy i każde wejście trwa tyle, co pierwsze.
Brak przyspieszenia przy kolejnych wejściach zalogowanego klienta na ten sam adres to sygnał, nie dowód. Potwierdzeniem jest liczba wpisów pamięci stron, która nie rośnie przy kolejnych wejściach, i porównanie klucza odczytu z kluczem zapisu dla tego samego żądania. Nagłówek Cache-Control: no-store niczego tu nie dowodzi.
Mogą, jeśli cena zależy od danych, których nie ma w kluczu danej pamięci. Typowy przypadek to rabaty przypisane do kont przy wszystkich klientach w jednej grupie: pamięć bloków z ceną i widżety produktów rozróżniają w kluczu tylko grupę. Sklep, który różnicuje ceny wyłącznie grupami klientów, tego problemu nie ma.
Tak, w 2.4.9. Informacje o wydaniu wymieniają poprawkę AC-14999 („FPC not work when login”), a w kodzie klasa IdentifierForSave czyta tam najpierw ciastko X-Magento-Vary, więc odczyt dla zalogowanych znowu trafia we właściwy klucz. W 2.4.8-p5 jeszcze bierze odcisk tylko z kontekstu. W 2.4.7 odczyt korzystał z klasy Identifier i tego problemu nie było.
Tak, w sklepach z katalogiem tylko dla zalogowanych. Pamięć stron odpowiada przed kontrolą logowania i wybiera stronę po ciastku X-Magento-Vary. Żądanie bez sesji z ciastkiem vary skopiowanym z zalogowanej przeglądarki powinno skończyć się przekierowaniem do logowania, a nie stroną z cenami.