Korištenje mcrouter-a za horizontalno skaliranje memcached-a

Korištenje mcrouter-a za horizontalno skaliranje memcached-a

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

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 ovo saopštenje. Između ostalog široka funkcionalnost 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 niza children. Svaki element ovog niza je zauzvrat rukovalac MissFailoverRoute, 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 MissFailoverRoute sa 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 upotrebe AllSyncRoute za operacije pisanja su morale biti napuštene, jer ova metoda zahtijeva pozitivan odgovor od всех servera u grupi - inače će se vratiti SERVER_ERROR. Iako će mcrouter dodati podatke u dostupne keš memorije, poziva PHP funkciju će vratiti grešku i daće obaveštenje. AllMajorityRoute nije 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 dokumentaciju na wikiju и projektna pitanja (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 werf, konfiguracija za koju će izgledati ovako:

NB: Liste dati u članku su objavljeni u spremištu 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: 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' ]

(werf.yaml)

...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 }}
         }
       }
     }
   }

(10-mcrouter.yaml)

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 “microuter php„U prvom planu je bio najstariji nerešeni problem projekta – nedostatak podrške 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 dokumentaciju na php.net 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 nepravedno odbijen odgovor.

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:

izvor: www.habr.com

Dodajte komentar