Bruke mcrouter for å skalere memcached horisontalt

Bruke mcrouter for å skalere memcached horisontalt

Å utvikle høybelastningsprosjekter på et hvilket som helst språk krever en spesiell tilnærming og bruk av spesialverktøy, men når det kommer til applikasjoner i PHP, kan situasjonen bli så forverret at du må utvikle f.eks. egen applikasjonsserver. I dette notatet vil vi snakke om den kjente smerten med distribuert øktlagring og databufring i memcached og hvordan vi løste disse problemene i ett "avdelings"-prosjekt.

Anledningens helt er en PHP-applikasjon basert på symfony 2.3-rammeverket, som slett ikke er inkludert i forretningsplanene for oppdatering. I tillegg til ganske standard øktlagring, utnyttet dette prosjektet fullt ut "buffer alt"-politikk i memcached: svar på forespørsler til databasen og API-servere, ulike flagg, låser for synkronisering av kodekjøring og mye mer. I en slik situasjon blir et sammenbrudd av memcached dødelig for driften av applikasjonen. I tillegg fører cache-tap til alvorlige konsekvenser: DBMS begynner å sprekke i sømmene, API-tjenester begynner å forby forespørsler, etc. Å stabilisere situasjonen kan ta titalls minutter, og i løpet av denne tiden vil tjenesten være fryktelig treg eller helt utilgjengelig.

Vi trengte å yte muligheten til å skalere applikasjonen horisontalt med liten innsats, dvs. med minimale endringer i kildekoden og full funksjonalitet bevart. Gjør cachen ikke bare motstandsdyktig mot feil, men prøv også å minimere tap av data fra den.

Hva er galt med memcached selv?

Generelt støtter memcached-utvidelsen for PHP distribuert data og øktlagring ut av esken. Mekanismen for konsekvent nøkkelhashing lar deg plassere data jevnt på mange servere, unikt adressere hver spesifikk nøkkel til en spesifikk server fra gruppen, og innebygde failover-verktøy sikrer høy tilgjengelighet av hurtigbuffertjenesten (men, dessverre, ingen data).

Ting er litt bedre med øktlagring: du kan konfigurere memcached.sess_number_of_replicas, som et resultat av at dataene vil bli lagret på flere servere samtidig, og i tilfelle feil på en memcached forekomst, vil dataene bli overført fra andre. Men hvis serveren kommer tilbake på nett uten data (som vanligvis skjer etter en omstart), vil noen av nøklene bli omfordelt til dens favør. Faktisk vil dette bety tap av øktdata, siden det ikke er noen måte å "gå" til en annen kopi i tilfelle en glipp.

Standard bibliotekverktøy er hovedsakelig rettet mot horisontal skalering: de lar deg øke hurtigbufferen til gigantiske størrelser og gi tilgang til den fra kode som ligger på forskjellige servere. Men i vår situasjon overstiger ikke volumet av lagrede data flere gigabyte, og ytelsen til en eller to noder er ganske nok. Følgelig kan de eneste nyttige standardverktøyene være å sikre tilgjengeligheten av memcached mens du opprettholder minst én cache-forekomst i fungerende tilstand. Det var imidlertid ikke mulig å dra nytte av selv denne muligheten... Her er det verdt å minne om antikken til rammeverket som ble brukt i prosjektet, og det var derfor det var umulig å få applikasjonen til å fungere med en pool av servere. La oss heller ikke glemme tapet av øktdata: Kundens øye rykket etter den enorme utloggingen av brukere.

Ideelt sett var det påkrevd replikering av poster i memcached og forbigående replikaer i tilfelle feil eller feil. Hjalp oss med å implementere denne strategien mcrouter.

mcrouter

Dette er en memcached ruter utviklet av Facebook for å løse problemene. Den støtter den memcached tekstprotokollen, som tillater skala memcached installasjoner til vanvittige proporsjoner. En detaljert beskrivelse av mcrouter finner du i denne kunngjøringen. Blant annet bred funksjonalitet den kan gjøre det vi trenger:

  • replikere posten;
  • gjøre fallback til andre servere i gruppen hvis det oppstår en feil.

Komme til saken!

mcrouter-konfigurasjon

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

Hvorfor tre bassenger? Hvorfor gjentas servere? La oss finne ut hvordan det fungerer.

  • I denne konfigurasjonen velger mcrouter banen som forespørselen skal sendes til basert på forespørselskommandoen. Fyren forteller ham dette OperationSelectorRoute.
  • GET-forespørsler går til behandleren RandomRoutesom tilfeldig velger en pool eller rute blant array-objekter children. Hvert element i denne matrisen er i sin tur en behandler MissFailoverRoute, som vil gå gjennom hver server i bassenget til den mottar et svar med data, som returneres til klienten.
  • Hvis vi utelukkende brukte MissFailoverRoute med en gruppe på tre servere, vil alle forespørsler komme først til den første memcached-forekomsten, og resten vil motta forespørsler på gjenværende basis når det ikke er data. En slik tilnærming vil føre til overdreven belastning på den første serveren på listen, så det ble besluttet å generere tre bassenger med adresser i forskjellige sekvenser og velge dem tilfeldig.
  • Alle andre forespørsler (og dette er en post) behandles vha AllMajorityRoute. Denne behandleren sender forespørsler til alle servere i bassenget og venter på svar fra minst N/2 + 1 av dem. Fra bruk AllSyncRoute for skriveoperasjoner måtte forlates, siden denne metoden krever en positiv respons fra Alle servere i gruppen - ellers kommer den tilbake SERVER_ERROR. Selv om mcrouter vil legge til data til tilgjengelige cacher, kaller PHP-funksjonen vil returnere en feil og vil generere varsel. AllMajorityRoute er ikke så streng og lar opptil halvparten av enhetene tas ut av drift uten problemene beskrevet ovenfor.

Den største ulempen Dette opplegget går ut på at hvis det virkelig ikke er noen data i hurtigbufferen, vil N forespørsler til memcached faktisk bli utført for hver forespørsel fra klienten - til alle servere i bassenget. Vi kan redusere antall servere i bassenger, for eksempel til to: vi ofrer lagringssikkerheten,оhøyere hastighet og mindre belastning fra forespørsler til manglende nøkler.

NB: Du kan også finne nyttige lenker for å lære mcrouter dokumentasjon på wiki и prosjektspørsmål (inkludert lukkede), som representerer et helt lagerhus med forskjellige konfigurasjoner.

Bygge og kjøre mcrouter

Vår applikasjon (og memcached seg selv) kjører i Kubernetes - følgelig er mcrouter også lokalisert der. Til beholdersammenstilling vi bruker werf, vil konfigurasjonen se slik ut:

NB: Listene gitt i artikkelen er publisert 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 skisser det Hjelmdiagram. Det interessante er at det bare er en konfigurasjonsgenerator basert på antall replikaer (hvis noen har et mer lakonisk og elegant alternativ, del det i kommentarfeltet):

{{- $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 den 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øking etter teksten til feilen ga ingen resultater, men for søket "mikrouter php"I forkant var det eldste uløste problemet med prosjektet - mangel på støtte memcached binær protokoll.

NB: ASCII-protokollen i memcached er tregere enn den binære, og standardmetoder for konsistent nøkkelhashing fungerer bare med den binære protokollen. Men dette skaper ikke problemer for en konkret sak.

Trikset er i boks: alt du trenger å gjøre er å bytte til ASCII-protokollen og alt vil fungere.... Men i dette tilfellet, vanen med å lete etter svar i dokumentasjon på php.net spilte en grusom spøk. Du finner ikke det riktige svaret der ... med mindre du selvfølgelig blar til slutten, hvor i delen "Bruker bidro med notater" vil være trofast og urettferdig nedstemt svar.

Ja, det riktige alternativnavnet er memcached.sess_binary_protocol. Den må deaktiveres, hvoretter øktene begynner å fungere. Alt som gjenstår er å sette beholderen med mcrouter i en pod med PHP!

Konklusjon

Dermed, med bare infrastrukturelle endringer, var vi i stand til å løse problemet: problemet med memcached feiltoleranse er løst, og påliteligheten til cache-lagring har blitt økt. I tillegg til de åpenbare fordelene for applikasjonen, ga dette handlingsrom når du arbeider på plattformen: når alle komponenter har en reserve, forenkles administratorens levetid betydelig. Ja, denne metoden har også sine ulemper, den kan se ut som en "krykke", men hvis den sparer penger, begraver problemet og ikke forårsaker nye - hvorfor ikke?

PS

Les også på bloggen vår:

Kilde: www.habr.com

Legg til en kommentar