Uporaba mcrouterja za vodoravno skaliranje predpomnilnika memcached

Uporaba mcrouterja za vodoravno skaliranje predpomnilnika memcached

Razvoj obremenjenih projektov v katerem koli jeziku zahteva poseben pristop in uporabo posebnih orodij, ko pa gre za aplikacije v PHP, se lahko situacija tako zaostri, da morate razviti npr. lasten aplikacijski strežnik. V tej opombi bomo govorili o znani težavi s shranjevanjem porazdeljenih sej in predpomnjenjem podatkov v predpomnilniku memcached ter o tem, kako smo rešili te težave v enem projektu »ward«.

Junak dogodka je aplikacija PHP, ki temelji na ogrodju symfony 2.3, ki sploh ni vključena v poslovne načrte za posodobitev. Poleg običajnega shranjevanja sej je ta projekt v celoti izkoristil pravilnik "predpomnilnik vsega". v memcached: odgovori na zahteve strežnikom baze podatkov in API, različne zastavice, ključavnice za sinhronizacijo izvajanja kode in še veliko več. V takšni situaciji postane okvara memcached usodna za delovanje aplikacije. Poleg tega izguba predpomnilnika povzroči resne posledice: DBMS začne pokati po šivih, storitve API začnejo prepovedovati zahteve itd. Stabilizacija situacije lahko traja več deset minut, v tem času pa bo storitev strašno počasna ali popolnoma nedostopna.

Morali smo zagotoviti možnost vodoravnega prilagajanja velikosti aplikacije z malo truda, tj. z minimalnimi spremembami izvorne kode in ohranjeno polno funkcionalnostjo. Naj bo predpomnilnik ne samo odporen na napake, ampak tudi poskusite zmanjšati izgubo podatkov iz njega.

Kaj je narobe s samim predpomnilnikom memcached?

Na splošno razširitev memcached za PHP podpira porazdeljene podatke in shranjevanje sej takoj po namestitvi. Mehanizem konsistentnega zgoščevanja ključev vam omogoča enakomerno razporejanje podatkov na več strežnikih, edinstveno naslovitev vsakega posebnega ključa na določen strežnik iz skupine, vgrajena orodja za preklop v primeru napake pa zagotavljajo visoko razpoložljivost storitve predpomnjenja (a na žalost, ni podatkov).

Stvari so nekoliko boljše pri shranjevanju sej: lahko konfigurirate memcached.sess_number_of_replicas, zaradi česar bodo podatki shranjeni na več strežnikih hkrati, v primeru okvare ene memcached instance pa se bodo podatki prenesli iz drugih. Če pa strežnik spet vzpostavi povezavo brez podatkov (kot se običajno zgodi po ponovnem zagonu), bodo nekateri ključi prerazporejeni v njegovo korist. Dejansko bo to pomenilo izguba podatkov seje, saj ni možnosti, da bi v primeru zgrešitve "šel" do druge replike.

Standardna knjižnična orodja so namenjena predvsem vodoravno skaliranje: omogočajo vam, da povečate predpomnilnik do ogromnih velikosti in omogočite dostop do njega iz kode, ki gostuje na različnih strežnikih. Vendar pa v naših razmerah obseg shranjenih podatkov ne presega več gigabajtov, zmogljivost enega ali dveh vozlišč pa je povsem dovolj. V skladu s tem bi lahko bila edina uporabna standardna orodja zagotavljanje razpoložljivosti memcached ob ohranjanju vsaj enega primerka predpomnilnika v delovnem stanju. Vendar pa niti te priložnosti ni bilo mogoče izkoristiti ... Tukaj velja opozoriti na staro ogrodje, uporabljeno v projektu, zaradi česar je bilo nemogoče doseči, da bi aplikacija delovala s skupino strežnikov. Ne pozabimo tudi na izgubo podatkov o sejah: kupcu se je oko zdrznilo zaradi množične odjave uporabnikov.

V idealnem primeru je bilo potrebno podvajanje zapisov v memcached in mimo replik v primeru pomote oz. Pomagal nam je uresničiti to strategijo mcrouter.

mcrouter

To je memcached usmerjevalnik, ki ga je razvil Facebook za reševanje svojih težav. Podpira besedilni protokol memcached, ki omogoča prilagodite namestitve memcached do norih razsežnosti. Podroben opis mcrouterja najdete v to objavo. Med drugim široka funkcionalnost lahko naredi, kar potrebujemo:

  • ponovitev zapisa;
  • če pride do napake, se vrnite na druge strežnike v skupini.

Pojdi na posel!

konfiguracijo mcrouterja

Grem naravnost na konfiguracijo:

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

Zakaj trije bazeni? Zakaj se strežniki ponavljajo? Ugotovimo, kako deluje.

  • V tej konfiguraciji mcrouter izbere pot, kamor bo poslana zahteva na podlagi ukaza zahteve. Fant mu to pove OperationSelectorRoute.
  • Zahteve GET gredo upravljavcu RandomRouteki naključno izbere bazen ali pot med matričnimi objekti children. Vsak element te matrike je nato obravnavalec MissFailoverRoute, ki bo šel skozi vsak strežnik v bazenu, dokler ne prejme odgovora s podatki, ki bodo vrnjeni odjemalcu.
  • Če uporabljamo izključno MissFailoverRoute z naborom treh strežnikov bi potem vse zahteve najprej prispele k prvi memcached instanci, ostale pa bi prejemale zahteve na podlagi ostankov, ko ni podatkov. Takšen pristop bi vodil do prekomerna obremenitev prvega strežnika na seznamu, zato je bilo odločeno ustvariti tri skupine z naslovi v različnih zaporedjih in jih naključno izbrati.
  • Vse druge zahteve (in to je zapis) se obdelajo z uporabo AllMajorityRoute. Ta upravljalnik pošilja zahteve vsem strežnikom v skupini in čaka na odgovore vsaj N/2 + 1 izmed njih. Od uporabe AllSyncRoute za operacije pisanja je bilo treba opustiti, saj ta metoda zahteva pozitiven odgovor Vse strežnikov v skupini - sicer se bo vrnil SERVER_ERROR. Čeprav bo mcrouter dodal podatke v razpoložljive predpomnilnike, klicna funkcija PHP bo vrnil napako in bo ustvaril obvestilo. AllMajorityRoute ni tako strog in dovoljuje, da se do polovica enot umakne iz uporabe brez zgoraj opisanih težav.

Glavna pomanjkljivost Ta shema je, da če v predpomnilniku res ni podatkov, potem bo za vsako zahtevo odjemalca dejansko izvedenih N zahtev za memcached – do vse strežniki v bazenu. Število strežnikov v bazenih lahko na primer zmanjšamo na dva: če žrtvujemo zanesljivost shranjevanja, dobimoоvišja hitrost in manjša obremenitev od zahtev do manjkajočih ključev.

NB: Morda boste našli tudi uporabne povezave za učenje mcrouterja dokumentacijo na wiki и projektna vprašanja (vključno z zaprtimi), ki predstavljajo celotno skladišče različnih konfiguracij.

Gradnja in izvajanje mcrouterja

Naša aplikacija (in sam memcached) teče v Kubernetesu - temu primerno se tam nahaja tudi mcrouter. Za montaža posode uporabljamo werf, za katerega bo konfiguracija videti takole:

NB: Seznami v članku so objavljeni v repozitoriju 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)

... in skiciraj Shema krmila. Zanimivo je, da obstaja le generator konfiguracije, ki temelji na številu replik (če ima kdo bolj jedrnato in elegantno možnost, jo delite v komentarjih):

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

Uvedemo ga v testno okolje in preverimo:

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

Iskanje po besedilu napake ni dalo rezultatov, poizvedba pa "mikrouter php»V ospredju je bil najstarejši nerešen problem projekta – pomanjkanje podpore memcached binarni protokol.

NB: Protokol ASCII v predpomnilniku memcached je počasnejši od binarnega in standardna sredstva doslednega zgoščevanja ključev delujejo samo z binarnim protokolom. Vendar to ne povzroča težav za določen primer.

Trik je v vreči: vse kar morate storiti je, da preklopite na protokol ASCII in vse bo delovalo.... Vendar v tem primeru navada, da odgovore iščemo v dokumentacijo na php.net odigral kruto šalo. Tam ne boste našli pravilnega odgovora ... razen če se seveda pomaknete do konca, kjer v rubriki "Uporabniške opombe" bo zvest in neupravičeno zavrnjen odgovor.

Da, pravilno ime možnosti je memcached.sess_binary_protocol. Mora biti onemogočen, potem pa bodo seje začele delovati. Vse kar ostane je, da vsebnik z mcrouterjem postavimo v pod s PHP!

Zaključek

Tako nam je samo z infrastrukturnimi spremembami uspelo rešiti težavo: odpravljena je bila težava z memcached toleranco napak in povečana je zanesljivost predpomnilniškega shranjevanja. Poleg očitnih prednosti za aplikacijo je to omogočilo manevrski prostor pri delu na platformi: ko imajo vse komponente rezervo, je življenje skrbnika močno poenostavljeno. Da, ta metoda ima tudi svoje slabosti, morda izgleda kot "bergla", ampak če prihrani denar, zakoplje problem in ne povzroči novih - zakaj pa ne?

PS

Preberite tudi na našem blogu:

Vir: www.habr.com

Dodaj komentar