
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, . 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
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ë . Përveç 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 vargutchildren. Çdo element i këtij vargu, nga ana e tij, është një procesorMissFailoverRoute, 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
MissFailoverRouteme 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 iAllSyncRoutepë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.AllMajorityRoutenuk ë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ë dhe (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 , konfigurimi për të cilin do të duket si më poshtë:
NB: Listimet e paraqitura në artikull janë publikuar në repositorin .
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' ]()
… 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 }}
}
}
}
}()
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 "" në radhët e para u shfaq një problem i vjetër i hapur i projektit — 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ë 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 .
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ë:
- «Praktika me dapp» (me rastin e symfony-demo): dhe ;
- «».
Burimi: habr.com
