Përdorim mcrouter për shkallëzimin horizontal të memcached

Përdorim mcrouter për shkallëzimin horizontal të memcached

Zhvillimi i projekteve me Ngarkesë të Lartë në çdo gjuhë kërkon një qasje të veçantë dhe përdorimin e mjeteve speciale, por kur flitet për aplikacione në PHP, situata mund të përkeqësohet deri në atë pikë saqë duhet të zhvillohet, për shembull, një server aplikacionesh të vetin. Në këtë shënim do të flitet për dhimbjen e njohur të të gjithëve me ruajtjen e shpërndarë të seancave dhe me caching e të dhënave në memcached dhe se si ne e zgjidhëm këtë probleme në një projekt "nën mbikqyrje".

Fajtor për festimin është një aplikacion në PHP, i bazuar në framework-un symfony 2.3, për të cilin rinovimi nuk është aspak në planet e biznesit. Përveç ruhatjes standarde të seancave, ky projekt përdorte masivisht politiken "cache-ing të gjithçkaje" në memcached: përgjigjet në kërkesat për DB dhe API-serverë, flamurë të ndryshëm, bllokime për sinkronizimin e ekzekutimit të kodit dhe shumë të tjera. Në një situatë të tillë, prishja e memcached bëhet fatale për funksionimin e aplikacionit. Përveç kësaj, humbja e cache-së ka pasoja serioze: DB fillon të çmendet, API-shërbimet ndalojnë kërkesat etj. Stabilizimi i situatës mund të zgjasë dhjetëra minuta, dhe gjatë kësaj kohe shërbimi do të ngadalësohet tërësisht ose do të bëhet plotësisht i paarritshëm.

Na duhej të siguronim mundësinë e shkallëzimit horizontal të aplikacionit lehtësisht, dmth. me ndryshime minimale në kodin burimor dhe ruajtjen e plotë të funksionalitetit. Të bëjmë që cache të jetë jo vetëm rezistent ndaj dështimeve, por edhe të përpiqemi të minimizojmë humbjet e të dhënave nga ai.

Çfarë nuk shkon me vetë memcached?

Në përgjithësi, zgjerimi i memcached për PHP "nga kutia" mbështet ruajtjen e shpërndarë të të dhënave dhe seancave. Mekanizmi i heshit të konsistentë të çelësave lejon vendosjen e barabartë të të dhënave në shumë servera, duke adresuar një çelës të veçantë për çdo server të grupit, dhe mjetet e ndërlidhjes së përcjelljes sigurojnë që shërbimi i caching të jetë me disponueshmëri të lartë (por, fatkeqësisht, jo të dhënat).

Me ruajtjen e seancave, gjërat shkojnë pak më mirë: mund të konfigurohet memcached.sess_number_of_replicas, që do të thotë se të dhënat do të ruajnë menjëherë në disa servera, dhe në rast të dështimit të një instance memcached, të dhënat do të merren nga të tjerët. Sidoqoftë, nëse serveri rikthehet në punë pa të dhëna (siç ndodh zakonisht pas një rivendosjeje), një pjesë e çelësave do të riparishtruar në favor të tij. Në fakt, kjo do të thotë humbje të të dhënave të sesionit, pasi nuk ka mundësi të "shkosh" në një replikë tjetër në rast gabimi.

Mjetet standarde të bibliotekës janë kryesisht të orientuara në shkallëzimin : ato lejojnë të rrisin cache-n në përmasa gjigante dhe të sigurojnë akses ndaj tij nga kodet që janë vendosur në servera të ndryshëm. Sidoqoftë, në situatën tonë, sasia e të dhënave të ruajtura nuk tejkalon disa gigabajt, dhe performanca e një ose dy nyjeve është mjaft e mjaftueshme. Prandaj, mjetet standarde mund të ofrojnë vetëm disponueshmërinë e memcached duke ruajtur të paktën një instancë të caches në gjendje pune. Megjithatë, nuk arritëm ta shfrytëzojmë këtë mundësi... Këtu duhet të kujtojmë për moshën e vjetër të strukturës së përdorur në projekt, që e bëri të pamundur të punojmë me një grup serverash. Po ashtu, mos harro humbjen e të dhënave të sesionit: nga daljet masive të përdoruesve, syni i porositësit u tërhoq.

Idealisht, kërkohej replicimi i regjistrimeve në memcached dhe kalimi përmes replikave në rast gabimi ose problemi. Të realizojmë këtë strategji na ndihmoi mcrouter.

mcrouter

Kjo është një router për memcached, i zhvilluar nga Facebook me qëllim të zgjidhjes së problemeve të saj. Ai mbështet protokollin tekstual memcached, i cili lejon shkallëzimin e instalimeve memcached në përmasa të çmendura. Një përshkrim të detajuar të mcrouter mund të gjendet në këtë njoftim. Përveç funksionaliteteve të tjera të gjera ai mund të bëjë atë që na nevojitet:

  • të replicojë regjistrimin;
  • të bëjë fallback në serverat e tjerë të grupit në rast të një problemi.

Le të fillojmë!

Konfigurimi i mcrouter

Do të kaloj menjëherë te konfigurimi:

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

Pse janë tre grupe? Pse përsëriten serverët? Le të shohim si funksionon kjo.

  • Në këtë konfigurim, mcrouter zgjedh rrugën përmes të cilës do të dërgohet kërkesa në përputhje me komandën e kërkesës. Këtë e tregon tipi OperationSelectorRoute.
  • Kërkesat GET kalojnë në procesorin RandomRoute, i cili zgjedh rastësisht një grup ose rrugë nga objektet e vargut children. Çdo element i këtij vargu, nga ana e tij, është një procesor MissFailoverRoute, i cili do të kalojë përmes çdo serveri në grup, derisa të marrë një përgjigje me të dhëna, e cila do t'i kthehet klientit.
  • Nëse do të përdornim ekskluzivisht MissFailoverRoute me një grup prej tre serverësh, të gjitha kërkesat do të shkonin fillimisht në instancën e parë të memcached, kurse pjesa tjetër do të merrte kërkesat sipas mbetjeve, kur të dhënat nuk ishin të pranishme. Ky qasje do të sillte ngarkesë të tepërt në serverin e parë në listë, prandaj u vendos të krijoheshin tre grupe me adresa në rend të ndryshëm dhe t'i zgjidheshin ato rastësisht.
  • Të gjitha kërkesat e tjera (kjo është regjistrimi) procesohen me anë të AllMajorityRoute. Ky procesor dërgon kërkesat në të gjitha serverët e grupit dhe pret përgjigje, nga të paktën N/2 + 1 prej tyre. Kemi hequr dorë nga përdorimi i AllSyncRoute për operacionet e regjistrimit, pasi ky metod kërkon përgjigje pozitive nga të gjitha serverët e grupit — në rast se kjo nuk ndodh, ai do të kthejë SERVER_ERROR. Edhe pse në këtë rast mcrouter do të ruajë të dhënat në caches të aksesueshme, funksioni i thirrjes PHP do të kthejë një gabim dhe do të gjenerojë një njoftim. AllMajorityRoute nuk është kaq i rreptë dhe lejon që të shfaqen deri në gjysmën e nyjave nga përdorimi pa problemet e mësipërme.

Disavantazhi kryesor i këtij skeme është se nëse të dhënat në cache nuk ekzistojnë me të vërtetë, për çdo kërkesë nga klienti, në fakt do të realizohen N kërkesa ndaj memcached — për me gjithçka serverët në grup. Mund të reduktohet numri i serverëve në grupe, për shembull, deri në dy: duke sakrifikuar besueshmërinë e ruajtjes, do të fitojmë boshpejtësi më të madhe dhe ngarkesë më të vogël nga kërkesat për çelësa të papërfshirë.

NB: Lidhjet e dobishme për studimin e mcrouter gjithashtu mund të jenë dokumentacioni në wiki dhe problemet e projektit (përfshirë ato të mbyllura), që përfaqësojnë një thesar të madh të konfiguracioneve të ndryshme.

Kompilimi dhe nisja e mcrouter

Aplikacioni (dhe vetë memcached) punon në Kubernetes — përkatësisht, atje është edhe mcrouter. Për kompilimin e kontejnerit ne përdorim werf, konfigurimi për të cilin do të duket si më poshtë:

NB: Listimet e paraqitura në artikull janë publikuar në repositorin 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: Instaloni kërkesat e domosdoshme
   apt:
     name: [ 'apt-transport-https', 'tzdata', 'locales' ]
     update_cache: yes
 - name: Shtoni çelësin APT për mcrouter
   apt_key:
     url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
 - name: Shtoni repositorin mcrouter
   apt_repository:
     repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
     filename: mcrouter
     update_cache: yes
 - name: Caktoni kohën
   timezone:
     name: "Europe/Moscow"
 - name: Sigurohuni që një lokal të ekzistojë
   locale_gen:
     name: en_US.UTF-8
     state: present
 install:
 - name: Instaloni mcrouter
   apt:
     name: [ 'mcrouter' ]

(werf.yaml)

… dhe po përgatitemi Helm-chart. Gjë interesante — këtu është vetëm gjeneratori i konfiguracionit në varësi të numrit të replikave (nëse dikush ka ndonjë variant më të shkurtër dhe më elegant — ndani në komentet):

{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- 
/* Shtojmë një array me dy kopje të serverëve: "0 1 2 0 1 2" */ -}}
{{- range until 2 -}}
 {{- range $i, $_ := until $count -}}
 {{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
 {{- end -}}
{{- end -}}
{{- 
/* Duke u lëvizur në array, marrim N grupe: "[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)

Ne nxjerrim në ambient testimi dhe kontrollojmë:

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

Kërkimi për tekstin e gabimit nuk dha rezultat, megjithatë në kërkimin "mcrouter php" në radhët e para u shfaq një problem i vjetër i hapur i projektit — mungesa e mbështetjes për protokollin binar të memcached.

NBProtokolli ASCII në memcached është më i ngadalshëm se ai binar, dhe gjithashtu mjetet standarde për heshit të koherencës të çelësave punojnë vetëm me protokollin binar. Por kjo nuk krijon probleme për rastin specifik.

Çështja është e zgjidhur: duhet thjesht të kaloni në protokollin ASCII dhe gjithçka do të funksionojë…. Megjithatë, në këtë rast, zakoni për të kërkuar përgjigje në dokumentacionin në php.net luajti një shaka të keqe. Nuk do të gjeni një përgjigje të saktë atje… nëse, sigurisht, nuk shfletoni deri në fund, ku në seksionin «Shënimet e kontribuara nga përdoruesit» do të jetë një përgjigje e saktë dhe e nënvlerësuar pa të drejtë.

Po, emri i saktë i opsionit është memcached.sess_binary_protocol. Duhet ta çaktivizoni atë, pas së cilës sesionet do të fillojnë të funksionojnë. Tani mbetet vetëm të vendosni kontejnerin me mcrouter në pod me PHP!

Përfundim

Në këtë mënyrë, përmes ndryshimeve vetëm në infrastrukturë arritëm të zgjidhnim detyrën e vendosur: çështja me qëndrueshmërinë e memcached u zgjidh, dhe besueshmëria e ruajtjes së caches u rrit. Përveç përfitimeve të dukshme për aplikacionin, kjo ofroi hapësirë për manovrim gjatë punës mbi platformën: kur të gjitha komponentët kanë rezervë, jeta e administratorit bëhet shumë më e lehtë. Po, ky metodë ka edhe disavantazhet e saj, mund të duket si një "përplasje", por nëse kursen para, fsheh problemin dhe nuk shkakton të reja — përse jo?

P.S.

Lexoni gjithashtu në blogun tonë:

Burimi: habr.com

Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS 🔥 Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS | ProHoster