
Razvoj projekata visokog opterećenja na bilo kom jeziku zahteva poseban pristup i upotrebu posebnih alata, ali kada je reč o aplikacijama u PHP-u, situacija se može toliko pogoršati da morate da razvijate npr. . U ovoj bilješci ćemo govoriti o poznatoj muci s distribuiranom pohranom sesija i keširanjem podataka u memcached-u i kako smo riješili ove probleme u jednom "ward" projektu.
Junak ove prilike je PHP aplikacija zasnovana na symfony 2.3 frameworku, koja uopšte nije uključena u poslovne planove za ažuriranje. Pored sasvim standardne memorije za sesije, ovaj projekat je u potpunosti iskoristio politika "keširanja svega". u memcached-u: odgovori na zahtjeve prema bazi podataka i API serverima, razne zastavice, brave za sinhronizaciju izvršavanja koda i još mnogo toga. U takvoj situaciji, kvar memcached-a postaje fatalan za rad aplikacije. Osim toga, gubitak predmemorije dovodi do ozbiljnih posljedica: DBMS počinje pucati po šavovima, API servisi počinju zabranjivati zahtjeve itd. Stabilizacija situacije može potrajati desetine minuta, a za to vrijeme usluga će biti užasno spora ili potpuno nedostupna.
Morali smo da obezbedimo mogućnost horizontalnog skaliranja aplikacije uz malo truda, tj. uz minimalne promjene izvornog koda i očuvanu punu funkcionalnost. Učinite keš memoriju ne samo otpornom na kvarove, već i pokušajte minimizirati gubitak podataka iz nje.
Šta nije u redu sa samim memcachedom?
Općenito, ekstenzija memcached za PHP podržava distribuirane podatke i skladištenje sesija izvan kutije. Mehanizam konzistentnog heširanja ključeva omogućava ravnomjerno postavljanje podataka na mnoge servere, jedinstveno adresiranje svakog specifičnog ključa na određeni server iz grupe, a ugrađeni alati za prelazak na grešku osiguravaju visoku dostupnost usluge keširanja (ali, nažalost, nema podataka).
Stvari su malo bolje sa pohranom sesije: možete konfigurirati memcached.sess_number_of_replicas, zbog čega će podaci biti pohranjeni na nekoliko servera odjednom, a u slučaju kvara jedne memcached instance, podaci će se prenijeti sa drugih. Međutim, ako se server vrati na mrežu bez podataka (kao što se obično dešava nakon ponovnog pokretanja), neki od ključeva će se preraspodijeliti u njegovu korist. U stvari, ovo će značiti gubitak podataka o sesiji, pošto ne postoji način da se u slučaju promašaja „pređe“ na drugu repliku.
Standardni bibliotečki alati su uglavnom namenjeni horizontalno skaliranje: omogućavaju vam da povećate keš memoriju na gigantske veličine i omogućite mu pristup iz koda koji se nalazi na različitim serverima. Međutim, u našoj situaciji, volumen pohranjenih podataka ne prelazi nekoliko gigabajta, a performanse jednog ili dva čvora su sasvim dovoljne. Shodno tome, jedini korisni standardni alati mogu biti da se osigura dostupnost memcached-a uz održavanje barem jedne instance keša u radnom stanju. Međutim, čak ni ovu priliku nije bilo moguće iskoristiti... Ovdje je vrijedno podsjetiti se na starinu korišćenog okvira u projektu, zbog čega je bilo nemoguće natjerati aplikaciju da radi sa skupom servera. Ne zaboravimo ni gubitak podataka o sesiji: oko kupca se trzlo od masovne odjave korisnika.
Idealno je bilo potrebno replikacija zapisa u memcached i zaobilaženje replika u slučaju greške ili greške. Pomogao nam je da implementiramo ovu strategiju .
mcrouter
Ovo je memcached ruter koji je razvio Facebook kako bi riješio svoje probleme. Podržava memcached tekstualni protokol, što omogućava scale memcached instalacije do ludih razmera. Detaljan opis mcroutera možete pronaći u . Između ostalog može da uradi ono što nam treba:
- replicirati zapis;
- vratite se na druge servere u grupi ako dođe do greške.
Za posao!
mcrouter konfiguracija
Idem direktno na konfiguraciju:
{
"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"
]
}
}
}
}Zašto tri bazena? Zašto se serveri ponavljaju? Hajde da shvatimo kako to funkcioniše.
- U ovoj konfiguraciji, mcrouter odabire putanju na koju će zahtjev biti poslan na osnovu naredbe request. Tip mu to kaže
OperationSelectorRoute. - GET zahtjevi idu rukovaocu
RandomRoutekoji nasumično bira skup ili rutu među objektima nizachildren. Svaki element ovog niza je zauzvrat rukovalacMissFailoverRoute, koji će prolaziti kroz svaki server u spremištu dok ne dobije odgovor s podacima, koji će biti vraćeni klijentu. - Ako bismo koristili isključivo
MissFailoverRoutesa skupom od tri servera, tada bi svi zahtjevi dolazili prvi do prve memcached instance, a ostali bi primali zahtjeve na rezidualnoj osnovi kada nema podataka. Takav pristup bi doveo do preveliko opterećenje na prvom serveru na listi, pa je odlučeno da se generišu tri skupa sa adresama u različitim sekvencama i da se biraju nasumično. - Svi ostali zahtjevi (a ovo je zapis) se obrađuju pomoću
AllMajorityRoute. Ovaj obrađivač šalje zahtjeve svim serverima u spremištu i čeka odgovore od najmanje N/2 + 1 njih. Od upotrebeAllSyncRouteza operacije pisanja su morale biti napuštene, jer ova metoda zahtijeva pozitivan odgovor od всех servera u grupi - inače će se vratitiSERVER_ERROR. Iako će mcrouter dodati podatke u dostupne keš memorije, poziva PHP funkciju će vratiti grešku i daće obaveštenje.AllMajorityRoutenije tako strog i dozvoljava da se do polovine jedinica stavi van upotrebe bez gore opisanih problema.
Glavni nedostatak Ova shema je da ako zaista nema podataka u kešu, tada će se za svaki zahtjev klijenta N zahtjeva za memcached stvarno izvršiti - do svima servere u bazenu. Možemo smanjiti broj servera u grupama, na primjer, na dva: žrtvujući pouzdanost skladištenja, dobijamoоveća brzina i manje opterećenje od zahtjeva do ključeva koji nedostaju.
NB: Takođe možete pronaći korisne veze za učenje mcroutera и (uključujući zatvorene), koji predstavljaju čitavo skladište različitih konfiguracija.
Izrada i pokretanje mcroutera
Naša aplikacija (i sam memcached) radi u Kubernetesu - shodno tome, mcrouter se takođe nalazi tamo. Za montaža kontejnera koristimo , konfiguracija za koju će izgledati ovako:
NB: Liste dati u članku su objavljeni u spremištu .
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' ]()
...i skicirati Helm chart. Zanimljivo je da postoji samo generator konfiguracije zasnovan na broju replika (ako neko ima lakoničniju i elegantniju opciju, podijeli je u komentarima):
{{- $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 }}
}
}
}
}()
Ubacujemo ga u testno okruženje i provjeravamo:
# 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 >Pretraživanje teksta greške nije dalo nikakve rezultate, ali za upit “„U prvom planu je bio najstariji nerešeni problem projekta – memcached binarni protokol.
NB: ASCII protokol u memcachedu je sporiji od binarnog, a standardna sredstva konzistentnog heširanja ključa rade samo s binarnim protokolom. Ali to ne stvara probleme za konkretan slučaj.
Trik je u torbi: sve što treba da uradite je da pređete na ASCII protokol i sve će raditi.... Međutim, u ovom slučaju, navika traženja odgovora u odigrao okrutnu šalu. Tamo nećete naći tačan odgovor... osim ako, naravno, ne skrolujete do kraja, gdje je u odjeljku "Bilješke koje su doprinijeli korisnici" biće vjerni i .
Da, ispravan naziv opcije je memcached.sess_binary_protocol. Mora biti onemogućen, nakon čega će sesije početi raditi. Sve što ostaje je da se kontejner sa mcrouter-om stavi u pod sa PHP-om!
zaključak
Dakle, samo infrastrukturnim promjenama uspjeli smo riješiti problem: riješen je problem tolerancije grešaka u memcach-u, a povećana je pouzdanost keš memorije. Pored očiglednih prednosti za aplikaciju, to je dalo prostor za manevar pri radu na platformi: kada sve komponente imaju rezervu, život administratora je uveliko pojednostavljen. Da, i ova metoda ima svoje nedostatke, može izgledati kao „štaka“, ali ako štedi novac, zatrpava problem i ne uzrokuje nove - zašto ne?
PS
Pročitajte i na našem blogu:
- "Vježbaj uz dapp" (koristeći symfony-demo kao primjer): и ;
- «".
izvor: www.habr.com
