
Tworzenie wysokoobciążonych projektów w każdym języku wymaga szczególnego podejścia i zastosowania specjalnych narzędzi, ale kiedy mowa o aplikacjach w PHP, sytuacja może się zaostrzyć do tego stopnia, że konieczne staje się opracowanie na przykład, . W tym artykule omówimy dobrze znany problem z rozproszonym przechowywaniem sesji i buforowaniem danych w memcached oraz jak rozwiązaliśmy te problemy w jednym z „naszych” projektów.
Sprawcą całego zamieszania jest aplikacja w PHP oparta na frameworku symfony 2.3, której aktualizacja nie mieści się w planach biznesowych. Oprócz standardowego przechowywania sesji w tym projekcie w pełni wykorzystywano politykę »buforowania wszystkiego« w memcached: odpowiedzi na zapytania do baz danych i serwerów API, różne flagi, blokady do synchronizacji wykonywania kodu i wiele innych. W takiej sytuacji awaria memcached staje się katastrofalna dla działania aplikacji. Dodatkowo, utrata bufora prowadzi do poważnych konsekwencji: DBMS zaczyna pękać w szwach, serwisy API – blokują zapytania itd. Stabilizacja sytuacji może zająć dziesiątki minut, a w tym czasie usługa będzie działać strasznie wolno lub całkowicie przestanie być dostępna.
Musieliśmy zapewnić możliwość horyzontalnego skalowania aplikacji przy minimalnych kosztach, to znaczy przy minimalnych zmianach w kodzie źródłowym i pełnym zachowaniu funkcjonalności. Uczynić bufor nie tylko odpornym na awarie, ale także starać się zminimalizować straty danych z niego.
Co nie tak z samym memcached?
Ogólnie rzecz biorąc, rozszerzenie memcached dla PHP „z pudełka” wspiera rozproszone przechowywanie danych i sesji. Mechanizm spójnego haszowania kluczy pozwala równomiernie umieszczać dane na wielu serwerach, jednoznacznie przypisując każdy konkretny klucz do określonego serwera w grupie, a wbudowane środki failover zapewniają wysoką dostępność usługi buforowania (ale, niestety, nie danych).
Z przechowywaniem sesji sprawy mają się nieco lepiej: można skonfigurować memcached.sess_number_of_replicas, w wyniku czego dane będą natychmiast zapisywane na kilku serwerach, a w przypadku awarii jednej instancji memcached dane będą pobierane z pozostałych. Jednak, jeśli serwer wróci do działania bez danych (co zazwyczaj dzieje się po restarcie), część kluczy będzie redistribuowana na jego korzyść. Faktycznie oznacza to utrata danych sesji, ponieważ nie ma możliwości „przejścia” do innej repliki w przypadku pomyłki.
Standardowe narzędzia biblioteki są skierowane głównie na poziome skalowanie: pozwalają one zwiększyć pamięć podręczną do gigantycznych rozmiarów i zapewniają dostęp do niej z kodu umieszczonego 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 zupełnie wystarczająca. W związku z tym, jedynie użyteczne standardowe narzędzia mogłyby zapewnić dostępność memcached przy zachowaniu przynajmniej jednej instancji pamięci podręcznej w działaniu. Niemniej jednak, nawet z tej możliwości nie udało się skorzystać… Należy przypomnieć o przestarzałości frameworka zastosowanego w projekcie, przez co zmuszenie aplikacji do działania z puli serwerów okazało się niemożliwe. Nie zapominajmy także o utratach danych sesji: masowe wylogowanie użytkowników u klienta powodowało drganie powieki.
W idealnym przypadku wymagana była replikacja zapisów w memcached i obejście replik w przypadku pomyłki lub błędu. Zrealizować tę strategię pomógł nam .
mcrouter
To router dla memcached, opracowany przez firmę Facebook w celu rozwiązania jej problemów. Obsługuje tekstowy protokół memcached, który umożliwia skalowanie instalacji memcached do niespotykanych rozmiarów. Szczegółowy opis mcroutera można znaleźć w . Oprócz wielu może zrobić to, czego potrzebujemy:
- replikować zapis;
- wykonać fallback na inne serwery grupy w przypadku wystąpienia błędu.
Do roboty!
Konfiguracja mcroutera
Przejdźmy 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 pule? Dlaczego serwery się powtarzają? Przyjrzyjmy się, jak to działa.
- W tej konfiguracji mcrouter wybiera ścieżkę, po której zostanie wysłane zapytanie w zależności od typu zapytania.
OperationSelectorRoute. - Zapytania GET trafiają do obsługi
RandomRoute, która losowo wybiera pulę lub trasę spośród obiektów tablicychildren. Każdy element tej tablicy jest z kolei obsługiwany przezMissFailoverRoute, który przeszuka każdy serwer w puli, aż otrzyma odpowiedź z danymi, które zostaną zwrócone klientowi. - Gdybyśmy używali tylko
MissFailoverRoutepuli z trzema serwerami, wszystkie zapytania trafiałyby najpierw do pierwszej instancji memcached, a pozostałe otrzymywałyby zapytania w pozostałej kolejności, gdy brakowało danych. Takie podejście doprowadziłoby do nadmiernego obciążenia pierwszego serwera na liście, dlatego zdecydowano się wygenerować trzy pule z adresami w różnych sekwencjach i wybierać je losowo. - Wszystkie pozostałe zapytania (a więc zapisy) są obsługiwane za pomocą
AllMajorityRoute. Ta obsługa wysyła zapytania do wszystkich serwerów w puli i czeka na odpowiedzi od przynajmniej N/2 + 1 z nich. Korzystanie zAllSyncRoutedla operacji zapisu musiano porzucić, ponieważ ta metoda wymaga pozytywnej odpowiedzi od wszystkich serwerów grupy — w przeciwnym razie zwróciSERVER_ERROR. Chociaż w takim przypadku mcrouter zapisuje dane w dostępnych pamięciach podręcznych, funkcja wywołująca PHP zwróci błąd i wygeneruje powiadomienie.AllMajorityRoutenie jest tak surowy i pozwala na wyłączenie do połowy węzłów bez opisanych wyżej problemów.
Główną wadą tego schematu jest to, że jeśli w pamięci podręcznej naprawdę brakuje danych, każde zapytanie od klienta właściwie spowoduje wykonanie N zapytań do memcached — do z wszystkiego. serwerów w puli. Można skrócić liczbę serwerów w pulach, na przykład do dwóch: poświęcając niezawodność przechowywania, uzyskamy bwiększąwiększa prędkość i mniejsze obciążenie zapytań brakiem kluczy.
NB: Przydatne linki do nauki mcrouter mogą również obejmować i (w tym również te zamknięte), stanowiące prawdziwy skarbiec różnych konfiguracji.
Budowanie i uruchamianie mcrouter
Aplikacja (a także sam memcached) działa u nas w Kubernetes — stąd również miejsce dla mcrouter. Dla budowy kontenera używamy , a konfiguracja będzie wyglądać następująco:
NB: Wykaz, podany w artykule, został opublikowany w repozytorium .
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: Zainstaluj wymagane pakiety
apt:
name: [ 'apt-transport-https', 'tzdata', 'locales' ]
update_cache: yes
- name: Dodaj klucz APT mcrouter
apt_key:
url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
- name: Dodaj repozytorium mcrouter
apt_repository:
repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
filename: mcrouter
update_cache: yes
- name: Ustaw strefę czasową
timezone:
name: "Europe/Moscow"
- name: Upewnij się, że locale istnieje
locale_gen:
name: en_US.UTF-8
state: present
install:
- name: Zainstaluj mcrouter
apt:
name: [ 'mcrouter' ]()
… i dodajemy Helm chart. Z ciekawostek — jest tu tylko generator konfiguracji od liczby replik (jeśli ktoś ma bardziej zwięzłą i elegancką wersję — podzielcie się w komentarzach):
{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- /* Populujemy tablicę dwoma kopiami serwerów: "0 1 2 0 1 2" */ -}}
{{- range until 2 -}}
{{- range $i, $_ := until $count -}}
{{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
{{- end -}}
{{- end -}}
{{- /* Przesuwając po tablicy, uzyskujemy N segmentów: "[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|poold" $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 }}
}
}
}
}()
Wprowadzamy do środowiska testowego 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 >Szukając w tekście błąd nie przyniósł rezultatów, jednak przy zapytaniu „” w pierwszych wynikach pojawił się najstarszy nierozwiązany problem projektu — binarnego protokołu memcached.
NB: Protokół ASCII w memcached jest wolniejszy od binarnego, a także standardowe narzędzia do spójnego haszowania kluczy działają tylko z protokołem binarnym. Jednak nie stwarza to problemów w tym konkretnym przypadku.
To jest proste: wystarczy przełączyć się na protokół ASCII i wszystko zacznie działać…. Jednak w tym przypadku przyzwyczajenie do szukania odpowiedzi w zagrało okrutny żart. Nie znajdziesz tam poprawnej odpowiedzi… jeśli, oczywiście, nie przewertujesz do końca, gdzie w sekcji „Uwagi użytkowników” będzie prawidłowa i .
Tak, poprawna nazwa opcji to — memcached.sess_binary_protocol. Należy ją wyłączyć, po czym sesje zaczną działać. Wystarczy umieścić kontener z mcrouter w podzie z PHP!
Podsumowanie
W ten sposób, dzięki zmianom tylko w infrastrukturze, udało nam się rozwiązać postawiony cel: problem z odpornością na awarie w memcached został rozwiązany, a niezawodność przechowywania pamięci podręcznej została zwiększona. Poza oczywistymi zaletami dla aplikacji, dało to przestrzeń do manewru przy pracach nad platformą: gdy wszystkie komponenty mają zapas, życie administratora znacznie się upraszcza. Tak, ta metoda ma również swoje wady i może wyglądać jak „krótkie spięcie”, ale jeśli oszczędza pieniądze, tuszuje problem i nie wywołuje nowych — czemu nie?
P.S.
Przeczytaj także na naszym blogu:
- „Praktyka z dapp” (na przykładzie symfony-demo): i ;
- «».
Źródło: habr.com
