Používame mcrouter na horizontálne škálovanie memcached

Používame mcrouter na horizontálne škálovanie memcached

Vývoj vysoko zaťažených projektov v akomkoľvek jazyku si vyžaduje osobitný prístup a použitie špeciálnych nástrojov, ale keď ide o aplikácie v PHP, situácia sa môže skomplikovať natoľko, že je potrebné vyvinúť napríklad vlastný aplikačný server. V tejto poznámke sa budeme venovať známej bolesti s distribuovaným uchovávaním relácií a vyrovnávaním dát v memcached a tomu, ako sme tieto problémy riešili v jednom „podopierajúcom“ projekte.

Hlavným aktérom je aplikácia v PHP, založená na frameworku symfony 2.3, ktorú aktualizácia nie je v pláne businessu. Okrem celkom štandardného uchovávania relácií sa v tomto projekte vo veľkej miere používala politika „kategorizácie všetkého“ v memcached: odpovedí na dotazy k DB a API serverom, rôznych vlajok, zámkov na synchronizáciu vykonávania kódu a mnohého iného. V takej situácii môže porucha memcached spôsobiť fatálne problémy pre fungovanie aplikácie. Navyše, strata cache vedie k vážnym následkom: DB sa začne rozpadávať, API služby - zablokujú dotazy a pod. Stabilizácia situácie môže trvať desiatky minút, zatiaľ čo služba bude hrozne spomaľovať alebo úplne nedostupná.

Potrebovali sme zabezpečiť možnosť horizontálneho škálovania aplikácie s malými nákladmi, t.j. s minimálnymi zmenami v zdrojovom kóde a plným zachovaním funkčnosti. Urobiť cache nielen odolným voči výpadkom, ale aj snažiť sa minimalizovať straty dát z nej.

Čo nie je v poriadku s memcached samotným?

Všeobecne, rozšírenie memcached pre PHP „z krabice“ podporuje distribuované uchovávanie dát a relácií. Mechanizmus konzistentného hashovania kľúčov umožňuje rovnomerne rozmiestniť dáta na viacerých serveroch, pričom každý konkrétny kľúč je jednoznačne adresovaný konkrétnemu serveru zo skupiny, a vstavané prostriedky failovera zabezpečujú vysokú dostupnosť služby cache (ale, žiaľ, nie dát).

S uchovávaním relácií sú veci o niečo lepšie: je možné nastaviť memcached.sess_number_of_replicas, v dôsledku čoho sa údaje budú ukladať okamžite na niekoľko serverov a v prípade zlyhania jedného uzla memcached budú údaje poskytované z ostatných. Avšak, ak sa server vráti do prevádzky bez údajov (ako to zvyčajne býva po reštarte), časť kľúčov bude preúsťovaná v jeho prospech. V skutočnosti to bude znamenať stratu údajov relácie, pretože nie je možné ‚ísť‘ do inej repliky v prípade chyby.

Štandardné prostriedky knižnice sú zamerané, predovšetkým, na horizontálne škálovanie: umožňujú zväčšiť cache na obrovskejšie veľkosti a zabezpečiť prístup k nej z kódu umiestneného na rôznych serveroch. V našej situácii však objem uchovávaných údajov nepresahuje niekoľko gigabajtov, a výkon jedného alebo dvoch uzlov je úplne dostatočný. Preto z užitočných štandardných prostriedkov by iba zabezpečili dostupnosť memcached s udržaním aspoň jedného funkčného uzla cache. Napriek tomu sa túto možnosť nepodarilo využiť... Tu je potrebné pripomenúť starobnosť frameworku použitým v projekte, kvôli čomu sa nepodarilo donútiť aplikáciu fungovať s poolom serverov. Nezabúdajme tiež na straty údajov relácií: masové odhlásenie používateľov u klienta vyvolalo nervozitu.

V ideálnom prípade bola potrebná replikácia zápisu v memcached a obchádzanie replik v prípade chyby alebo zlyhania. Realizovať túto stratégiu nám pomohol mcrouter.

mcrouter

Je to router pre memcached, vyvinutý spoločnosťou Facebook na riešenie jej problémov. Podporuje textový protokol memcached, ktorý umožňuje škálovať inštalácie memcached do obrovských rozmerov. Podrobný popis mcroutera nájdete v tejto správe. Okrem inej širokej funkčnosti on môže urobiť to, čo potrebujeme:

  • replikovať zápis;
  • v prípade chyby prejsť na iné servery v skupine.

Do akcie!

Konfigurácia mcroutera

Prejdeme rovno k konfigurácii:

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

Prečo sú tri pooly? Prečo sa servery opakujú? Poďme sa pozrieť, ako to funguje.

  • V tejto konfigurácii mcrouter vyberá cestu, ktorou bude odoslaný požiadavok na základe typu požiadavky. O tom ho informuje typ OperationSelectorRoute.
  • Požiadavky GET sa dostávajú do spracovateľa RandomRoute, ktorý náhodne vyberá pool alebo trasu z objektov poľa children. Každý prvok tohto poľa je zároveň spracovateľom MissFailoverRoute, ktorý prejde každým serverom v poole, kým nedostane odpoveď s údajmi, ktorá bude vrátená klientovi.
  • Keby sme používali iba MissFailoverRoute pool s tromi servermi, všetky požiadavky by najprv prichádzali na prvý exemplár memcached a ostatné by dostávali požiadavky iba v prípade, že údaje chýbajú. Takýto prístup by vedú k nadmernému zaťaženiu prvého servera v zozname, preto bolo rozhodnuté generovať tri pooly s adresami v rôznych sekvenciách a vyberať ich náhodne.
  • Všetky ostatné požiadavky (čo sú zápisy) sú spracovávané pomocou AllMajorityRoute. Tento spracovateľ odosiela požiadavky na všetky servery poľa a čaká na odpovede aspoň od N/2 + 1 z nich. Od používania AllSyncRoute pre operácie zápisu bolo potrebné upustiť, pretože táto metóda vyžaduje pozitívnu odpoveď od všetkých serverov skupiny — inak vráti SERVER_ERROR. Hoci mcrouter uloží údaje do dostupných cache, volajúca funkcia PHP vráti chybu a vygeneruje upozornenie. AllMajorityRoute nie je tak prísny a umožňuje odstaviť až polovicu uzlov bez uvedených problémov.

Hlavná nevýhoda tohto schémy je, že ak údaje v cache skutočne nie sú, na každý požiadavok od klienta sa v skutočnosti vykoná N požiadaviek na memcached — ku všetkým serverom v poole. Môžeme znížiť počet serverov v pooloch, napríklad na dva: obetovaním spoľahlivosti ukladania získame bovyššiu rýchlosť a menšiu záťaž od požiadaviek na neexistujúce kľúče.

Poznámka: Užitečné odkazy na štúdium mcroutera môžu byť tiež dokumentácia v wiki a problémy projektu (vrátane uzavretých), ktoré predstavujú celú studňu rôznych konfigurácií.

Zostavenie a spustenie mcroutera

Aplikácia (a samotný memcached) u nás funguje v Kubernetes — preto tam má miesto aj mcrouter. Pre zostavenie kontajnera používame werf, konfigurácia bude vyzerať nasledovne:

Poznámka: Zoznamy uvedené v článku sú publikované v repozitári 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: Nainštalovať predpoklady
   apt:
     name: [ 'apt-transport-https', 'tzdata', 'locales' ]
     update_cache: yes
 - name: Pridať mcrouter APT kľúč
   apt_key:
     url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
 - name: Pridať mcrouter repo
   apt_repository:
     repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
     filename: mcrouter
     update_cache: yes
 - name: Nastaviť časovú zónu
   timezone:
     name: "Europe/Moscow"
 - name: Zabezpečiť, aby existovala lokalita
   locale_gen:
     name: en_US.UTF-8
     state: present
 install:
 - name: Nainštalovať mcrouter
   apt:
     name: [ 'mcrouter' ]

(werf.yaml)

… a len tak nadhadzujeme Helm graf. Zaujímavé je, že tu len generátor konfigurácie na základe počtu replik (ak má niekto elegantnejšie a stručnejšie riešenie — podeľte sa v komentároch):

{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- \/\* Naplňujeme pole dvěma kopiemi serverů: "0 1 2 0 1 2" \*\/ -}}
{{- range until 2 -}}
 {{- range $i, $_ := until $count -}}
 {{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
 {{- end -}}
{{- end -}}
{{- \/\* Posunem po poli získáváme N segmentů: "[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 }}
 }
 }
 }
 }

(10-mcrouter.yaml)

Nasadzujeme do testovacieho prostredia a kontrolujeme:

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

Hľadanie v texte chyby nič nepomohlo, avšak pri dopyte „mcrouter php“ sa v prvých miestach umiestnila najstaršia nezatvorená probléma projektu — nepodpora binárneho protokolu memcached.

PoznámkaASCII protokol v memcached je pomalší než binární protokol a standardní nástroje pro konzistentní hashování klíčů fungují pouze s binárním protokolem. Ale pro konkrétní případ to nepředstavuje žádný problém.

Je to hotové: stačí přepnout na ASCII protokol a vše začne fungovat… Avšak v tomto případě se zvyk hledat odpovědi v dokumentaci na php.net se mi vymstil. Správnou odpověď tam nenajdete… pokud se, samozřejmě, neproscrollujete na konec, kde v sekci „User contributed notes“ nalezneme správnou a nespravedlivě mínusovanou odpověď..

Ano, správný název volby je memcached.sess_binary_protocol. Je potřebné ji deaktivovat, poté začnou sezení fungovat. Stačí jen umístit kontejner s mcrouter do podu s PHP!

Záver

Takto jsme dokázali vyřešit daný úkol pouze změnami v infrastruktuře: otázka s odolností memcached byla řešena, spolehlivost ukládání cache byla zvýšena. Kromě zřejmých výhod pro aplikaci to poskytlo prostor pro manévry při práci na platformě: když mají všechny komponenty zálohu, život administrátora se výrazně zjednodušuje. Ano, tato metoda má své nevýhody, může vypadat jako "šidítko", ale pokud šetří peníze, zakrývá problém a nezpůsobuje nové — proč ne?

P.S.

Prečítajte si tiež na našom blogu:

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster