Folosind mcrouter pentru a scala memcached pe orizontală

Folosind mcrouter pentru a scala memcached pe orizontală

Dezvoltarea de proiecte cu sarcină mare în orice limbă necesită o abordare specială și utilizarea unor instrumente speciale, dar când vine vorba de aplicații în PHP, situația poate deveni atât de agravată încât trebuie să dezvoltați, de exemplu, propriul server de aplicații. În această notă vom vorbi despre durerea familiară cu stocarea sesiunii distribuite și memorarea în cache a datelor în memcached și despre cum am rezolvat aceste probleme într-un singur proiect „secție”.

Eroul ocaziei este o aplicație PHP bazată pe framework-ul symfony 2.3, care nu este deloc inclusă în planurile de afaceri de actualizat. Pe lângă stocarea de sesiune destul de standard, acest proiect a folosit pe deplin politica „cache totul”. în memcached: răspunsuri la cererile către serverele de bază de date și API, diferite steaguri, blocări pentru sincronizarea execuției codului și multe altele. Într-o astfel de situație, o defecțiune a memcached-ului devine fatală pentru funcționarea aplicației. În plus, pierderea cache-ului duce la consecințe grave: DBMS-ul începe să explodeze, serviciile API încep să interzică cererile etc. Stabilizarea situației poate dura zeci de minute, iar în acest timp serviciul va fi teribil de lent sau complet indisponibil.

Trebuia să oferim capacitatea de a scala orizontal aplicația cu puțin efort, adică cu modificări minime ale codului sursă și funcționalitatea completă păstrată. Faceți cache-ul nu numai să fie rezistent la eșecuri, ci și încercați să minimizați pierderile de date din acesta.

Ce este în neregulă cu memcache-ul în sine?

În general, extensia memcached pentru PHP acceptă date distribuite și stocare de sesiune din cutie. Mecanismul de hashing coerent al cheilor vă permite să plasați în mod uniform date pe mai multe servere, adresând în mod unic fiecare cheie specifică unui anumit server din grup, iar instrumentele de failover încorporate asigură disponibilitatea ridicată a serviciului de stocare în cache (dar, din păcate, nu există date).

Lucrurile sunt puțin mai bune cu stocarea sesiunii: puteți configura memcached.sess_number_of_replicas, în urma căreia datele vor fi stocate pe mai multe servere simultan, iar în cazul eșecului unei instanțe memcache, datele vor fi transferate de la altele. Cu toate acestea, dacă serverul revine online fără date (cum se întâmplă de obicei după o repornire), unele dintre chei vor fi redistribuite în favoarea sa. De fapt asta va însemna pierderea datelor de sesiune, deoarece nu există nicio modalitate de a „mergi” la o altă replică în cazul unei rateuri.

Instrumentele standard de bibliotecă sunt destinate în principal orizontală scalare: vă permit să creșteți memoria cache la dimensiuni gigantice și să oferiți acces la acesta din codul găzduit pe diferite servere. Cu toate acestea, în situația noastră, volumul de date stocate nu depășește câțiva gigaocteți, iar performanța unuia sau a două noduri este destul de suficientă. În consecință, singurele instrumente standard utile ar putea fi asigurarea disponibilității memcached-ului menținând în același timp cel puțin o instanță cache în stare de funcționare. Totuși, nici măcar această oportunitate nu s-a putut profita... Aici merită să reamintim vechimea cadrului folosit în proiect, motiv pentru care a fost imposibil ca aplicația să funcționeze cu un pool de servere. De asemenea, să nu uităm de pierderea datelor de sesiune: ochiul clientului s-a trezit de la deconectarea masivă a utilizatorilor.

Ideal ar fi fost necesar replicarea înregistrărilor în replici memcache și ocolitoare în cazul unei greşeli sau greşeli. Ne-a ajutat să implementăm această strategie mcrouter.

mcrouter

Acesta este un router memcached dezvoltat de Facebook pentru a-și rezolva problemele. Acceptă protocolul text memcached, care permite instalații cu memcache la scară la proporții nebunești. O descriere detaliată a mcrouter poate fi găsită în acest anunt. Printre alte lucruri funcționalitate largă poate face ceea ce avem nevoie:

  • înregistrarea replicată;
  • faceți alternativă la alte servere din grup dacă apare o eroare.

Sa trecem la afaceri!

configurație mcrouter

Voi merge direct la configurație:

{
 "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 trei piscine? De ce se repetă serverele? Să ne dăm seama cum funcționează.

  • În această configurație, mcrouter selectează calea către care va fi trimisă cererea pe baza comenzii de solicitare. Tipul îi spune asta OperationSelectorRoute.
  • Solicitările GET ajung la handler RandomRoutecare selectează aleatoriu un grup sau o rută printre obiectele matrice children. Fiecare element al acestui tablou este la rândul său un handler MissFailoverRoute, care va trece prin fiecare server din pool până când va primi un răspuns cu date, care vor fi returnate clientului.
  • Dacă am folosi exclusiv MissFailoverRoute cu un grup de trei servere, atunci toate cererile vor veni mai întâi la prima instanță memcache, iar restul vor primi cereri reziduale atunci când nu există date. O astfel de abordare ar duce la încărcare excesivă pe primul server din listă, așa că s-a decis să se genereze trei pool-uri cu adrese în secvențe diferite și să le selecteze aleatoriu.
  • Toate celelalte solicitări (și aceasta este o înregistrare) sunt procesate folosind AllMajorityRoute. Acest handler trimite cereri către toate serverele din pool și așteaptă răspunsuri de la cel puțin N/2 + 1 dintre ele. De la utilizare AllSyncRoute pentru operațiunile de scriere a trebuit să fie abandonate, deoarece această metodă necesită un răspuns pozitiv din partea toate servere din grup - altfel va reveni SERVER_ERROR. Deși mcrouter va adăuga datele în cache-urile disponibile, funcția PHP de apelare va returna o eroare și va genera notificare. AllMajorityRoute nu este atât de strict și permite ca până la jumătate din unități să fie scoase din funcțiune fără problemele descrise mai sus.

Principalul dezavantaj Această schemă este că, dacă într-adevăr nu există date în cache, atunci pentru fiecare cerere de la client N cereri către memcached vor fi de fapt executate - pentru toate servere din bazin. Putem reduce numărul de servere din pool-uri, de exemplu, la două: sacrificând fiabilitatea stocării, obținemоviteză mai mare și încărcare mai mică de la solicitări la cheile lipsă.

NB: De asemenea, puteți găsi link-uri utile pentru a învăța mcrouter documentație pe wiki и probleme ale proiectului (inclusiv cele închise), reprezentând un întreg depozit de diverse configurații.

Construirea și rularea unui mcrouter

Aplicația noastră (și memcache-ul în sine) rulează în Kubernetes - în consecință, mcrouter se află și acolo. Pentru ansamblu container folosim werf, configurația pentru care va arăta astfel:

NB: Listările date în articol sunt publicate în depozit 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: 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)

... și schițați-l Diagrama de cârmă. Lucrul interesant este că există doar un generator de configurații bazat pe numărul de replici (dacă cineva are o opțiune mai laconică și mai elegantă, împărtășește-o în comentarii):

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

Îl lansăm în mediul 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 niciun rezultat, dar pentru interogarea „mcrouter php„În prim plan a fost cea mai veche problemă nerezolvată a proiectului - lipsă de suport protocol binar memcached.

NB: Protocolul ASCII din memcached este mai lent decât cel binar, iar mijloacele standard de hashing coerent al cheilor funcționează numai cu protocolul binar. Dar acest lucru nu creează probleme pentru un caz anume.

Trucul este în geantă: tot ce trebuie să faci este să treci la protocolul ASCII și totul va funcționa.... Cu toate acestea, în acest caz, obiceiul de a căuta răspunsuri în documentație pe php.net a făcut o glumă crudă. Nu vei găsi răspunsul corect acolo... decât dacă, desigur, derulezi până la sfârșit, unde în secțiune „Note contribuite de utilizator” va fi credincios şi răspuns negativ votat pe nedrept.

Da, numele corect al opțiunii este memcached.sess_binary_protocol. Trebuie dezactivat, după care sesiunile vor începe să funcționeze. Tot ce rămâne este să puneți containerul cu mcrouter într-un pod cu PHP!

Concluzie

Astfel, doar cu modificări de infrastructură am reușit să rezolvăm problema: problema cu toleranța la erori memcached a fost rezolvată, iar fiabilitatea stocării cache a fost crescută. Pe lângă avantajele evidente pentru aplicație, acest lucru a dat spațiu de manevră atunci când lucrați pe platformă: atunci când toate componentele au o rezervă, viața administratorului este mult simplificată. Da, această metodă are și dezavantajele ei, poate arăta ca o „cârjă”, dar dacă economisește bani, îngroapă problema și nu provoacă altele noi - de ce nu?

PS

Citește și pe blogul nostru:

Sursa: www.habr.com

Adauga un comentariu