Upotreba mcroutera za vodoravno skaliranje memcached-a

Upotreba mcroutera za vodoravno skaliranje memcached-a

Razvoj visokoopterećenih projekata na bilo kojem jeziku zahtijeva poseban pristup i korištenje posebnih alata, no kada su u pitanju aplikacije u PHP-u, situacija se može toliko pogoršati da morate razvijati npr. vlastiti poslužitelj aplikacija. U ovoj bilješci govorit ćemo o poznatoj muci s distribuiranom pohranom sesija i predmemorijom podataka u memcachedu i kako smo riješili te probleme u jednom "odjeljenom" projektu.

Junak prigode je PHP aplikacija temeljena na symfony 2.3 okviru, koja uopće nije uključena u poslovne planove za ažuriranje. Uz sasvim standardnu ​​pohranu sesije, ovaj je projekt u potpunosti iskoristio pravilo "spremanje svega". u memcached: odgovori na zahtjeve bazi podataka i API poslužiteljima, razne zastavice, brave za sinkronizaciju izvršavanja koda i još mnogo toga. U takvoj situaciji, kvar memcached postaje koban za rad aplikacije. Osim toga, gubitak predmemorije dovodi do ozbiljnih posljedica: DBMS počinje pucati po šavovima, API usluge počinju zabranjivati ​​zahtjeve itd. Stabilizacija situacije može trajati desetke minuta, a za to vrijeme usluga će biti užasno spora ili potpuno nedostupna.

Trebali smo pružiti mogućnost vodoravnog skaliranja aplikacije uz malo truda, tj. s minimalnim izmjenama izvornog koda i očuvanom punom funkcionalnošću. Učinite predmemoriju ne samo otpornom na kvarove, već pokušajte minimizirati gubitak podataka iz nje.

Što nije u redu sa samim memcachedom?

Općenito, proširenje memcached za PHP podržava distribuirane podatke i pohranu sesija odmah po otvaranju. Mehanizam za dosljedno raspršivanje ključeva omogućuje ravnomjerno postavljanje podataka na više poslužitelja, jedinstveno adresiranje svakog ključa na određeni poslužitelj iz grupe, a ugrađeni alati za nadogradnju jamče visoku dostupnost usluge predmemoriranja (ali, nažalost, nema podataka).

Stvari su malo bolje s pohranom sesije: možete konfigurirati memcached.sess_number_of_replicas, zbog čega će podaci biti pohranjeni na nekoliko poslužitelja odjednom, au slučaju kvara jedne memcached instance, podaci će se prenijeti s drugih. Međutim, ako se poslužitelj vrati na mrežu bez podataka (kao što se obično događa nakon ponovnog pokretanja), neki od ključeva bit će redistribuirani u njegovu korist. U stvari ovo će značiti gubitak podataka o sesiji, budući da ne postoji način da se u slučaju promašaja “otiđe” na drugu repliku.

Alati standardne knjižnice uglavnom su usmjereni na vodoravno skaliranje: omogućuju vam da povećate predmemoriju do ogromnih veličina i omogućite joj pristup iz koda koji se nalazi na različitim poslužiteljima. Međutim, u našoj situaciji količina pohranjenih podataka ne prelazi nekoliko gigabajta, a performanse jednog ili dva čvora sasvim su dovoljne. U skladu s tim, jedini korisni standardni alati mogu biti osiguranje dostupnosti memcached-a uz održavanje najmanje jedne instance predmemorije u radnom stanju. Međutim, nije bilo moguće iskoristiti čak ni ovu priliku... Ovdje vrijedi podsjetiti na starinu okvira koji se koristi u projektu, zbog čega je bilo nemoguće natjerati aplikaciju da radi s skupom poslužitelja. Ne zaboravimo ni na gubitak podataka o sesiji: klijentu je trznulo oko zbog masovne odjave korisnika.

Idealno je bilo potrebno replikacija zapisa u memcached i zaobilazne replike u slučaju pogreške ili greške. Pomogao nam je u provedbi ove strategije mcrouter.

mcrouter

Ovo je memcached usmjerivač koji je razvio Facebook kako bi riješio svoje probleme. Podržava memcached tekstualni protokol, koji omogućuje mjeri memcached instalacije do suludih razmjera. Detaljan opis mcroutera možete pronaći u ovu najavu. Između ostalog široka funkcionalnost može učiniti ono što trebamo:

  • replicirati zapis;
  • vratite se na druge poslužitelje u grupi ako dođe do pogreške.

Bacimo se na posao!

konfiguracija mcroutera

Idem ravno 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 poslužitelji ponavljaju? Hajdemo shvatiti kako to funkcionira.

  • U ovoj konfiguraciji mcrouter odabire put na koji će zahtjev biti poslan na temelju naredbe zahtjeva. Tip mu to kaže OperationSelectorRoute.
  • GET zahtjevi idu rukovatelju RandomRoutekoji nasumično odabire skup ili rutu među objektima niza children. Svaki element ovog niza je zauzvrat rukovatelj MissFailoverRoute, koji će proći kroz svaki poslužitelj u skupu dok ne dobije odgovor s podacima, koji će biti vraćeni klijentu.
  • Ako bismo koristili isključivo MissFailoverRoute s skupom od tri poslužitelja, tada bi svi zahtjevi prvo dolazili do prve memcachirane instance, a ostali bi primali zahtjeve na rezidualnoj osnovi kada nema podataka. Takav bi pristup doveo do pretjerano opterećenje prvog poslužitelja na listi, pa je odlučeno generirati tri skupa s adresama u različitim sekvencama i odabrati ih nasumično.
  • Svi ostali zahtjevi (a ovo je zapis) obrađuju se pomoću AllMajorityRoute. Ovaj rukovatelj šalje zahtjeve svim poslužiteljima u skupu i čeka odgovore od najmanje N/2 + 1 od njih. Od upotrebe AllSyncRoute za operacije pisanja morali smo napustiti, jer ova metoda zahtijeva pozitivan odgovor od Sve poslužitelji u grupi - inače će se vratiti SERVER_ERROR. Iako će mcrouter dodati podatke u dostupne predmemorije, PHP funkcija poziva će vratiti grešku i generira obavijest. AllMajorityRoute nije tako strog i dopušta da se do polovica jedinica povuče iz upotrebe bez gore opisanih problema.

Glavni nedostatak Ova shema je da ako stvarno nema podataka u predmemoriji, tada će se za svaki zahtjev klijenta N zahtjeva za memcached zapravo izvršiti - da svima poslužitelji u bazenu. Možemo smanjiti broj poslužitelja u skupovima, na primjer, na dva: žrtvujući pouzdanost pohrane, dobivamoоveća brzina i manje opterećenja od zahtjeva do ključeva koji nedostaju.

NB: Također možete pronaći korisne poveznice za učenje mcroutera dokumentaciju na wiki и projektna pitanja (uključujući zatvorene), koji predstavljaju cijelo skladište različitih konfiguracija.

Izgradnja i pokretanje mcroutera

Naša aplikacija (i sam memcached) radi u Kubernetesu - sukladno tome, mcrouter se također nalazi tamo. Za montaža kontejnera koristimo werf, čija će konfiguracija izgledati ovako:

NB: Popisi navedeni u članku objavljeni su u repozitoriju ravni/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 skicirajte ga Shema kormila. Zanimljivo je da postoji samo generator konfiguracije na temelju broja replika (ako netko ima lakonskiju i elegantniju opciju, podijelite 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)

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

Pretraga po tekstu greške nije dala rezultate, ali po upitu “mcrouter php“U prvom planu bio je najstariji neriješeni problem projekta – nedostatak podrške memcached binarni protokol.

NB: ASCII protokol u memcached-u sporiji je od binarnog, a standardni načini dosljednog hashiranja ključa rade samo s binarnim protokolom. Ali to ne stvara probleme za konkretan slučaj.

Trik je u torbi: sve što trebate učiniti je prijeći na ASCII protokol i sve će raditi.... Međutim, u ovom slučaju navika traženja odgovora u dokumentacija na php.net odigrao okrutnu šalu. Tamo nećete pronaći točan odgovor... osim ako, naravno, ne skrolate do kraja, gdje u odjeljku "Korisničke bilješke" bit će vjerni i nepravedno negativan odgovor.

Da, točan naziv opcije je memcached.sess_binary_protocol. Mora biti onemogućen, nakon čega će sesije početi raditi. Sve što preostaje je staviti spremnik s mcrouterom u pod s PHP-om!

Zaključak

Stoga smo samo infrastrukturnim promjenama uspjeli riješiti problem: problem s memcached tolerancijom na greške je riješen, a pouzdanost pohrane predmemorije je povećana. Uz očite prednosti za aplikaciju, to je dalo manevarski prostor pri radu na platformi: kada sve komponente imaju rezervu, život administratora je znatno pojednostavljen. Da, ova metoda ima i svojih nedostataka, možda izgleda kao "štaka", ali ako štedi novac, zakopava problem i ne stvara nove - zašto ne?

PS

Pročitajte i na našem blogu:

Izvor: www.habr.com

Dodajte komentar