Používame mcrouter na horizontálne škálovanie memcached.

Používame mcrouter na horizontálne škálovanie memcached.

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, vlastiti server aplikacija. 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.

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 tem obvestilu. Poleg vse široke funkcionalnosti 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 polju children. Vsak element tega polja je prav tako obdelovalec MissFailoverRoute, ki preišče vsak strežnik v bazenu, dokler ne prejme odgovora s podatki, kar bo nato vrnjeno stranki.
  • Če bi uporabljali izključno MissFailoverRoute bazen 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. Uporaba AllSyncRoute za operacije zapisovanja je bila opustitev, saj ta metoda zahteva pozitiven odgovor od vseh strežnikov skupine — v nasprotnem primeru bo vrnila SERVER_ERROR. Čeprav mcrouter shranjuje podatke v dostopne predpomnilnike, bo klicna funkcija PHP vrnila napako in generirala obvestilo. AllMajorityRoute ni 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 могут также оказаться документация в wiki in проблемы проекта (включая закрытые), представляющие собой целый кладезь различных конфигураций.

Сборка и запуск mcrouter

Приложение (и сам memcached) у нас работает в Kubernetes — соответственно, там же располагается и mcrouter. Для сборки контейнера мы используем werf, конфиг для которого будет выглядеть следующим образом:

NB: Листинги, приведенные в статье, опубликованы в репозитории flant/mcrouter.

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' ]

(werf.yaml)

… и формируем 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 }}
 }
 }
 }
 }

(10-mcrouter.yaml)

Выкатываем в тестовое окружение и проверяем:

# 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 >

Поиск по тексту ошибки результата не дал, однако по запросу «mcrouter 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 the documentation at php.net 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 unduly downvoted answer..

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:

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster