Folosim mcrouter pentru scalarea orizontală a memcached

Folosim mcrouter pentru scalarea orizontală a memcached

Dezvoltarea proiectelor cu încărcare mare în orice limbă necesită o abordare specială și utilizarea unor instrumente speciale, dar atunci când vine vorba de aplicații PHP, situația poate deveni atât de critică încât este necesar să dezvoltăm, de exemplu, un server de aplicații propriu. În această notă se va discuta despre durerea familiară tuturor cu stocarea distribuită a sesiunilor și cache-ului de date în memcached și despre cum am rezolvat aceste probleme într-un proiect „sub protecția” noastră.

Vinovatul de sărbătoare este o aplicație PHP bazată pe framework-ul symfony 2.3, actualizarea căruia nu se încadrează în planurile de afaceri. Pe lângă stocarea standard a sesiunilor, în acest proiect a fost utilizată pe larg politica „cache-ării totului” în memcached: răspunsurile la cererile către bazele de date și serverele API, diferite steaguri, blocaje pentru sincronizarea execuției codului și multe altele. Într-o astfel de situație, defecțiunea memcached devine fatală pentru funcționarea aplicației. În plus, pierderea cache-ului duce la consecințe grave: SGBD-ul începe să crape, serviciile API – blochează cererile etc. Stabilizarea situației poate dura zeci de minute, iar în acest timp serviciul va întâmpina întârzieri mari sau va deveni complet inaccesibil.

A trebuit să asigurăm posibilitatea de scalare orizontală a aplicației cu costuri reduse, adică cu modificări minime ale codului sursă și păstrând în totalitate funcționalitatea. Să facem cache-ul nu doar rezistent la defecțiuni, ci și să încercăm să minimizăm pierderile de date din acesta.

Ce nu este în regulă cu însuși memcached?

De fapt, extensia memcached pentru PHP „din cutie” suportă stocarea distribuită a datelor și sesiunilor. Mecanismul de hashing consistent al cheilor permite plasarea uniformă a datelor pe mai multe servere, adresând în mod specific fiecare cheie concretă unui anumit server din grup, iar instrumentele integrate de failover asigură o disponibilitate ridicată a serviciului de caching (dar, din păcate, nu a datelor).

Stocarea sesiunilor stă puțin mai bine: se poate configura memcached.sess_number_of_replicas, astfel încât datele să fie salvate imediat pe mai multe servere, iar în cazul căderii unui exemplarul memcached, datele vor fi furnizate din alte surse. Totuși, dacă serverul revine în funcțiune fără date (cum se întâmplă de obicei după repornire), o parte din chei va fi redistribuită în favoarea lui. Practic, acest lucru va însemna pierdere de date ale sesiunii, deoarece nu există posibilitatea de a accesa o altă replică în cazul unui eșec.

Instrumentele standard ale bibliotecii sunt îndreptate, în principal, către scalarea orizontală: ele permit extinderea cache-ului la dimensiuni uriașe și asigurarea accesului la acesta din codul plasat pe diferite servere. Totuși, în situația noastră, volumul datelor stocate nu depășește câteva gigabytes, iar performanța unui sau două noduri este suficientă. Prin urmare, din ceea ce era util, instrumentele standard ar fi putut doar să asigure disponibilitatea memcached menținând cel puțin un exemplat al cache-ului în stare de funcționare. Totuși, nu am reușit să profităm nici de această posibilitate... Aici trebuie să amintim despre vechimea framework-ului utilizat în proiect, din cauza căruia nu am reușit să facem aplicația să funcționeze cu un pool de servere. Să nu uităm nici de pierderile de date ale sesiunilor: de la deconectările masive ale utilizatorilor, clientul părea să aibă spasme.

În ideal, era necesară replicarea înregistrărilor în memcached și ocolirea replicilor în cazul unui eșec sau al unei erori. Implementarea acestei strategii ne-a ajutat mcrouter.

mcrouter

Acesta este un router pentru memcached, dezvoltat de compania Facebook pentru a rezolva problemele sale. Suportă protocolul text memcached, care permite scalarea instalațiilor memcached la dimensiuni extreme. O descriere detaliată a mcrouter poate fi găsită în această anunțare. Pe lângă o funcționalitate vastă , el poate face exact ceea ce ne trebuie:

  • replicarea înregistrărilor;
  • fallback pe alte servere din grup în caz de eroare.

Să trecem la treabă!

Configurarea mcrouter

Voi trece direct la configurare:

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

De ce sunt trei pool-uri? De ce se repetă serverele? Hai să vedem cum funcționează acest sistem.

  • În această configurație, mcrouter selectează calea pe care va fi trimisă cererea în funcție de comanda cererii. Acest lucru îi este indicat de tipul OperationSelectorRoute..
  • Cererea GET ajunge în procesorul RandomRoute, care alege aleatoriu un pool sau un traseu dintre obiectele din array-ul children.Fiecare element al acestui array este, la rândul său, un procesor MissFailoverRoute,care va trece prin fiecare server din pool, până când va obține un răspuns cu date, care va fi returnat clientului.
  • Dacă am folosi un singur pool cu trei servere, toate cererile ar ajunge întâi la prima instanță memcached, iar celelalte ar primi cereri doar în cazul în care nu existau date. Această abordare ar duce la MissFailoverRoute, o încărcare excesivă a primului server din listă, de aceea s-a decis să se genereze trei pool-uri cu adrese în ordine diferită și să fie selecționate aleatoriu.Toate celelalte cereri (adică scrierile) sunt procesate prin
  • AllMajorityRoute. Acest procesor trimite cererile la toate serverele din pool și așteaptă răspunsuri de la cel puțin N/2 + 1 dintre ele. FolosireaAllSyncRoute pentru operațiile de scriere a fost abandonată, deoarece acest metod impune un răspuns pozitiv de la serverele din grup — în caz contrar va returna tuturor SERVER_ERROR. Deși mcrouter va stoca datele în cache-urile disponibile, funcția PHP apelatăva returna o eroare și va genera o notificare. Este mai puțin strict și permite excluderea a până la jumătate din noduri din exploatare fără problemele menționate anterior. Acest procesor trimite cererile la toate serverele din pool și așteaptă răspunsuri de la cel puțin N/2 + 1 dintre ele. Folosirea Principalul dezavantaj al

această scheme este că, dacă nu există date în cache, pentru fiecare cerere de la client vor fi executate de fapt N cereri către memcached — către serverele din pool. Numărul serverelor din pool-uri poate fi redus, de exemplu, la două: sacrificând fiabilitatea stocării, obținem b toate răt cu costuri reduse.mareo viteză mai mare și o sarcină mai mică de la solicitările către cheile absente.

NB: Linkuri utile pentru a studia mcrouter ar putea fi documentația din wiki și problemele proiectului (inclusiv cele închise), care reprezintă o adevărată comoară de diverse configurații.

Compilarea și rularea mcrouter

Aplicația (și memcached) funcționează în Kubernetes — așadar, mcrouter se află și acolo. Pentru compilarea containerului folosim werf, configurația va arăta astfel:

NB: Listările prezentate în articol sunt publicate în depozitul 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: Instalează cerințele
   apt:
     name: [ 'apt-transport-https', 'tzdata', 'locales' ]
     update_cache: yes
 - name: Adaugă cheia APT pentru mcrouter
   apt_key:
     url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
 - name: Adaugă depozitul mcrouter
   apt_repository:
     repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
     filename: mcrouter
     update_cache: yes
 - name: Setează fusul orar
   timezone:
     name: "Europe/Moscow"
 - name: Asigură că există o localitate
   locale_gen:
     name: en_US.UTF-8
     state: present
 install:
 - name: Instalează mcrouter
   apt:
     name: [ 'mcrouter' ]

(werf.yaml)

… și facem schițe Helm-chart. Din lucruri interesante — aici există doar generatorul de configurații pe baza numărului de replici (dacă cineva are o variantă mai concisă și mai elegantă — împărtășiți în comentarii):

{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- /* Umplem array-ul cu două copii ale serverelor: "0 1 2 0 1 2" */ -}}
{{- range until 2 -}}
 {{- range $i, $_ := until $count -}}
 {{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
 {{- end -}}
{{- end -}}
{{- /* Mergând prin array, obținem N secțiuni: "[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)

Rolăm în mediu de testare și verificăm:

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

Căutarea textului erorii nu a dat rezultate, însă prin cererea „mcrouter php” în primele rânduri se număra cea mai veche problemă nerezolvată a proiectului — lipsa suportului pentru protocolul binar memcached.

NB: Protocolul ASCII în memcached este mai lent decât cel binar, iar instrumentele standard de hashing consistent al cheilor funcționează doar cu protocolul binar. Dar aceasta nu creează probleme pentru cazul specific.

E clar: nu mai rămâne decât să treci la protocolul ASCII și totul va funcționa…. Totuși, în acest caz, obiceiul de a căuta răspunsuri în documentația de pe php.net a jucat o farsă. Răspunsul corect nu-l veți găsi acolo… dacă, desigur, nu derulați până la final, unde, în secțiunea „Notele contribuitorilor” va fi răspunsul corect și nejustificat negativat.

Da, denumirea corectă a opțiunii este memcached.sess_binary_protocol. Trebuie să fie dezactivată, după care sesiunile vor începe să funcționeze. Rămâne doar să pui containerul cu mcrouter în podurile cu PHP!

Concluzie

Astfel, printr-o simplă modificare a infrastructurii am reușit să rezolvăm sarcina propusă: problema redundanței memcached a fost rezolvată, fiabilitatea stocării cache-ului a fost crescută. Pe lângă avantajele evidente pentru aplicație, acest lucru a oferit o marjă de manevră în realizarea lucrărilor asupra platformei: când toate componentele au un rezervor, viața administratorului devine mult mai simplă. Da, această metodă are și dezavantajele sale, poate părea o „soluție de compromis”, dar dacă economisește bani, ascunde problema și nu cauzează altele noi – de ce nu?

P.S.

Citiți și în blogul nostru:

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster