
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. . 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
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 . Okrem iného 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ľachildren. Každý prvok tohto poľa je zasa handleromMissFailoverRoute, 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
MissFailoverRoutes 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žívaniaAllSyncRoutepretož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átiSERVER_ERROR. Hoci mcrouter pridá údaje do dostupných vyrovnávacích pamätí, volanie funkcie PHP vráti chybu a vygeneruje upozornenie.AllMajorityRoutenie 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 и (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 , ktorého konfigurácia bude vyzerať takto:
NB: Zoznamy uvedené v článku sú zverejnené v úložisku .
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' ]()
... 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 }}
}
}
}
}()
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 „„V popredí bol najstarší nevyriešený problém projektu – 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 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 .
Á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:
- "Cvičte s dapp" (ako príklad použite symfony-demo): и ;
- «".
Zdroj: hab.com
