Używanie mcroutera do skalowania memcached w poziomie
Tworzenie projektów o dużym obciążeniu w dowolnym języku wymaga specjalnego podejścia i użycia specjalnych narzędzi, jednak w przypadku aplikacji w PHP sytuacja może się tak pogorszyć, że trzeba będzie opracować np. własny serwer aplikacji. W tej notatce porozmawiamy o znanych problemach związanych z rozproszonym przechowywaniem sesji i buforowaniem danych w memcached oraz o tym, jak rozwiązaliśmy te problemy w jednym projekcie „oddziałowym”.
Bohaterem okazji jest aplikacja PHP oparta na frameworku symfony 2.3, która w ogóle nie jest uwzględniona w biznesplanach aktualizacji. Oprócz dość standardowego przechowywania sesji, projekt ten w pełni wykorzystał zasady „buforowania wszystkiego”. w memcached: odpowiedzi na żądania do serwerów baz danych i API, różne flagi, blokady do synchronizacji wykonywania kodu i wiele więcej. W takiej sytuacji awaria memcachedu staje się fatalna dla działania aplikacji. Ponadto utrata pamięci podręcznej prowadzi do poważnych konsekwencji: DBMS zaczyna pękać w szwach, usługi API zaczynają blokować żądania itp. Ustabilizowanie sytuacji może zająć kilkadziesiąt minut, a w tym czasie usługa będzie strasznie powolna lub całkowicie niedostępna.
Musieliśmy zapewnić możliwość skalowania aplikacji w poziomie przy niewielkim wysiłku, tj. przy minimalnych zmianach w kodzie źródłowym i zachowaniu pełnej funkcjonalności. Spraw, aby pamięć podręczna była nie tylko odporna na awarie, ale także staraj się minimalizować utratę z niej danych.
Co jest nie tak z samym memcached?
Ogólnie rzecz biorąc, rozszerzenie memcached dla PHP obsługuje rozproszone dane i przechowywanie sesji od razu po wyjęciu z pudełka. Mechanizm spójnego mieszania kluczy pozwala na równomierne rozmieszczenie danych na wielu serwerach, jednoznacznie adresując każdy konkretny klucz do konkretnego serwera z grupy, a wbudowane narzędzia Failover zapewniają wysoką dostępność usługi buforowania (ale niestety brak danych).
Sprawa wygląda trochę lepiej w przypadku przechowywania sesji: możesz skonfigurować memcached.sess_number_of_replicas, w efekcie czego dane będą przechowywane na kilku serwerach jednocześnie, a w przypadku awarii jednej instancji memcached dane zostaną przeniesione z innych. Jeśli jednak serwer powróci do trybu online bez danych (co zwykle ma miejsce po ponownym uruchomieniu), część kluczy zostanie ponownie rozdystrybuowana na jego korzyść. W rzeczywistości będzie to oznaczać utrata danych sesji, ponieważ w przypadku chybienia nie ma możliwości „przejść” do innej repliki.
Standardowe narzędzia biblioteczne są przeznaczone głównie do poziomy skalowanie: pozwalają zwiększyć pamięć podręczną do gigantycznych rozmiarów i zapewniają dostęp do niej z kodu hostowanego na różnych serwerach. Jednak w naszej sytuacji objętość przechowywanych danych nie przekracza kilku gigabajtów, a wydajność jednego lub dwóch węzłów jest wystarczająca. W związku z tym jedynymi użytecznymi standardowymi narzędziami mogłoby być zapewnienie dostępności memcached przy jednoczesnym utrzymaniu co najmniej jednej instancji pamięci podręcznej w dobrym stanie. Jednak nawet z tej okazji nie udało się skorzystać... Warto w tym miejscu przypomnieć o starożytności frameworku użytego w projekcie, przez co nie udało się doprowadzić aplikacji do współpracy z pulą serwerów. Nie zapominajmy też o utracie danych sesji: oko klienta zakręciło się od masowego wylogowania użytkowników.
Idealnie było to wymagane replikacja rekordów w pamięci podręcznej i omijanie replik w przypadku błędu lub pomyłki. Pomógł nam wdrożyć tę strategię mcrouter.
mcrouter
Jest to router memcached opracowany przez Facebooka w celu rozwiązania jego problemów. Obsługuje protokół tekstowy memcached, który umożliwia skalować instalacje memcached do szalonych proporcji. Szczegółowy opis mcroutera można znaleźć w to ogłoszenie. Między innymi szeroka funkcjonalność może zrobić to, czego potrzebujemy:
replikować rekord;
w przypadku wystąpienia błędu wykonaj operację awaryjną na innych serwerach w grupie.
Dlaczego trzy baseny? Dlaczego serwery się powtarzają? Zastanówmy się, jak to działa.
W tej konfiguracji mcrouter na podstawie polecenia request wybiera ścieżkę, do której zostanie wysłane żądanie. Facet mu to mówi OperationSelectorRoute.
Żądania GET trafiają do procedury obsługi RandomRoutektóry losowo wybiera pulę lub trasę spośród obiektów tablicy children. Każdy element tej tablicy jest z kolei procedurą obsługi MissFailoverRoute, która przejdzie przez każdy serwer w puli do czasu otrzymania odpowiedzi z danymi, które zostaną zwrócone klientowi.
Gdybyśmy używali wyłącznie MissFailoverRoute przy puli trzech serwerów wszystkie żądania trafiałyby najpierw do pierwszej instancji memcached, a reszta otrzymywałaby żądania rezydualnie, gdy nie było danych. Takie podejście prowadziłoby do nadmierne obciążenie pierwszego serwera na liście, dlatego zdecydowano się wygenerować trzy pule z adresami w różnej kolejności i wybrać je losowo.
Wszystkie pozostałe żądania (i to jest zapis) są przetwarzane przy użyciu AllMajorityRoute. Ta procedura obsługi wysyła żądania do wszystkich serwerów w puli i czeka na odpowiedzi od co najmniej N/2 + 1 z nich. Z użycia AllSyncRoute w przypadku operacji zapisu trzeba było porzucić, ponieważ ta metoda wymaga pozytywnej odpowiedzi wszystko serwerów w grupie - w przeciwnym razie powróci SERVER_ERROR. Chociaż mcrouter doda dane do dostępnych pamięci podręcznych, wywołująca funkcja PHP zwróci błąd i wygeneruje powiadomienie. AllMajorityRoute nie jest tak rygorystyczny i pozwala na wycofanie z eksploatacji nawet połowy jednostek bez problemów opisanych powyżej.
Główna wada Ten schemat polega na tym, że jeśli w pamięci podręcznej rzeczywiście nie ma danych, to dla każdego żądania od klienta faktycznie zostanie wykonanych N żądań do memcached - aby wszystko serwery w puli. Możemy na przykład zmniejszyć liczbę serwerów w pulach do dwóch, poświęcając w ten sposób niezawodność pamięci masowejоwiększa prędkość i mniejsze obciążenie od żądań do brakujących kluczy.
NB: Możesz także znaleźć przydatne linki do nauki mcroutera dokumentacja na wiki и kwestie projektowe (w tym zamknięte), reprezentujące cały magazyn o różnych konfiguracjach.
Budowanie i uruchamianie mcroutera
Nasza aplikacja (i sam memcached) działa w Kubernetesie - w związku z tym znajduje się tam również mcrouter. Dla montaż kontenera Używamy werf, konfiguracja będzie wyglądać następująco:
NB: Listy podane w artykule są publikowane w repozytorium flant/mcrouter.
... i naszkicuj to Wykres steru. Ciekawostką jest to, że istnieje tylko generator konfiguracji oparty na liczbie replik (jeśli ktoś ma bardziej lakoniczną i elegancką opcję, podzielcie się nią w komentarzach):
# php -a
Interactive mode enabled
php > # Проверяем запись и чтение
php > $m = new Memcached();
php > $m->addServer('mcrouter', 11211);
php > var_dump($m->set('test', 'value'));
bool(true)
php > var_dump($m->get('test'));
string(5) "value"
php > # Работает! Тестируем работу сессий:
php > ini_set('session.save_handler', 'memcached');
php > ini_set('session.save_path', 'mcrouter:11211');
php > var_dump(session_start());
PHP Warning: Uncaught Error: Failed to create session ID: memcached (path: mcrouter:11211) in php shell code:1
Stack trace:
#0 php shell code(1): session_start()
#1 {main}
thrown in php shell code on line 1
php > # Не заводится… Попробуем задать session_id:
php > session_id("zzz");
php > var_dump(session_start());
PHP Warning: session_start(): Cannot send session cookie - headers already sent by (output started at php shell code:1) in php shell code on line 1
PHP Warning: session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning: session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning: session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning: session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning: session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning: session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning: session_start(): Unable to clear session lock record in php shell code on line 1
PHP Warning: session_start(): Failed to read session data: memcached (path: mcrouter:11211) in php shell code on line 1
bool(false)
php >
Wyszukiwanie tekstu błędu nie dało żadnych rezultatów, ale pojawiło się zapytanie „mcrouter php„Na pierwszym planie znajdował się najstarszy nierozwiązany problem projektu – brak wsparcia protokół binarny memcached.
NB: Protokół ASCII w memcached jest wolniejszy niż protokół binarny, a standardowe sposoby spójnego mieszania kluczy działają tylko z protokołem binarnym. Nie stwarza to jednak problemów w konkretnym przypadku.
Sztuczka jest w torbie: wystarczy, że przełączysz się na protokół ASCII i wszystko będzie działać.... Jednak w tym przypadku nawyk szukania odpowiedzi w dokumentacja na php.net odegrał okrutny żart. Nie znajdziesz tam prawidłowej odpowiedzi... chyba że przewiniesz do końca, gdzie w danym dziale „Notatki przesłane przez użytkownika” będzie wierny i niesprawiedliwie zminusowana odpowiedź.
Tak, prawidłowa nazwa opcji to memcached.sess_binary_protocol. Należy go wyłączyć, po czym sesje zaczną działać. Pozostaje tylko umieścić kontener z mcrouterem w kapsule z PHP!
wniosek
W ten sposób jedynie zmianami infrastrukturalnymi udało nam się rozwiązać problem: rozwiązano problem z odpornością na błędy memcached i zwiększono niezawodność pamięci podręcznej. Oprócz oczywistych korzyści dla aplikacji, dało to pole manewru podczas pracy na platformie: gdy wszystkie komponenty mają rezerwę, życie administratora jest znacznie uproszczone. Tak, ta metoda ma też swoje wady, może wyglądać jak „kula”, ale jeśli pozwala zaoszczędzić pieniądze, zakopuje problem i nie powoduje nowych - dlaczego nie?