Używanie mcroutera do skalowania memcached w poziomie

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.

Przejdźmy do rzeczy!

konfiguracja mcroutera

Przejdę od razu do konfiguracji:

{
 "pools": {
   "pool00": {
     "servers": [
       "mc-0.mc:11211",
       "mc-1.mc:11211",
       "mc-2.mc:11211"
   },
   "pool01": {
     "servers": [
       "mc-1.mc:11211",
       "mc-2.mc:11211",
       "mc-0.mc:11211"
   },
   "pool02": {
     "servers": [
       "mc-2.mc:11211",
       "mc-0.mc:11211",
       "mc-1.mc:11211"
 },
 "route": {
   "type": "OperationSelectorRoute",
   "default_policy": "AllMajorityRoute|Pool|pool00",
   "operation_policies": {
     "get": {
       "type": "RandomRoute",
       "children": [
         "MissFailoverRoute|Pool|pool02",
         "MissFailoverRoute|Pool|pool00",
         "MissFailoverRoute|Pool|pool01"
       ]
     }
   }
 }
}

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.

configVersion: 1
project: mcrouter
deploy:
 namespace: '[[ env ]]'
 helmRelease: '[[ project ]]-[[ env ]]'
---
image: mcrouter
from: ubuntu:16.04
mount:
- from: tmp_dir
 to: /var/lib/apt/lists
- from: build_dir
 to: /var/cache/apt
ansible:
 beforeInstall:
 - name: Install prerequisites
   apt:
     name: [ 'apt-transport-https', 'tzdata', 'locales' ]
     update_cache: yes
 - name: Add mcrouter APT key
   apt_key:
     url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
 - name: Add mcrouter Repo
   apt_repository:
     repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
     filename: mcrouter
     update_cache: yes
 - name: Set timezone
   timezone:
     name: "Europe/Moscow"
 - name: Ensure a locale exists
   locale_gen:
     name: en_US.UTF-8
     state: present
 install:
 - name: Install mcrouter
   apt:
     name: [ 'mcrouter' ]

(werf.yaml)

... 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):

{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- /* Заполняем  массив двумя копиями серверов: "0 1 2 0 1 2" */ -}}
{{- range until 2 -}}
 {{- range $i, $_ := until $count -}}
   {{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
 {{- end -}}
{{- end -}}
{{- /* Смещаясь по массиву, получаем N срезов: "[0 1 2] [1 2 0] [2 0 1]" */ -}}
{{- range $i, $_ := until $count -}}
 {{- $pool := dict "servers" (slice $servers $i (add $i $count)) -}}
 {{- $_ := set $pools (printf "MissFailoverRoute|Pool|pool%02d" $i) $pool -}}
{{- end -}}
---
apiVersion: v1
kind: ConfigMap
metadata:
 name: mcrouter
data:
 config.json: |
   {
     "pools": {{- $pools | toJson | replace "MissFailoverRoute|Pool|" "" -}},
     "route": {
       "type": "OperationSelectorRoute",
       "default_policy": "AllMajorityRoute|Pool|pool00",
       "operation_policies": {
         "get": {
           "type": "RandomRoute",
           "children": {{- keys $pools | toJson }}
         }
       }
     }
   }

(10-mcrouter.yaml)

Wdrażamy go na środowisko testowe i sprawdzamy:

# 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?

PS

Przeczytaj także na naszym blogu:

Źródło: www.habr.com

Dodaj komentarz