Brug af mcrouter til at skalere memcached vandret

Brug af mcrouter til at skalere memcached vandret

At udvikle højbelastningsprojekter på ethvert sprog kræver en særlig tilgang og brug af specielle værktøjer, men når det kommer til applikationer i PHP, kan situationen blive så forværret, at man skal udvikle f.eks. egen applikationsserver. I dette notat vil vi tale om den velkendte smerte med distribueret sessionslagring og datacaching i memcached, og hvordan vi løste disse problemer i et "afdelings"-projekt.

Anledningens helt er en PHP-applikation baseret på symfony 2.3-rammeværket, som slet ikke er inkluderet i forretningsplanerne for at opdatere. Ud over ganske standard sessionsopbevaring gjorde dette projekt fuld brug af "Caching af alt" politik i memcached: svar på anmodninger til databasen og API-servere, forskellige flag, låse til synkronisering af kodeudførelse og meget mere. I en sådan situation bliver et sammenbrud af memcached fatalt for applikationens drift. Derudover fører cachetab til alvorlige konsekvenser: DBMS begynder at briste i sømmene, API-tjenester begynder at forbyde anmodninger osv. At stabilisere situationen kan tage snesevis af minutter, og i løbet af denne tid vil tjenesten være frygtelig langsom eller fuldstændig utilgængelig.

Vi var nødt til at yde evnen til at skalere applikationen horisontalt med en lille indsats, dvs. med minimale ændringer af kildekoden og fuld funktionalitet bevaret. Gør cachen ikke kun modstandsdygtig over for fejl, men prøv også at minimere tab af data fra den.

Hvad er der galt med selve memcached?

Generelt understøtter memcached-udvidelsen til PHP distribueret data og sessionslagring ud af boksen. Mekanismen for konsekvent nøglehashing giver dig mulighed for jævnt at placere data på mange servere, unikt adressere hver specifik nøgle til en specifik server fra gruppen, og indbyggede failover-værktøjer sikrer høj tilgængelighed af caching-tjenesten (men desværre, ingen data).

Tingene er lidt bedre med sessionslagring: du kan konfigurere memcached.sess_number_of_replicas, som et resultat af, at dataene vil blive gemt på flere servere på én gang, og i tilfælde af en fejl i en memcached instans, vil dataene blive overført fra andre. Men hvis serveren kommer online igen uden data (som det normalt sker efter en genstart), vil nogle af nøglerne blive omfordelt til dens fordel. Faktisk vil dette betyde tab af sessionsdata, da der ikke er nogen måde at "gå" til en anden replika i tilfælde af en miss.

Standard biblioteksværktøjer er hovedsageligt rettet mod vandret skalering: De giver dig mulighed for at øge cachen til gigantiske størrelser og give adgang til den fra kode hostet på forskellige servere. Men i vores situation overstiger mængden af ​​lagrede data ikke flere gigabyte, og ydeevnen af ​​en eller to noder er ganske nok. Følgelig kunne de eneste nyttige standardværktøjer være at sikre tilgængeligheden af ​​memcached, mens mindst én cache-forekomst bevares i fungerende tilstand. Det var dog ikke muligt at udnytte selv denne mulighed... Her er det værd at minde om oldtiden af ​​de rammer, der blev brugt i projektet, hvorfor det var umuligt at få applikationen til at fungere med en pulje af servere. Lad os heller ikke glemme tabet af sessionsdata: Kundens øje rykkede fra det massive logout af brugere.

Ideelt set var det påkrævet replikering af poster i memcached og forbigående replikaer i tilfælde af fejl eller fejl. Hjælp os med at implementere denne strategi mcrouter.

mcrouter

Dette er en memcached router udviklet af Facebook til at løse sine problemer. Den understøtter den memcached tekstprotokol, som tillader skala memcachede installationer til vanvittige proportioner. En detaljeret beskrivelse af mcrouter kan findes i denne meddelelse. Blandt andet bred funktionalitet den kan gøre hvad vi har brug for:

  • replikere rekord;
  • gøre fallback til andre servere i gruppen, hvis der opstår en fejl.

Komme til sagen!

mcrouter konfiguration

Jeg går direkte 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 pools? Hvorfor gentages servere? Lad os finde ud af, hvordan det virker.

  • I denne konfiguration vælger mcrouter stien, som anmodningen vil blive sendt til, baseret på kommandoen forespørgsel. Fyren fortæller ham dette OperationSelectorRoute.
  • GET-anmodninger går til behandleren RandomRoutesom tilfældigt vælger en pulje eller rute blandt array-objekter children. Hvert element i dette array er på sin side en handler MissFailoverRoute, som vil gå gennem hver server i puljen, indtil den modtager et svar med data, som vil blive returneret til klienten.
  • Hvis vi udelukkende brugte MissFailoverRoute med en pulje på tre servere, så ville alle anmodninger komme først til den første memcached instans, og resten ville modtage anmodninger på en resterende basis, når der ikke er nogen data. En sådan tilgang ville føre til for stor belastning på den første server på listen, så det blev besluttet at generere tre puljer med adresser i forskellige sekvenser og vælge dem tilfældigt.
  • Alle andre anmodninger (og dette er en registrering) behandles vha AllMajorityRoute. Denne handler sender anmodninger til alle servere i puljen og venter på svar fra mindst N/2 + 1 af dem. Fra brug AllSyncRoute for skriveoperationer måtte opgives, da denne metode kræver et positivt svar fra Alle servere i gruppen - ellers vender den tilbage SERVER_ERROR. Selvom mcrouter vil tilføje data til tilgængelige caches, kalder PHP-funktionen vil returnere en fejl og vil generere besked. AllMajorityRoute er ikke så streng og gør det muligt at tage op til halvdelen af ​​enhederne ud af drift uden de ovenfor beskrevne problemer.

Største ulempe Denne ordning er, at hvis der virkelig ikke er nogen data i cachen, så vil der for hver anmodning fra klienten faktisk blive udført N anmodninger til memcached - til til alle servere i poolen. Vi kan reducere antallet af servere i puljer, for eksempel til to: vi ofrer lagerpålidelighed, får viоhøjere hastighed og mindre belastning fra anmodninger til manglende nøgler.

NB: Du kan også finde nyttige links til at lære mcrouter dokumentation på wiki и projektspørgsmål (inklusive lukkede), der repræsenterer et helt lagerhus af forskellige konfigurationer.

Bygge og køre mcrouter

Vores applikation (og memcached selv) kører i Kubernetes - derfor er mcrouter også placeret der. Til beholdersamling vi bruger werf, vil konfigurationen se sådan ud:

NB: Listerne i artiklen er offentliggjort i depotet 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)

... og skitser det Hjelm diagram. Det interessante er, at der kun er en config-generator baseret på antallet af replikaer (hvis nogen har en mere lakonisk og elegant mulighed, del den i kommentarerne):

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

Vi ruller det ud i testmiljøet og tjekker:

# 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 teksten til fejlen gav ingen resultater, men forespørgslen "mikrouter php"I spidsen var projektets ældste uløste problem - manglende støtte memcached binær protokol.

NB: ASCII-protokollen i memcached er langsommere end den binære, og standardmetoder til konsekvent nøglehashing fungerer kun med den binære protokol. Men det skaber ikke problemer for en konkret sag.

Tricket er i bagagen: alt du skal gøre er at skifte til ASCII-protokollen og alt vil fungere.... Men i dette tilfælde er vanen med at lede efter svar i dokumentation på php.net spillede en grusom joke. Du finder ikke det rigtige svar der ... medmindre du selvfølgelig ruller til slutningen, hvor i afsnittet "Brugerbidragede noter" vil være trofast og uretfærdigt nedstemt svar.

Ja, det korrekte valgnavn er memcached.sess_binary_protocol. Den skal deaktiveres, hvorefter sessionerne begynder at virke. Tilbage er blot at sætte beholderen med mcrouter i en pod med PHP!

Konklusion

Med blot infrastrukturelle ændringer var vi i stand til at løse problemet: problemet med memcached fejltolerance er blevet løst, og pålideligheden af ​​cache-lagring er blevet øget. Ud over de åbenlyse fordele for applikationen gav dette manøvrerum, når man arbejdede på platformen: når alle komponenter har en reserve, forenkles administratorens levetid betydeligt. Ja, denne metode har også sine ulemper, den kan ligne en "krykke", men hvis den sparer penge, begraver problemet og ikke forårsager nye - hvorfor ikke?

PS

Læs også på vores blog:

Kilde: www.habr.com

Tilføj en kommentar