
Augstas slodzes projektu izstrāde, izmantojot jebkuru valodu, prasa īpašu pieeju un speciālu rīku lietošanu. Taču, runājot par PHP lietojumprogrammām, situācija var kļūt tik sarežģīta, ka ir nepieciešams izstrādāt, piemēram, . Šajā rakstā tiks apspriesta visiem zināmā problēma ar sesiju izkliedētu glabāšanu un datu kešošanu memcached, kā arī tas, kā mēs risinājām šīs problēmas vienā no mūsu 'pacientu' projektiem.
Svētku varonis ir PHP lietojumprogramma, kas balstās uz Symfony 2.3, kuras atjaunināšana biznesa plānos nav iekļauta. Papildus standarta sesiju glabāšanai šajā projektā aktīvi tika izmantota kēšošanas politika memcached: atbildes uz pieprasījumiem uz datu bāzēm un API serveriem, dažādi karogi, kodu izpildes sinhronizācijas bloķēšanas un daudz kas cits. Šādā situācijā memcached kļūme kļūst kritiska lietojumprogrammas darbībai. Turklāt, keša zudums rada nopietnas sekas: datu bāze sāk sabrukt, API pakalpojumi – bloķē pieprasījumus utt. Situācijas stabilizācija var aizņemt desmitiem minūšu, un šajā laikā pakalpojums darbosies ļoti lēni vai kļūs pilnīgi nepieejams.
Mums bija nepieciešams nodrošināt iespēju horizontāli paplašināt lietojumprogrammu ar minimālām izmaiņām sākotnējā kodā un pilnīgu funkcionalitātes saglabāšanu. Jāizveido kešatmiņa, kas ir ne tikai izturīga pret kļūdām, bet arī jāmēģina minimizēt datu zudumus no tās., t.i. ar minimāliem izmaiņām avota kodā un pilnīgu funkcionalitātes saglabāšanu. Izveidot kešu ne tikai izturīgu pret kļūdām, bet arī noteikti samazināt datu zudumu no tā.
Kas nav kārtībā ar pašu memcached?
Patiesībā memcached paplašinājums PHP 'no kastes' atbalsta izkliedētu datu un sesiju glabāšanu. Atbilstošā taustiņu konsekventa hashēšana ļauj vienmērīgi izvietot datus uz vairākiem serveriem, skaidri adresējot katru konkrēto taustiņu uz noteiktu serveri grupā, bet iebūvētie failover rīki nodrošina augstu kešošanas pakalpojuma pieejamību (bet, diemžēl, ne datus.).
Sesiju glabāšana ir nedaudz labāka: varat iestatīt memcached.sess_number_of_replicas, kā rezultātā dati tiks saglabāti uz vairākām serveriem, un viena memcached instances atteikuma gadījumā dati tiks nodoti no citām. Tomēr, ja servers atgriežas darbībā bez datiem (kā parasti notiek pēc restartēšanas), daļa atslēgu tiks pārdalīta par labu tam. Faktiski tas nozīmēs sesijas datu zudumu, jo nav iespēju "doties" uz citu replikāciju gadījumā, ja rodas kļūda.
Standarta bibliotēkas rīki ir vērsti, galvenokārt, uz horizontālo scalēšanu: tie ļauj palielināt kešu līdz gigantiskām dimensijām un nodrošināt piekļuvi tam no koda, kas izvietots dažādos serveros. Tomēr mūsu situācijā uzglabāto datu apjoms nepārsniedz dažus gigabaitus, un viena vai divu mezglu produktivitāte ir pilnīgi pietiekama. Attiecīgi no noderīgā standarta rīki varētu tikai nodrošināt memcached pieejamību, saglabājot vismaz vienu keša instanci darba kārtībā. Tomēr pat šo iespēju izmantot neizdevās... Jāatgādina par projekta ietvarā izmantotā rāmja novecošanu, kuru dēļ nebija iespējams likt strādāt lietojumprogrammu ar serveru baseinu. Neaizmirsīsim arī par sesiju datu zudumiem: no masveida lietotāju izrakstīšanās klients sajuta to.
Ideālā gadījumā bija nepieciešama rekliācijas ieraksta memcached un replikāciju apiet gadījumā, ja rodas kļūda vai kļūda. Šīs stratēģijas īstenošana mums palīdzēja .
mcrouter
Tas ir memcached maršrutētājs, ko izstrādājusi Facebook, lai atrisinātu savas problēmas. Tas atbalsta memcached teksta protokolu, kas ļauj skalēšanas memcached instalācijas līdz nepamatītiem izmēriem. Detalizētu aprakstu par mcrouter var atrast . Papildus citai tas var paveikt to, kas mums nepieciešams:
- reklēšanas ierakstu;
- veikt fallback uz citām serveru grupas gadījumā kļūdas rašanas.
Uz priekšu!
mcrouter konfigurācija
Pāriesim uz konfigurāciju:
{
"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"
]
}
}
}
}Kāpēc trīs baseini? Kāpēc serveri atkārtojas? Apskatīsim, kā tas darbojas.
- Šajā konfigurācijā mcrouter izvēlas ceļu, pa kuru tiks nosūtīts pieprasījums, pamatojoties uz pieprasījuma komandu. Par to viņam saka tips
OperationSelectorRoute. - GET pieprasījumi nonāk apstrādē
RandomRoute, kas nejauši izvēlas baseinu vai maršrutu no masīva objektuchildren. Katrs šī masīva elements savukārt ir apstrādātājsMissFailoverRoute, kas izies cauri katram serverim baseinā, kamēr neiegūs atbildi ar datiem, kas tiks atgriezta klientam. - Ja mēs izmantotu tikai
MissFailoverRoutear trīs serveriem bāzē, tad visi pieprasījumi vispirms nonāktu pirmajā memcached instancē, bet pārējie saņemtu pieprasījumus pēc atlikuma principa, kad dati nav pieejami. Šāda pieeja novestu pie pārmērīgas slodzes pirmajam saraksta serverim, tāpēc tika nolemts ģenerēt trīs baseinus ar adresēm atšķirīgā secībā un izvēlēties tos nejauši. - Visi pārējie pieprasījumi (t.i., rakstīšana) tiek apstrādāti, izmantojot
AllMajorityRoute. Šis apstrādātājs sūta pieprasījumus visiem serveriem baseinā un gaida atbildes, vismaz no N/2 + 1 no tiem. IzmantošanaAllSyncRouterakstīšanas operācijām bija jāatmet, jo šī metode prasa pozitīvu atbildi no visi grupas serveriem — pretējā gadījumā tā atgriežSERVER_ERROR. Lai gan šajā gadījumā mcrouter saglabās datus pieejamajos kešos, taču izsaucošā PHP funkcija atgriezīs kļūdu un ģenerēs paziņojumu.AllMajorityRoutenav tik stingrs un ļauj izslēgt līdz puscitu mezglu bez iepriekš aprakstītām problēmām.
Galvenais trūkums šai shēmai ir tas, ka, ja kešā patiešām nav datu, tad katram pieprasījumam no klienta faktiski tiks veikti N pieprasījumi uz memcached — uz visiem servery bāzē. Var samazināt serveru skaitu bāzēs, piemēram, līdz diviem: upurējot uzglabāšanas uzticamību, mēs iegūsim bolielāka ātruma un mazāka slodze uz pieprasījumiem uz neesošām atslēgām.
Piezīme: Noderīgas saites mcrouter izpētei var būt arī un (tostarp slēgtās), kas ir īsta dārgumu krātuve dažādām konfigurācijām.
mcrouter montāža un palaišana
Lietojumprogramma (un pats memcached) darbojas Kubernetes — attiecīgi, turpat atrodas arī mcrouter. Lai izveidotu konteineru mēs izmantojam , konfigurācija izskatīsies šādi:
Piezīme: Rakstā pārpublicētie saraksti atrodami repozitorijā .
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: Uzstādīt priekšnosacījumus
apt:
name: [ 'apt-transport-https', 'tzdata', 'locales' ]
update_cache: yes
- name: Pievienot mcrouter APT atslēgu
apt_key:
url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
- name: Pievienot mcrouter repozitoriju
apt_repository:
repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
filename: mcrouter
update_cache: yes
- name: Iestatīt laika joslu
timezone:
name: "Europe/Moscow"
- name: Pārliecināties, ka pastāv valoda
locale_gen:
name: en_US.UTF-8
state: present
install:
- name: Instalēt mcrouter
apt:
name: [ 'mcrouter' ]()
… un uzmetam Helm-chart. No interesantā — šeit ir tikai konfigurācijas ģenerators atkarībā no repliku skaita (ja kādam ir kāds kodolīgāks un elegants risinājums — dalieties komentāros):
{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- Filling the array with two copies of servers: "0 1 2 0 1 2" *\/ -}}
{{- range until 2 -}}
{{- range $i, $_ := until $count -}}
{{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
{{- end -}}
{{- end -}}
{{- Shifting through the array, we get N slices: "[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 }}
}
}
}
}()
Izvietojam testēšanas vidē un pārbaudām:
# 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ļūdas teksta meklējumi rezultātu nesniedza, tomēr meklējot „” pirmajās rindās parādījās vecākā neatrisinātā problēma projektā — binārā protokola memcached.
Piezīme: ASCII protokols memcached ir lēnāks par bināro, un arī iebūvētie rīki konsekventa atslēgu hashēšanai darbojas tikai ar bināro protokolu. Taču tas konkrētajā gadījumā nerada problēmas.
Jautājums risināts: atliek tikai pāriet uz ASCII protokolu un viss darbosies…. Tomēr šajā gadījumā ieradums meklēt atbildes dokumentācijā spēlē sliktu joks. Pareizu atbildi tur neatradīsiet… ja vien, protams, nesavilksiet līdz beigām, kur sadaļā «Lietotāju sniegtās piezīmes» būs pareiza un .
Jā, pareizais opcijas nosaukums ir memcached.sess_binary_protocol. To nepieciešams izslēgt, pēc tam sesijas sāks darboties. Atliek tikai ievietot mcrouter konteineru PHP podā!
Secinājums
Tādējādi ar vienkāršām infrastruktūras izmaiņām izdevās atrisināt anoto jautājumu: jautājums par memcached drošību ir atrisināts, un kešatmiņas glabāšanas uzticamība ir palielinājusies. Papildus acīmredzamajiem ieguvumiem lietojumprogrammā tas radīja manevrēšanas iespējas platformas uzlabošanas procesā: kad visiem komponentiem ir rezerve, administratora dzīve ievērojami atvieglojas. Jā, šai metodei ir arī savi trūkumi, tā var izskatīties kā «Ķīselis», bet, ja tas ietaupa naudu, slēpj problēmu un nerada jaunas — kāpēc gan ne?
P.S.
Lasiet arī mūsu emuāra rakstos:
- «Prakse ar dapp» (uz symfony-demo piemēra): un ;
- «».
Avots: habr.com
