
Udviklingen af højtydende projekter på ethvert sprog kræver en særlig tilgang og brugen af specielle værktøjer, men når det kommer til applikationer på PHP, kan situationen blive så vanskelig, at man er nødt til at udvikle for eksempel . I denne kommentar vil vi tale om den velkendte udfordring med distribueret sessionlagring og datacaching i memcached, og hvordan vi løste disse problemer i et "projekt under vores opsyn".
Hovedpersonen er en PHP-applikation, der er baseret på rammeværket symfony 2.3, som ikke er planlagt til at blive opdateret i forretningens planer. Udover den ganske standard sessionlagring anvendtes der i dette projekt en politik for "caching af alt" i memcached: svar på anmodninger til databaser og API-servere, forskellige flags, låse til synkronisering af kodeudførelse og meget andet. I en sådan situation bliver en fejl i memcached fatalt for applikationens drift. Desuden fører tab af cache til alvorlige konsekvenser: databasen begynder at knække under presset, API-tjenesterne blokerer anmodninger osv. Stabilisering af situationen kan tage mange minutter, i mellemtiden vil servicen være ekstremt langsom eller blive helt utilgængelig.
Vi måtte sikre muligheden for horisontal skalering af applikationen med minimal indsats, dvs. med minimale ændringer af kildekoden og fuld bevarelse af funktionalitet. At gøre cachen ikke bare fejltolerance, men også at forsøge at minimere datatab fra den.
Hvad er der galt med selve memcached?
Generelt understøtter memcached-udvidelsen til PHP "ud af boksen" distribueret lagring af data og sessioner. Mekanismen for konsistent hashning af nøgler muliggør jævn placering af data på mange servere ved entydigt at adressere hver specifik nøgle til en bestemt server i gruppen, og de indbyggede failover-midler sikrer høj tilgængelighed af cachingtjenesten (men desværre) ikke dataene.).
Med lagring af sessioner er situationen lidt bedre: man kan konfigurere memcached.sess_number_of_replicas, hvilket betyder, at data vil blive gemt simultant på flere servere, og i tilfælde af, at en instans af memcached fejler, vil data blive returneret fra andre. Men hvis serveren vender tilbage uden data (som ofte sker efter en genstart), vil nogle nøgler blive omfordelt til dens fordel. Faktisk vil dette betyde tab af sessionsdata, da der ikke er mulighed for at 'besøge' en anden replika i tilfælde af fejlslag.
De standardværktøjer, der er tilgængelige i biblioteket, er primært rettet mod horisontal skalerbarhed: de giver mulighed for at udvide cachen til enorme størrelser og sikre adgang til den fra kode, der er placeret på forskellige servere. Men i vores situation overskrider mængden af gemte data ikke et par gigabyte, og ydeevnen fra en eller to noder er helt tilstrækkelig. Følgelig kunne de nyttige standardværktøjer kun sikre tilgængeligheden af memcached, så længe der var mindst én arbejdsduelig cache-instans. Dog kunne vi ikke få selv denne mulighed til at fungere... Her bør man mindes om det gamle framework, der blev brugt i projektet, hvilket gjorde det umuligt at få applikationen til at fungere med en serverpulje. Lad os heller ikke glemme tabet af sessionsdata: fra masselogs udviste kunden nervøsitet.
Ideelt set var der behov for replikering af optegnelser i memcached og omgåelse af replikater i tilfælde af fejlslag eller fejl. At implementere denne strategi blev hjulpet af .
mcrouter
Det er en router til memcached, udviklet af Facebook for at løse deres problemer. Den understøtter tekstprotokollen memcached, som gør det muligt at skalerer memcached-installationer til vanvittige størrelser. En detaljeret omtale af mcrouter kan findes i . Udover anden kan den det, vi har brug for:
- replikere optegnelser;
- falde tilbage til andre servere i gruppen i tilfælde af fejl.
Lad os komme i gang!
Mcrouter-konfiguration
Jeg går straks til konfigurationen:
{
"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"
]
}
}
}
}Hvorfor tre puljer? Hvorfor gentages serverne? Lad os finde ud af, hvordan det fungerer.
- I denne konfiguration vælger mcrouter en rute baseret på typen af forespørgsel. Dette angives af typen
OperationSelectorRoute. - GET-forespørgsler sendes til behandleren
RandomRoute, som tilfældigt vælger en pulje eller rute blandt objekterne i arrayetchildren. Hvert element i dette array er i sig selv en behandlerMissFailoverRoute, som gennemgår hver server i puljen, indtil den får et svar med data, som derefter returneres til klienten. - Hvis vi kun havde brugt
MissFailoverRoutemed en pulje af tre servere, ville alle forespørgsler først komme til den første instans af memcached, mens de andre ville modtage forespørgsler efter behov, når data mangler. Denne tilgang ville føre til overbelastning af den første server i listen, derfor blev det besluttet at generere tre puljer med adresser i forskellige sekvenser og vælge dem tilfældigt. - Alle andre forespørgsler (dvs. skrivninger) behandles ved hjælp af
AllMajorityRoute. Denne behandler sender forespørgsler til alle servere i puljen og venter på svar fra mindst N/2 + 1 af dem. Brugen afAllSyncRoutetil skriveoperationer måtte opgives, da denne metode kræver et positivt svar fra af alle serverne i gruppen — ellers returnerer denSERVER_ERROR. Selvom mcrouter samlet set vil lagre data i de tilgængelige cache, vil den kaldende PHP-funktion returnere en fejl og generere en notice.AllMajorityRouteer ikke så striks og tillader at op til halvdelen af noderne bringes ud af drift uden de ovennævnte problemer.
Den største ulempe ved denne ordning er, at hvis data ikke er tilgængelige i cachen, så vil der i virkeligheden blive udført N forespørgsler til memcached for hver klientforespørgsel — til alle serverne i puljen. Man kan reducere antallet af servere i puljerne, f.eks. til to: ved at ofre datahosghed får vi bfleste afstørre hastighed og mindre belastning fra forespørgsler til manglende nøgler.
NB: Nyttige links til at lære om mcrouter kan også være og (herunder lukkede), som repræsenterer en skat af forskellige konfigurationer.
Bygning og kørsel af mcrouter
Applikationen (og selve memcached) kører vi i Kubernetes — derfor er mcrouter også der. Til bygning af containeren bruger vi , konfigurationen ser således ud:
NB: Listerne, der er nævnt i artiklen, er offentliggjort i repositoryet .
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' ]()
… og vi laver et udkast Helm-chart. Af interessant er her kun konfiggeneratoren baseret på antallet af replikaer (hvis nogen har en mere kompakt og elegant løsning — del gerne i kommentarerne):
{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{-
$A AA A A A {{- range until 2 -}}
{{- range $i, $_ := until $count -}}
{{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
{{- end -}}
{{- end -}}
{{-
A A A AA ${{A{{replicate . $e }}}A A}}.mc:11211"
}
}\n }}A }" -}}
{
A {
A {"pools": {{- $pools | toJson | replace "MissFailoverRoute|Pool|" "" -}},
"route": {
"type": "OperationSelectorRoute",
"default_policy": "AllMajorityRoute|Pool|pool00",
"operation_policies": {
"get": {
"type": "RandomRoute",
"children": {{- keys $pools | toJson }}
}
}
}
}()
Udrul i testmiljøet og tjek:
# 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 >Søgning efter fejlen gav ikke resultater, men ved forespørgslen «» blev den ældste åbne problemstilling i projektet nævnt — den binære protokol memcached.
NB: ASCII-protokol i memcached er langsommere end binær, og de indbyggede værktøjer til konsistent hashing af nøgler fungerer kun med binær protokol. Men det skaber ikke problemer for det specifikke tilfælde.
Sagen er klar: det eneste, der mangler, er at skifte til ASCII-protokollen, og så vil det hele fungere… Men i dette tilfælde spillede vanen med at søge svar i et snyderi. Det korrekte svar finder du ikke der… medmindre du selvfølgelig scroller til sidst, hvor i sektionen «User contributed notes» det vil være korrekt og .
Ja, den rigtige betegnelse for indstillingen er memcached.sess_binary_protocol. Den skal deaktiveres, hvorefter sessionerne vil begynde at fungere. Det eneste, der mangler, er at placere containeren med mcrouter i pod’en med PHP!
Konklusion
På denne måde lykkedes det os at løse den stillede opgave med blot infrastrukturelle ændringer: spørgsmålet om memcacheds fejltolerance er løst, pålideligheden af cachelagringen er blevet forbedret. Udover de åbenlyse fordele for applikationen gav det også plads til manøvrering ved arbejdet på platformen: når alle komponenter har en backup, bliver administratorens liv meget lettere. Ja, denne metode har også sine ulemper, det kan se ud som en «nødsolution», men hvis det sparer penge, gemmer problemet og ikke skaber nye — hvorfor ikke?
P.S.
Læs også på vores blog:
- «Praktik med dapp» (eksemplet symfony-demo): og ;
- «».
Kilde: habr.com
