We use mcrouter for horizontal scaling of memcached

We use mcrouter for horizontal scaling of memcached

Utvikling av høytytende prosjekter på ethvert språk krever en spesiell tilnærming og bruk av spesielle verktøy, men når det gjelder applikasjoner skrevet i PHP, kan situasjonen bli så alvorlig at man må utvikle for eksempel sin egen applikasjonsserver. I denne artikkelen skal vi ta for oss den velkjente smerten ved distribuert lagring av sesjoner og datacaching i memcached, og hvordan vi løste disse problemene i et "misjonerende" prosjekt.

Hovedpersonen er en PHP-applikasjon basert på rammeverket symfony 2.3, som det ikke er planlagt å oppdatere i bedriftens forretningsstrategi. I tillegg til den ganske standard lagringen av sesjoner, benyttet dette prosjektet i stor grad strategien med «caching av alt» i memcached: svar på forespørselen til databaser og API-servere, forskjellige flagg, låser for synkronisering av kodekjøring og mye mer. I en slik situasjon blir en feil i memcached katastrofal for applikasjonens drift. I tillegg vil tap av cache føre til alvorlige konsekvenser: DBMS begynner å knake i sammenføyningene, API-tjenester blokerer forespørslene osv. Å stabilisere situasjonen kan ta mange minutter, og i løpet av denne tiden vil tjenesten oppleve betydelig treghet eller til og med bli utilgjengelig.

Vi måtte sikre muligheten for horisontal skalering av applikasjonen med minimale inngrep, det vil si med minimale endringer i kildekoden og full bevaring av funksjonaliteten. Å gjøre cachen ikke bare motstandsdyktig mot feil, men også å forsøke å minimere tap av data fra den.

Hva er ikke bra med selve memcached?

Generelt støtter memcached-utvidelsen for PHP «out of the box» distribusjon av lagring av data og sesjoner. Mekanismen for konsistent hashing av nøkler gjør det mulig å jevnt fordele data på mange servere, ved entydig å tilordne hver spesifikke nøkkel til en bestemt server i gruppen, mens de innebygde feilsikringselementene gir høy tilgjengelighet av cache-tjenesten (men, dessverre, ikke dataene).

Det er litt bedre med lagring av sesjoner: man kan konfigurere memcached.sess_number_of_replicas, noe som gjør at dataene vil bli lagret på flere servere samtidig, og i tilfelle en instans av memcached feiler, vil dataene bli hentet fra andre. Men hvis serveren kommer tilbake online uten data (som vanlig etter en omstart), vil deler av nøklene bli omfordelt i dens favør. Dette vil i praksis bety tap av sesjonsdata, siden det ikke er mulig å "gå til" en annen replika i tilfelle en feil.

Standardverktøyene i biblioteket er hovedsakelig rettet mot horisontal skalerbarhet: de lar deg øke cache-størrelsen til enorme dimensjoner og gi tilgang til den fra kode som kjører på forskjellige servere. Men i vår situasjon overskrider ikke mengden lagrede data flere gigabyte, og ytelsen til en eller to noder er mer enn tilstrekkelig. Følgelig kunne de nyttige standardverktøyene bare sikre tilgjengeligheten av memcached ved å opprettholde minst én fungerende cache-instans. Imidlertid klarte vi ikke engang å dra nytte av denne muligheten ... Her må det nevnes at rammeverket som ble brukt i prosjektet er gammelt, noe som gjorde det umulig å få applikasjonen til å fungere med en serverpool. La oss heller ikke glemme tapet av sesjonsdata: den hyppige utloggingen av brukere fikk kunden til å reagere.

Ideelt sett var det nødvendig med replikering av poster i memcached og omgåelse av replikas i tilfelle en feil eller krasj. Denne strategien ble hjulpet av mcrouter.

mcrouter

Dette er en ruter for memcached utviklet av Facebook for å løse deres problemer. Den støtter det tekstlige memcached-protokollet, som gjør det mulig å skalerer memcached-installasjoner til enorme dimensjoner. En detaljert beskrivelse av mcrouter kan man finne i denne kunngjøringen. I tillegg til annen bred funksjonalitet kan den gjøre det vi trenger:

  • replikere poster;
  • falle tilbake til andre servere i gruppen i tilfelle en feil skjer.

La oss komme i gang!

Konfigurasjon av mcrouter

Jeg går rett til konfigurasjonen:

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

Kvifor tre bassengar? Kvifor gjentekast serverane? La oss sjå korleis dette fungerer.

  • I denne konfigurasjonen velger mcrouter vegen som forespørselen skal sendast basert på forespørselstypen. Dette fortel han ved hjelp av typen OperationSelectorRoute.
  • GET-forespørslene går til håndteraren RandomRoute, som tilfeldig vel eit basseng eller ein rute blant objekta i matrisen children. Kvar enkelt element i denne matrisen er igjen ein håndterar MissFailoverRoute, som går gjennom kvar server i bassenget til han får eit svar med data, som vil bli sendt tilbake til klienten.
  • Dersom vi berre hadde brukt MissFailoverRoute med eit basseng av tre serverar, ville alle forespørslene først gått til den første instansen av memcached, og dei andre ville fått forespørslene etter restprinsippet når data ikkje var tilgjengeleg. Denne tilnærminga ville ført til overbelastning av den første serveren i lista, så det vart bestemt å generere tre basseng med adresse i ulik sekvens og velje dei tilfeldig.
  • Alle andre forespørslene (som er skrivingsforespørslene) blir handsama ved hjelp av AllMajorityRoute. Denne håndteraren sender forespørslene til alle serverane i bassenget og ventar på svar, helst frå N/2 + 1 av dei. Bruken av AllSyncRoute for skrivingsoperasjonar måtte droppast, då denne metoden krev eit positivt svar frå alle serverane i gruppa - elles vil han returnere SERVER_ERROR. Sjølv om mcrouter samlar dataene i tilgjengelege cachear, vil den kallande PHP-funksjonen returnere ein feil og generere ei merknad. AllMajorityRoute er ikkje så streng og tillèt å ta ut opp til halve noder frå drift utan ovanfor beskrevne problem.

Hovudulempa med denne løysinga er at dersom data i cachen faktisk ikkje er tilgjengeleg, vil det bli utført N forespørselar til memcached for kvar forespørsel frå klienten - til alle serverane i bassenget. Ein kan redusere talet på serverar i bassenga til to: ved å ofre påliteligheita av lagringa får vi eitomhøyere hastighet og lavere belastning fra forespørslene til manglende nøkler.

NB: Nyttige lenker for å studere mcrouter kan også vise seg å være dokumentasjonen i wiki og prosjektets issues (inkludert lukkede), som representerer en hel skatt av ulike konfigurasjoner.

Bygging og oppstart av mcrouter

Applikasjonen (og selve memcached) kjører hos oss i Kubernetes — tilsvarende finner vi mcrouter der. For bygging av containeren bruker vi werf, konfigurasjonen vil se omtrent slik ut:

NB: Listene som er presentert i artikkelen, er publisert i repository 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 vi skisserer Helm-diagram. Det interessante her — dette er bare config-generatoren basert på antall replikaer (hvis noen har et mer kortfattet og elegant alternativ — del gjerne i kommentarfeltet):

{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- \/\* Fyller opp arrays med to kopier av serverne: "0 1 2 0 1 2" *\/ -}}
{{- range until 2 -}}
 {{- range $i, $_ := until $count -}}
 {{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
 {{- end -}}
{{- end -}}
{{- \/\* Ved å flytte gjennom arrayet, får vi N skiver: "[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)

Vi ruller ut i testmiljøet og sjekker:

# 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øket etter teksten i feilmeldingen ga ingen resultater, men ved søk etter «mcrouter php» i de første radene ble den eldste åpne saken i prosjektet — manglende støtte for memcached binærprotokoll.

NB: ASCII-protokollen i memcached er tregere enn den binære, og de innebygde verktøyene for konsistent hashing av nøkler fungerer kun med det binære protokollen. Men dette skaper ingen problemer for den spesifikke saken.

Saken er klar: det gjenstår bare å bytte til ASCII-protokollen, og alt vil fungere... Imidlertid har vanen med å lete etter svar i dokumentasjonen på php.net spilt et slemt spill. Du finner ikke det rette svaret der... med mindre du blar ned til slutten, hvor i seksjonen «Brukerbidragte notater» vil det være et korrekt og ufortjent nedstemt svar..

Ja, det riktige navnet på alternativet er memcached.sess_binary_protocol. Du må deaktivere det, og deretter vil øktene begynne å fungere. Det gjenstår bare å plassere containeren med mcrouter i pod'en med PHP!

Konklusjon

Slik klarte vi å løse den stille oppgaven bare med infrastrukturelle endringer: spørsmålet om feiltoleranse for memcached er løst, påliteligheten til cachelagringen er økt. I tillegg til de åpenbare fordelene for applikasjonen, ga dette rom for manøvrering under arbeidet med plattformen: når alle komponenter har en reserve, blir livet til administratoren mye enklere. Ja, denne metoden har også sine ulemper, kan se ut som en 'lapp', men hvis den sparer penger, begraver problemet og ikke skaper nye — hvorfor ikke?

P.S.

Also read in our blog:

Kilde: habr.com

Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere 🔥 Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster