
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 . 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
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 . Okrem inej 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ľachildren. Každý prvok tohto poľa je zároveň spracovateľomMissFailoverRoute, ktorý prejde každým serverom v poole, kým nedostane odpoveď s údajmi, ktorá bude vrátená klientovi. - Keby sme používali iba
MissFailoverRoutepool 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žívaniaAllSyncRoutepre 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átiSERVER_ERROR. Hoci mcrouter uloží údaje do dostupných cache, volajúca funkcia PHP vráti chybu a vygeneruje upozornenie.AllMajorityRoutenie 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ž a (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 , konfigurácia bude vyzerať nasledovne:
Poznámka: Zoznamy uvedené v článku sú publikované v repozitári .
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' ]()
… 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 }}
}
}
}
}()
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 „“ sa v prvých miestach umiestnila najstaršia nezatvorená probléma projektu — 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 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 .
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:
- „Praxe s dapp“ (na příkladu symfony-demo): a ;
- «».
Zdroj: habr.com
