
Kuriant didelės apkrovos projektus bet kuria kalba reikia specialaus požiūrio ir specialių įrankių naudojimo, tačiau kalbant apie programas PHP, padėtis gali taip pablogėti, kad tenka kurti, pvz. . Šioje pastaboje kalbėsime apie pažįstamą skausmą paskirstytoje seanso saugykloje ir duomenų talpykloje „memcached“ ir kaip mes išsprendėme šias problemas viename „palatos“ projekte.
Šios progos herojus – PHP programa, pagrįsta symfony 2.3 sistema, kuri visiškai neįtraukta į verslo planus atnaujinti. Be gana standartinės seansų saugyklos, šis projektas visiškai išnaudojo „viską saugoti talpykloje“ politika „memcached“: atsakymai į duomenų bazės ir API serverių užklausas, įvairios vėliavėlės, kodo vykdymo sinchronizavimo užraktai ir daug daugiau. Esant tokiai situacijai, „memcached“ gedimas tampa lemtingas programos veikimui. Be to, talpyklos praradimas sukelia rimtų pasekmių: DBVS pradeda sprogti, API paslaugos pradeda uždrausti užklausas ir pan. Situacijos stabilizavimas gali užtrukti keliasdešimt minučių, o per tą laiką paslauga bus siaubingai lėta arba visiškai nepasiekiama.
Mums reikėjo aprūpinti galimybė be pastangų horizontaliai keisti programos mastelį, t.y. su minimaliais šaltinio kodo pakeitimais ir išsaugomas visas funkcionalumas. Padarykite talpyklą ne tik atsparią gedimams, bet ir stenkitės kuo labiau sumažinti duomenų praradimą.
Kas negerai su memcached?
Apskritai, atmintyje išsaugotas PHP plėtinys palaiko paskirstytus duomenis ir seansų saugyklą. Nuoseklios raktų maišos mechanizmas leidžia tolygiai talpinti duomenis daugelyje serverių, unikaliai adresuojant kiekvieną konkretų raktą į konkretų grupės serverį, o integruoti perkėlimo įrankiai užtikrina aukštą talpyklos paslaugos prieinamumą (bet, deja, nėra duomenų).
Su seansų saugykla viskas yra šiek tiek geriau: galite konfigūruoti memcached.sess_number_of_replicas, ko pasekoje duomenys bus saugomi keliuose serveriuose vienu metu, o sugedus vienam atmintyje įrašytam egzemplioriui, duomenys bus perkelti iš kitų. Tačiau jei serveris vėl prisijungs be duomenų (kaip paprastai atsitinka po paleidimo iš naujo), kai kurie raktai bus perskirstyti jo naudai. Tiesą sakant, tai reikš sesijos duomenų praradimas, nes praleidimo atveju nėra galimybės „eiti“ į kitą kopiją.
Standartinės bibliotekos priemonės daugiausia skirtos horizontaliai mastelio keitimas: jie leidžia padidinti talpyklą iki milžiniškų dydžių ir suteikti prieigą prie jos iš kodo, esančio skirtinguose serveriuose. Tačiau mūsų situacijoje saugomų duomenų kiekis neviršija kelių gigabaitų, o vieno ar dviejų mazgų našumo visiškai pakanka. Atitinkamai, vieninteliai naudingi standartiniai įrankiai galėtų būti atmintinės talpyklos prieinamumo užtikrinimas, išlaikant bent vieno talpyklos egzemplioriaus veikimo būklę. Tačiau net ir šia galimybe pasinaudoti nepavyko... Čia verta priminti projekte naudoto karkaso senumą, dėl ko nepavyko aplikacijos dirbti su serverių baseinu. Nepamirškime ir apie sesijos duomenų praradimą: kliento akys trūkčiojo nuo masinio vartotojų atsijungimo.
Idealiu atveju to reikėjo Įrašų replikavimas atmintinėje išsaugotose ir apeinančiose kopijose klaidos ar klaidos atveju. Padėjo mums įgyvendinti šią strategiją .
mcrouter
Tai atmintyje išsaugotas maršrutizatorius, kurį „Facebook“ sukūrė savo problemoms išspręsti. Jis palaiko atmintyje įrašytą teksto protokolą, kuris leidžia mastelio atmintinės instaliacijos iki beprotiškų proporcijų. Išsamų mcrouter aprašymą galite rasti . Be kita ko tai gali padaryti tai, ko mums reikia:
- pakartoti įrašą;
- jei įvyktų klaida, grįžkite į kitus grupės serverius.
Į darbą!
mcrouter konfigūracija
Eisiu tiesiai į konfigūraciją:
{
"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"
]
}
}
}
}Kodėl trys baseinai? Kodėl serveriai kartojasi? Išsiaiškinkime, kaip tai veikia.
- Šioje konfigūracijoje mcrouter pasirenka kelią, kuriuo bus siunčiama užklausa, remdamasi užklausos komanda. Vaikinas jam tai sako
OperationSelectorRoute. - GET užklausos siunčiamos tvarkytojui
RandomRoutekuri atsitiktinai parenka telkinį arba maršrutą tarp masyvo objektųchildren. Kiekvienas šio masyvo elementas savo ruožtu yra tvarkytuvasMissFailoverRoute, kuris eis per kiekvieną telkinio serverį, kol gaus atsakymą su duomenimis, kurie bus grąžinti klientui. - Jei naudotume išskirtinai
MissFailoverRoutesu trijų serverių telkiniu, tada visos užklausos pirmiausia gautų pirmąjį atmintinėje išsaugotą egzempliorių, o likusieji gautų užklausas likutiniu pagrindu, kai nėra duomenų. Toks požiūris paskatintų per didelė pirmojo sąrašo serverio apkrova, todėl buvo nuspręsta sugeneruoti tris telkinius su adresais skirtingose sekose ir pasirinkti juos atsitiktine tvarka. - Visos kitos užklausos (ir tai yra įrašas) apdorojamos naudojant
AllMajorityRoute. Šis tvarkytuvas siunčia užklausas visiems telkinyje esantiems serveriams ir laukia atsakymų iš bent N/2 + 1 iš jų. Nuo naudojimoAllSyncRouterašymo operacijų teko atsisakyti, nes šis metodas reikalauja teigiamo atsakymo iš visi serverių grupėje – kitaip grįšSERVER_ERROR. Nors mcrouter pridės duomenis prie turimų talpyklų, iškviečiama PHP funkcija grąžins klaidą ir sukurs pranešimą.AllMajorityRoutenėra toks griežtas ir leidžia išjungti iki pusės agregatų be aukščiau aprašytų problemų.
Pagrindinis trūkumas Ši schema yra tokia, kad jei talpykloje tikrai nėra duomenų, tada kiekvienai kliento užklausai iš tikrųjų bus vykdoma N užklausų į atmintinę. visi serveriai baseine. Mes galime sumažinti serverių skaičių telkiniuose, pavyzdžiui, iki dviejų: paaukodami saugyklos patikimumą, gaunameоdidesnis greitis ir mažesnė apkrova nuo užklausų iki trūkstamų raktų.
NB: Taip pat galite rasti naudingų nuorodų mokantis mcrouter и (įskaitant uždaras), atstovaujančias visą įvairių konfigūracijų sandėlį.
„Mcrouter“ kūrimas ir valdymas
Mūsų programa (ir pati atmintinė) veikia Kubernetes - atitinkamai ten yra ir mcrouter. Dėl konteinerio surinkimas mes naudojame , kurios konfigūracija atrodys taip:
NB: Straipsnyje pateikti sąrašai skelbiami saugykloje .
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' ]()
... ir nubraižykite jį Vairo diagrama. Įdomu tai, kad yra tik konfigūracijos generatorius, pagrįstas kopijų skaičiumi (jei kas turi lakoniškesnį ir elegantiškesnį variantą, pasidalinkite komentaruose):
{{- $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 }}
}
}
}
}()
Iškeliame jį į bandomąją aplinką ir patikriname:
# 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 >Klaidos teksto paieška nedavė jokių rezultatų, bet užklausa „„Priešakyje buvo seniausia neišspręsta projekto problema – atmintyje išsaugotas dvejetainis protokolas.
NB: Memcached ASCII protokolas yra lėtesnis nei dvejetainis, o standartinės nuoseklios raktų maišos priemonės veikia tik su dvejetainiu protokolu. Tačiau tai nesukelia problemų konkrečiu atveju.
Triukas maiše: tereikia persijungti į ASCII protokolą ir viskas veiks.... Tačiau šiuo atveju įprotis ieškoti atsakymų suvaidino žiaurų pokštą. Ten nerasite teisingo atsakymo... nebent, žinoma, slinksite iki galo, kur skiltyje „Naudotojo pateiktos pastabos“ bus ištikimas ir .
Taip, teisingas parinkties pavadinimas memcached.sess_binary_protocol. Jis turi būti išjungtas, po kurio užsiėmimai pradės veikti. Belieka įdėti konteinerį su mcrouter į podelį su PHP!
išvada
Taigi tik infrastruktūros pakeitimais pavyko išspręsti problemą: buvo išspręsta atmintinės gedimų tolerancijos problema, padidintas talpyklos saugojimo patikimumas. Be akivaizdžių privalumų programai, tai suteikė erdvės manevruoti dirbant platformoje: kai visi komponentai turi rezervą, administratoriaus gyvenimas labai supaprastėja. Taip, šis metodas turi ir trūkumų, jis gali atrodyti kaip „ramentas“, bet jei sutaupo pinigų, užkasa problemą ir nesukelia naujų – kodėl gi ne?
PS
Taip pat skaitykite mūsų tinklaraštyje:
- "Praktika su dapp" (kaip pavyzdį naudojant symfony-demo): и ;
- «".
Šaltinis: www.habr.com
