
Razvoj visoko opterećenih projekata na bilo kojem jeziku zahteva poseban pristup i upotrebu specijalnih alata, ali kada je reč o aplikacijama na PHP-u, situacija može postati toliko ozbiljna da je potrebno razviti, na primer, . U ovom članku govorićemo o poznatoj boli sa distribuiranim skladištenjem sesija i keširanjem podataka u memcached-u i o tome kako smo rešavali te probleme u jednom 'podobnom' projektu.
Glavni junak - aplikacija na PHP-u, koja se bazira na okviru symfony 2.3, čija nadogradnja nije deo poslovnog plana. Pored sasvim standardnog skladištenja sesija, u ovom projektu je u punoj meri korišćena politika „keširanja svega“ u memcached-u: odgovora na zahteve ka Bazi podataka i API serverima, raznih oznaka, zaključavanja za sinhronizaciju izvršenja koda i mnogo drugih stvari. U takvoj situaciji kvar memcached-a postaje fatalan za rad aplikacije. Pored toga, gubitak keša dovodi do ozbiljnih posledica: DBMS počinje da popušta po šavovima, API servisi blokiraju zahteve itd. Stabilizacija situacije može potrajati desetine minuta, a u tom vremenu servis će jako usporiti ili će potpuno postati nedostupan.
Potrebno je obezbediti mogućnost horizontalnog skaliranja aplikacije uz minimalne troškove, tj. sa minimalnim izmenama izvorne koda i potpunim očuvanjem funkcionalnosti. Napraviti keš ne samo otpornim na kvarove, već i nastojati da se minimiziraju gubici podataka iz njega.
Šta nije u redu sa samim memcached-om?
Uopšte, ekstenzija memcached za PHP 'iz kutije' podržava distribuirano skladištenje podataka i sesija. Mehanizam konzistentnog heširanja ključeva omogućava ravnomerno raspoređivanje podataka na mnogim serverima, jasno adresirajući svaki određeni ključ određenom serveru iz grupe, a ugrađena sredstva za failover obezbeđuju visoku dostupnost usluge keširanja (ali, nažalost, ne podataka).
Sa skladištenjem sesija stvari su malo bolje: može se konfigurisati memcached.sess_number_of_replicas, kar katerega se podatki shranjujejo takoj na več strežnikov, v primeru okvare enega primerka memcached pa se podatki dostavljajo z drugih. Vendar, če se strežnik vrne v omrežje brez podatkov (kot je običajno po ponovnem zagonu), bo del ključev prerazporejen v njegovo korist. To v resnici pomeni izgubo podatkov seje, saj ni možnosti, da bi 'odšli' na drugo replikacijo v primeru napake.
Standardna orodja knjižnice so večinoma usmerjena v horizontalno širitev: omogočajo povečanje predpomnilnika do velikanskih velikosti in zagotavljajo dostop do njega iz kode, ki je nameščena na različnih strežnikih. Vendar pa v naši situaciji obseg shranjenih podatkov ne presega več gigabajtov, poleg tega pa je zmogljivost enega oziroma dveh vozlišč povsem dovolj. Zato so lahko koristna standardna orodja le zagotavljala dostopnost memcached ob ohranjanju vsaj enega primerka predpomnilnika v delovnem stanju. Kljub temu pa te možnosti nismo mogli uporabiti ... Tu velja omeniti starost okvira, uporabljenega v projektu, zaradi katerega se aplikacije ni mogoče spraviti v delovanje s skladom strežnikov. Prav tako ne smemo pozabiti na izgube podatkov seje: množično odjavljanje uporabnikov je povzročalo, da je naročnik trpel.
Ideja je bila replikacija zapisov v memcached in obhod replik v primeru napake ali težave. To strategijo nam je pomagal uresničiti .
mcrouter
To je usmerjevalnik za memcached, ki ga je razvilo podjetje Facebook, da bi rešilo svoje težave. Podpira besedilni protokol memcached, ki omogoča širitev namestitev memcached do neverjetnih velikosti. Podroben opis mcrouterja lahko najdete v . Poleg vse lahko stori to, kar potrebujemo:
- replikacija zapisov;
- preklapljanje na druge strežnike v skupini v primeru napake.
Na delo!
Konfiguracija mcrouterja
Takoj preidem na konfiguracijo:
{
"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"
]
}
}
}
}Zakaj tri bazene? Zakaj se strežniki ponavljajo? Poglejmo, kako to deluje.
- V tej konfiguraciji mcrouter izbira pot, po kateri bo poslan zahtevek, na podlagi ukaza zahtevka. To mu pove tip
OperationSelectorRoute. - Zahtevki GET gredo v obdelovalec
RandomRoute, ki naključno izbere bazen ali pot med objekti v poljuchildren. Vsak element tega polja je prav tako obdelovalecMissFailoverRoute, ki preišče vsak strežnik v bazenu, dokler ne prejme odgovora s podatki, kar bo nato vrnjeno stranki. - Če bi uporabljali izključno
MissFailoverRoutebazen s tremi strežniki, bi vsi zahtevki najprej prišli na prvo instanco memcached, preostali pa bi dobili zahtevke na preostali osnovi, ko podatki manjkajo. Tak pristop bi privedel do prekomerne obremenitve prvega strežnika na seznamu, zato je bila odločitev generirati tri bazene z naslovi v različnih zaporedjih in jih izbirati naključno. - Vse ostale zahtevke (to so zapisi) obdelujejo z uporabo
AllMajorityRoute. Ta obdelovalec pošlje zahtevke vsem strežnikom v bazenu in čaka na odgovore, vsaj od N/2 + 1 izmed njih. UporabaAllSyncRouteza operacije zapisovanja je bila opustitev, saj ta metoda zahteva pozitiven odgovor od vseh strežnikov skupine — v nasprotnem primeru bo vrnilaSERVER_ERROR. Čeprav mcrouter shranjuje podatke v dostopne predpomnilnike, bo klicna funkcija PHP vrnila napako in generirala obvestilo.AllMajorityRouteni tako strog in omogoča izklop do polovice vozlišč brez zgoraj omenjenih težav.
Glavna pomanjkljivost tega shema pa je, da če podatkov v predpomnilniku res ni, bo za vsak zahtevek stranke dejansko izvedeno N zahtevkov v memcached — na z vsem strežnike v bazenu. Število strežnikov v bazenih lahko zmanjšamo, na primer, na dva: s tem, da žrtvujemo zanesljivost shranjevanja, pridobimo boчитая скорость и уменьшая нагрузку от запросов к отсутствующим ключам.
NB: Полезные ссылки для изучения mcrouter могут также оказаться in (включая закрытые), представляющие собой целый кладезь различных конфигураций.
Сборка и запуск mcrouter
Приложение (и сам memcached) у нас работает в Kubernetes — соответственно, там же располагается и mcrouter. Для сборки контейнера мы используем , конфиг для которого будет выглядеть следующим образом:
NB: Листинги, приведенные в статье, опубликованы в репозитории .
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: Установить зависимости
apt:
name: [ 'apt-transport-https', 'tzdata', 'locales' ]
update_cache: yes
- name: Добавить ключ APT mcrouter
apt_key:
url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
- name: Добавить репозиторий mcrouter
apt_repository:
repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
filename: mcrouter
update_cache: yes
- name: Установить часовой пояс
timezone:
name: "Europe/Moscow"
- name: Обеспечить наличие локали
locale_gen:
name: en_US.UTF-8
state: present
install:
- name: Установить mcrouter
apt:
name: [ 'mcrouter' ]()
… и формируем Helm-chart. Из интересного — здесь только генератор конфигурации от количества реплик (если у кого-то есть более лаконичный и элегантный вариант — делитесь в комментариях):
{{- $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|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 }}
}
}
}
}()
Выкатываем в тестовое окружение и проверяем:
# 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 >Поиск по тексту ошибки результата не дал, однако по запросу «» в первых рядах значилась старая незакрытая проблема проекта — бинарного протокола memcached.
NB: ASCII-protocol in memcached is slower than the binary one, and the standard consistent hashing of keys only works with the binary protocol. But this does not create any problems for the specific case.
It's a done deal: you just need to switch to the ASCII protocol and everything will work... However, in this case, the habit of looking for answers in played a cruel joke. You won't find the correct answer there... unless, of course, you scroll to the end, where in the section "User contributed notes" there will be the right and .
Yes, the correct name of the option is memcached.sess_binary_protocol. It needs to be disabled, after which the sessions will start to work. All that's left is to place the container with mcrouter in the pod with PHP!
Zaključek
Thus, with just infrastructure changes, we managed to solve the task at hand: the issue of memcached's fault tolerance is resolved, the reliability of cache storage is increased. Besides the obvious advantages for the application, this provided room for maneuver during work on the platform: when all components have a reserve, the life of the administrator becomes much easier. Yes, this method has its disadvantages, it may look like a "crutch," but if it saves money, hides the problem, and doesn't cause new ones—why not?
P.S.
Preberite tudi na našem blogu:
- "Practice with dapp" (using symfony-demo as an example): in ;
- «».
Izvor: habr.com
