Použití mcrouter pro horizontální měřítko memcached

Použití mcrouter pro horizontální měřítko memcached

Vývoj vysoce zatěžovaných projektů v jakémkoli jazyce vyžaduje speciální přístup a použití speciálních nástrojů, ale pokud jde o aplikace v PHP, situace se může natolik vyhrotit, že budete muset vyvíjet např. vlastní aplikační server. V této poznámce budeme hovořit o známé bolesti s distribuovaným ukládáním relací a ukládáním dat do mezipaměti v memcached a jak jsme tyto problémy vyřešili v jednom projektu „ward“.

Hrdinou této příležitosti je aplikace PHP založená na frameworku Symfony 2.3, která není vůbec zahrnuta v obchodních plánech na aktualizaci. Kromě zcela standardního úložiště relací tento projekt plně využil zásada „ukládání všeho do mezipaměti“. v memcached: odpovědi na požadavky na databázové a API servery, různé příznaky, zámky pro synchronizaci spouštění kódu a mnoho dalšího. V takové situaci se selhání memcached stane pro chod aplikace fatální. Ztráta mezipaměti navíc vede k vážným následkům: DBMS začne praskat ve švech, služby API začnou zakazovat požadavky atd. Stabilizace situace může trvat desítky minut a během této doby bude služba strašně pomalá nebo úplně nedostupná.

Potřebovali jsme zajistit schopnost horizontálně škálovat aplikaci s malým úsilím, tj. s minimálními změnami ve zdrojovém kódu a zachováním plné funkčnosti. Udělejte mezipaměť nejen odolnou vůči selhání, ale také se snažte minimalizovat ztrátu dat z ní.

Co je špatného na samotném memcached?

Rozšíření memcached pro PHP obecně podporuje distribuovaná data a úložiště relací ihned po vybalení. Mechanismus konzistentního hašování klíčů vám umožňuje rovnoměrně umístit data na mnoho serverů, přičemž každý konkrétní klíč jedinečně adresujete konkrétnímu serveru ze skupiny, a vestavěné nástroje pro překonání selhání zajišťují vysokou dostupnost služby mezipaměti (ale bohužel, žádná data).

S úložištěm relace je to trochu lepší: můžete nakonfigurovat memcached.sess_number_of_replicas, v důsledku čehož budou data uložena na více serverech najednou a v případě výpadku jedné instance memcached budou data přenesena z dalších. Pokud se však server vrátí do režimu online bez dat (jak se obvykle stává po restartu), některé klíče budou přerozděleny v jeho prospěch. Ve skutečnosti to bude znamenat ztráta dat relace, protože neexistuje způsob, jak „přejít“ do jiné repliky v případě neúspěchu.

Standardní knihovní nástroje jsou zaměřeny především na horizontální škálování: umožňují vám zvětšit mezipaměť na gigantické velikosti a poskytnout k ní přístup z kódu hostovaného na různých serverech. V naší situaci však objem uložených dat nepřesahuje několik gigabajtů a výkon jednoho nebo dvou uzlů je docela dost. Jedinými užitečnými standardními nástroji by tedy mohlo být zajištění dostupnosti memcached při zachování alespoň jedné instance mezipaměti ve funkčním stavu. Ani této příležitosti však nebylo možné využít... Zde je vhodné připomenout starobylost frameworku použitého v projektu, a proto nebylo možné aplikaci přimět k práci s poolem serverů. Nezapomínejme také na ztrátu dat relace: zákazníkovi cukalo oko při masivním odhlašování uživatelů.

V ideálním případě to bylo vyžadováno replikace záznamů v memcached a obcházení replik v případě chyby nebo omylu. Pomohli nám implementovat tuto strategii mcrouter.

mcrouter

Jedná se o memcached router vyvinutý Facebookem k vyřešení jeho problémů. Podporuje textový protokol memcached, který umožňuje škálovat instalace v memcached do šílených rozměrů. Podrobný popis mcrouteru naleznete v toto oznámení. Mimo jiné široká funkčnost umí to, co potřebujeme:

  • replikovat záznam;
  • pokud dojde k chybě, vraťte se na ostatní servery ve skupině.

Jít přímo k věci!

konfigurace mcrouteru

Přejdu rovnou ke konfiguraci:

{
 "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"
       ]
     }
   }
 }
}

Proč tři bazény? Proč se servery opakují? Pojďme zjistit, jak to funguje.

  • V této konfiguraci mcrouter na základě příkazu request vybere cestu, na kterou bude požadavek odeslán. Ten chlap mu to řekne OperationSelectorRoute.
  • Požadavky GET jdou k obsluze RandomRoutekterý náhodně vybere fond nebo trasu mezi objekty pole children. Každý prvek tohoto pole je zase handler MissFailoverRoute, který bude procházet každým serverem ve fondu, dokud neobdrží odpověď s daty, která se vrátí klientovi.
  • Pokud bychom používali výhradně MissFailoverRoute s fondem tří serverů by pak všechny požadavky přicházely jako první do první instance uložené v memcachingu a zbytek by dostával požadavky na zbytkové bázi, když nejsou žádná data. Takový přístup by vedl k nadměrné zatížení prvního serveru v seznamu, takže bylo rozhodnuto vygenerovat tři fondy s adresami v různém pořadí a vybrat je náhodně.
  • Všechny ostatní požadavky (a toto je záznam) jsou zpracovávány pomocí AllMajorityRoute. Tento handler odesílá požadavky na všechny servery ve fondu a čeká na odpovědi od alespoň N/2 + 1 z nich. Z používání AllSyncRoute protože operace zápisu musely být opuštěny, protože tato metoda vyžaduje kladnou odezvu vše servery ve skupině - jinak se vrátí SERVER_ERROR. Ačkoli mcrouter přidá data do dostupných mezipamětí, volání funkce PHP vrátí chybu a vygeneruje upozornění. AllMajorityRoute není tak striktní a umožňuje vyřadit z provozu až polovinu jednotek bez výše popsaných problémů.

Hlavní minus Toto schéma spočívá v tom, že pokud v mezipaměti skutečně nejsou žádná data, pak pro každý požadavek od klienta bude skutečně provedeno N požadavků na memcached - do vše servery v bazénu. Můžeme snížit počet serverů ve fondech například na dva: obětujeme spolehlivost úložištěоvyšší rychlost a menší zatížení z požadavků na chybějící klíče.

NB: Můžete také najít užitečné odkazy pro výuku mcrouter dokumentace na wiki и problémy projektu (včetně uzavřených), představující celý sklad různých konfigurací.

Stavba a provoz mcrouteru

Naše aplikace (a samotná memcached) běží v Kubernetes - podle toho je tam umístěn i mcrouter. Pro sestava kontejneru používáme werf, jehož konfigurace bude vypadat takto:

NB: Výpisy uvedené v článku jsou zveřejněny v úložišti plochý/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)

... a načrtni to Tabulka kormidla. Zajímavostí je, že existuje pouze generátor konfigurací podle počtu replik (pokud má někdo lakoničtější a elegantnější možnost, podělte se o ni v komentářích):

{{- $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)

Zavedeme jej do testovacího prostředí a zkontrolujeme:

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

Hledání textu chyby nepřineslo žádné výsledky, ale dotaz „mikrouter php„V popředí byl nejstarší nevyřešený problém projektu – nedostatek podpory binární protokol memcached.

NB: Protokol ASCII v memcached je pomalejší než binární a standardní prostředky konzistentního hašování klíčů fungují pouze s binárním protokolem. To však nezpůsobuje problémy pro konkrétní případ.

Trik je v pytli: stačí přepnout na protokol ASCII a vše bude fungovat.... Nicméně v tomto případě je zvyk hledat odpovědi v dokumentace na php.net zahrál krutý vtip. Nenajdete tam správnou odpověď... pokud samozřejmě nepřejdete na konec, kde v sekci "Poznámky přidané uživateli" bude věrný a nespravedlivě záporná odpověď.

Ano, správný název možnosti je memcached.sess_binary_protocol. Musí být deaktivován, poté začnou relace fungovat. Zbývá pouze vložit kontejner s mcrouterem do podu s PHP!

Závěr

Pouhými změnami v infrastruktuře jsme tedy dokázali problém vyřešit: problém s odolností proti chybám memcached byl vyřešen a spolehlivost úložiště mezipaměti byla zvýšena. Kromě zjevných výhod pro aplikaci to dalo prostor pro manévrování při práci na platformě: když mají všechny komponenty rezervu, život administrátora se výrazně zjednoduší. Ano, tato metoda má také své nevýhody, může to vypadat jako „berlička“, ale pokud šetří peníze, pohřbívá problém a nezpůsobuje nové - proč ne?

PS

Přečtěte si také na našem blogu:

Zdroj: www.habr.com

Přidat komentář