Použitie mcrouter na horizontálne škálovanie memcached

Použitie mcrouter na horizontálne škálovanie memcached

Vývoj projektov s vysokou záťažou v akomkoľvek jazyku si vyžaduje špeciálny prístup a použitie špeciálnych nástrojov, ale pokiaľ ide o aplikácie v PHP, situácia sa môže natoľko vyhrotiť, že budete musieť vyvíjať napr. vlastný aplikačný server. V tejto poznámke budeme hovoriť o známej bolesti s distribuovaným ukladaním relácií a ukladaním údajov do vyrovnávacej pamäte v memcached a ako sme tieto problémy vyriešili v jednom projekte „ward“.

Hrdinom tejto príležitosti je aplikácia PHP založená na frameworku Symfony 2.3, ktorá nie je vôbec zahrnutá v obchodných plánoch na aktualizáciu. Okrem celkom štandardného úložiska relácií tento projekt plne využil zásady „ukladanie všetkého do vyrovnávacej pamäte“. v memcached: odpovede na požiadavky na databázové a API servery, rôzne príznaky, zámky na synchronizáciu spúšťania kódu a mnoho ďalšieho. V takejto situácii sa porucha memcached stane osudnou pre fungovanie aplikácie. Okrem toho strata vyrovnávacej pamäte vedie k vážnym následkom: DBMS začne praskať vo švíkoch, služby API začnú zakazovať požiadavky atď. Stabilizácia situácie môže trvať desiatky minút a počas tejto doby bude služba strašne pomalá alebo úplne nedostupná.

Potrebovali sme poskytnúť schopnosť horizontálne škálovať aplikáciu s malým úsilím, t.j. s minimálnymi zmenami zdrojového kódu a zachovaním plnej funkčnosti. Zabezpečte, aby bola vyrovnávacia pamäť nielen odolná voči zlyhaniam, ale tiež sa snažte minimalizovať stratu údajov z nej.

Čo je zlé na samotnom memcached?

Vo všeobecnosti rozšírenie memcached pre PHP podporuje distribuované dáta a ukladanie relácií hneď po vybalení. Mechanizmus konzistentného hashovania kľúčov vám umožňuje rovnomerne umiestňovať údaje na mnohých serveroch, pričom každý konkrétny kľúč jedinečne adresujete konkrétnemu serveru zo skupiny a vstavané nástroje na prepnutie pri zlyhaní zaisťujú vysokú dostupnosť služby ukladania do vyrovnávacej pamäte (ale, bohužiaľ, žiadne dáta).

S ukladaním relácií je to trochu lepšie: môžete ho nakonfigurovať memcached.sess_number_of_replicas, v dôsledku čoho budú dáta uložené na viacerých serveroch naraz a v prípade výpadku jednej memcached inštancie budú dáta prenesené z iných. Ak sa však server vráti do režimu online bez údajov (ako sa to zvyčajne stáva po reštarte), niektoré kľúče budú prerozdelené v jeho prospech. V skutočnosti to bude znamenať strata údajov relácie, keďže neexistuje spôsob, ako „prejsť“ na inú repliku v prípade zmeškania.

Štandardné knižničné nástroje sú zamerané hlavne na horizontálne škálovanie: umožňujú vám zväčšiť vyrovnávaciu pamäť na gigantické veľkosti a poskytnúť k nej prístup z kódu hosťovaného na rôznych serveroch. V našej situácii však objem uložených dát nepresahuje niekoľko gigabajtov a výkon jedného alebo dvoch uzlov úplne postačuje. Jedinými užitočnými štandardnými nástrojmi by teda mohlo byť zabezpečenie dostupnosti memcached pri udržiavaní aspoň jednej inštancie vyrovnávacej pamäte v prevádzkovom stave. Ani túto možnosť však nebolo možné využiť... Tu je vhodné pripomenúť starobylosť frameworku použitého v projekte, a preto nebolo možné prinútiť aplikáciu pracovať s poolom serverov. Nezabúdajme ani na stratu údajov o relácii: zákazníkovi trhlo oko pri masívnom odhlásení používateľov.

V ideálnom prípade to bolo potrebné replikácia záznamov v memcached a obchádzanie replík v prípade omylu alebo omylu. Pomohli nám implementovať túto stratégiu mcrouter.

mcrouter

Ide o memcached router vyvinutý Facebookom na riešenie jeho problémov. Podporuje textový protokol memcached, ktorý umožňuje škálovať inštalácie uložené v pamäti cache do šialených rozmerov. Podrobný popis mcrouter nájdete v toto oznámenie. Okrem iného široká funkčnosť dokáže to, čo potrebujeme:

  • replikovať záznam;
  • ak sa vyskytne chyba, vráťte sa na iné servery v skupine.

Poďme na vec!

konfigurácia mcrouter

Prejdem rovno ku 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 tri bazény? Prečo sa servery opakujú? Poďme zistiť, ako to funguje.

  • V tejto konfigurácii mcrouter vyberie cestu, na ktorú bude požiadavka odoslaná na základe príkazu request. Chlapík mu to povie OperationSelectorRoute.
  • Požiadavky GET idú obsluhe RandomRoutektorý náhodne vyberie oblasť alebo trasu medzi objektmi poľa children. Každý prvok tohto poľa je zasa handlerom MissFailoverRoute, ktorý bude prechádzať každým serverom v oblasti, kým nedostane odpoveď s údajmi, ktoré sa vrátia klientovi.
  • Ak by sme používali výlučne MissFailoverRoute s fondom troch serverov by potom všetky požiadavky prichádzali ako prvé do prvej inštancie uloženej v memcached a zvyšok by dostával požiadavky na zvyškovej báze, keď neexistujú žiadne údaje. Takýto prístup by viedol k nadmerné zaťaženie prvého servera v zozname, preto bolo rozhodnuté vygenerovať tri fondy s adresami v rôznych sekvenciách a vybrať ich náhodne.
  • Všetky ostatné požiadavky (a toto je záznam) sú spracované pomocou AllMajorityRoute. Tento handler posiela požiadavky na všetky servery v oblasti a čaká na odpovede od aspoň N/2 + 1 z nich. Z používania AllSyncRoute pretože operácie zápisu museli byť opustené, pretože táto metóda vyžaduje kladnú odozvu všetko serverov v skupine - inak sa vráti SERVER_ERROR. Hoci mcrouter pridá údaje do dostupných vyrovnávacích pamätí, volanie funkcie PHP vráti chybu a vygeneruje upozornenie. AllMajorityRoute nie je taký prísny a umožňuje vyradiť z prevádzky až polovicu jednotiek bez vyššie popísaných problémov.

Hlavná nevýhoda Táto schéma je taká, že ak v cache naozaj nie sú žiadne dáta, tak pre každú požiadavku od klienta sa skutočne vykoná N požiadaviek na memcached - do každý servery v bazéne. Môžeme znížiť počet serverov v pooloch napríklad na dva: obetujeme spoľahlivosť úložiskaоvyššia rýchlosť a menšia záťaž z požiadaviek na chýbajúce kľúče.

NB: Môžete tiež nájsť užitočné odkazy na učenie sa mcrouter dokumentácia na wiki и problematika projektu (vrátane uzavretých), predstavujúce celý sklad rôznych konfigurácií.

Stavba a prevádzka mcrouteru

Naša aplikácia (a samotná memcached) beží v Kubernetes - podľa toho sa tam nachádza aj mcrouter. Pre zostava kontajnera používame werf, ktorého konfigurácia bude vyzerať takto:

NB: Zoznamy uvedené v článku sú zverejnené v úložisku 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črtnite to Tabuľka kormidla. Zaujímavosťou je, že existuje iba generátor konfigurácií podľa počtu replík (ak má niekto lakonickejšiu a elegantnejšiu možnosť, podeľte sa o ňu v komentároch):

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

Zavedieme ho do testovacieho prostredia a skontrolujeme:

# 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 textu chyby neprinieslo žiadne výsledky, ale dopyt „mcrouter php„V popredí bol najstarší nevyriešený problém projektu – nedostatok podpory binárny protokol memcached.

NB: Protokol ASCII v memcached je pomalší ako binárny a štandardné prostriedky konzistentného hashovania kľúčov fungujú iba s binárnym protokolom. To však nevytvára problémy pre konkrétny prípad.

Trik je vo vrecku: stačí prepnúť na protokol ASCII a všetko bude fungovať.... Avšak v tomto prípade zvyk hľadať odpovede v dokumentáciu na php.net zahral krutý vtip. Nenájdete tam správnu odpoveď... pokiaľ, samozrejme, neprejdete na koniec, kde v sekcii "Poznámky pridané používateľom" bude verný a nespravodlivo záporná odpoveď.

Áno, správny názov možnosti je memcached.sess_binary_protocol. Musí byť deaktivovaný, po ktorom začnú relácie fungovať. Zostáva len vložiť kontajner s mcrouterom do podu s PHP!

Záver

Takže iba zmenami v infraštruktúre sme dokázali vyriešiť problém: problém s toleranciou chýb memcached bol vyriešený a spoľahlivosť vyrovnávacej pamäte sa zvýšila. Okrem očividných výhod pre aplikáciu to dávalo priestor na manévrovanie pri práci na platforme: keď majú všetky komponenty rezervu, výrazne sa zjednoduší život správcu. Áno, táto metóda má aj svoje nevýhody, môže to vyzerať ako „barla“, ale ak šetrí peniaze, pochováva problém a nespôsobuje nové - prečo nie?

PS

Prečítajte si aj na našom blogu:

Zdroj: hab.com

Pridať komentár