Používáme mcrouter pro horizontální škálování memcached

Používáme mcrouter pro horizontální škálování memcached

Vývoj vysoce zatížených projektů v libovolném jazyce si vyžaduje zvláštní přístup a použití speciálních nástrojů, ale když jde o aplikace na PHP, situace se může zhoršit natolik, že je nutné vyvinout například vlastní server aplikací. V této poznámce se budeme věnovat dobře známému problému s distribuovaným ukládáním relací a kešováním dat v memcached a tomu, jak jsme tyto problémy řešili v jednom "svěřeném" projektu.

Hlavní aktér — aplikace na PHP, postavená na frameworku symfony 2.3, jejíž aktualizace není součástí plánů podnikání. Kromě zcela standardního ukládání relací v tomto projektu bylo plně využíváno politiky "kešování všeho" v memcached: odpovědí na dotazy k DB a API serverům, různých vlajek, zámků pro synchronizaci vykonávání kódu a mnohého dalšího. V takové situaci porucha memcached se stává fatální pro fungování aplikace. Kromě toho, ztráta keše vede k vážným následkům: DB začne praskat ve švech, API služby — zakazovat dotazy atd. Stabilizace situace může trvat desítky minut, a v tuto dobu bude služba hrozně zpomalovat nebo se úplně stane nedostupnou.

Bylo potřeba zajistit možnost horizontálního škálování aplikace s minimálními náklady, tj. s minimálními změnami source kódu a plným zachováním funkčnosti. Udělat keš nejen odolnou vůči poruchám, ale také se pokusit minimalizovat ztráty dat z ní.

Co není v pořádku se samotným memcached?

Rozšíření memcached pro PHP "out of the box" vlastně podporuje distribuované ukládání dat a relací. Mechanismus konzistentního hashování klíčů umožňuje rovnoměrně rozložit data na mnoha serverech, jednoznačně adresuje každý konkrétní klíč k určenému serveru ve skupině, a integrované prostředky failoveru zajišťují vysokou dostupnost kešovací služby (ale bohužel, ne dat).

S ukládáním relací to funguje o něco lépe: lze nastavit memcached.sess_number_of_replicas, což způsobí, že data budou uložena hned na několika serverech, a v případě selhání jednoho instance memcached budou data poskytována z jiných. Pokud se však server vrátí do provozu bez dat (jak to obvykle bývá po restartu), část klíčů bude přerozdělena v jeho prospěch. Vlastně to bude znamenat ztrátu dat relace, protože není možnost „zajít“ do jiné repliky v případě selhání.

Standardní nástroje knihovny jsou primárně zaměřeny na horizontální škálování: umožňují zvýšit cache na obrovské velikosti a zajistit přístup k ní z kódu umístěného na různých serverech. Nicméně v naší situaci objem uchovávaných dat nepřesahuje několik gigabajtů, a výkon jednoho nebo dvou uzlů je zcela dostatečný. Odpovídajícím způsobem, ze užitečného, by standardní nástroje mohly pouze zajistit dostupnost memcached při zachování alespoň jednoho instance cache v provozuschopném stavu. Přesto se ani tuto možnost nepodařilo využít... Je třeba připomenout stáří frameworku použitého v projektu, proto se applikaci s pool jsou serverů nedařilo rozchodit. Také nezapomínejme na ztráty dat relací: očím zákazníka skákal víc než jen nerv.

Ideálně bylo zapotřebí replikace záznamu v memcached a obcházení replik v případě selhání nebo chyby. Tuto strategii nám pomohl realizovat mcrouter.

mcrouter

Je to router pro memcached, který vyvinula společnost Facebook za účelem řešení svých problémů. Podporuje textový protokol memcached, který umožňuje škálovat instalace memcached do šílených rozměrů. Podrobný popis mcrouteru naleznete v této anonce. Kromě jiného širokého spektra funkcionality může to, co potřebujeme:

  • replikovat záznam;
  • provést fallback na jiné servery skupiny v případě chyby.

Do akce!

Konfigurace mcrouter

Přejdu rovnou k konfiguraci:

{
 "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"
       ]
     }
   }
 }
}

Proč jsou tři pooly? Proč se servery opakují? Pojďme se podívat, jak to funguje.

  • V této konfiguraci mcrouter vybírá cestu, kterou bude požadavek odeslán, na základě typu požadavku. OperationSelectorRoute.
  • GET požadavky směřují k zpracovateli RandomRoute, který náhodně vybírá pool nebo trasu z objektů pole children. Každý prvek v tomto poli je zpracovatelem MissFailoverRoute, který projde každým serverem v poolu, dokud nezíská odpověď s daty, která bude vrácena klientovi.
  • Pokud bychom používali výhradně MissFailoverRoute pool se třemi servery, všechny požadavky by nejprve přišly na první instanci memcached, zatímco ostatní by dostávaly požadavky na zbylé, pokud data nebyla k dispozici. Tento přístup by vedl k přetížení prvního serveru v seznamu, proto bylo rozhodnuto vygenerovat tři pooly s adresami v různých sekvencích a náhodně je vybírat.
  • Všechny ostatní požadavky (tedy zápis) jsou zpracovávány pomocí AllMajorityRoute. Tento zpracovatel odesílá požadavky na všechny servery poolu a čeká na odpovědi nejméně od N/2 + 1 z nich. Použití AllSyncRoute pro operace zápisu bylo nutné opustit, protože tato metoda vyžaduje pozitivní odpověď od všechny serverů skupiny — jinak vrátí SERVER_ERROR. Přestože mcrouter uloží data do dostupných cache, vyvolávající funkce PHP vrátí chybu a vygeneruje upozornění. AllMajorityRoute není tak přísná a umožňuje vypnout až polovinu uzlů bez výše uvedených problémů.

Hlavní nevýhoda této schématu je, že pokud data v cache skutečně nejsou, pro každý požadavek od klienta se fakticky vykoná N požadavků k memcached — k všem serverům v poolu. Můžeme zkrátit počet serverů v poolech, například na dva: obětováním spolehlivosti uložení, získáme bovětší rychlost a menší zátěž z požadavků na chybějící klíče.

NB: Užitečné odkazy pro studium mcrouter mohou také zahrnovat dokumentaci na wiki a problémy projektu (včetně uzavřených), které představují celou studnici různých konfigurací.

Sestavení a spuštění mcrouter

Aplikace (a samotný memcached) funguje v Kubernetes — a proto je i mcrouter na stejném místě. Pro sestavení kontejneru používáme werf, konfigurace, která bude vypadat následovně:

NB: Seznamy uvedené v článku jsou publikovány v repozitáři 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)

… a načrtneme Helm chart. Zajímavé je, že zde je pouze generátor konfigurace podle počtu replik (jestli má někdo kratší a elegantnější varianta — podělte se v komentářích):

{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{-  Naplníme pole dvěma kopií serverů: "0 1 2 0 1 2" *\/ -}}
{{- range until 2 -}}
 {{- range $i, $_ := until $count -}}
 {{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
 {{- end -}}
{{- end -}}
{{-  Posuneme se po poli, získáme N řezů: "[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)

Nasazujeme na testovací prostředí a kontrolujeme:

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

Hledání podle textu chyby nepřineslo výsledky, avšak při dotazu "mcrouter php" vyšla na prvních místech nejstarší neuzavřená problém projektu — nepodpora binárního protokolu memcached.

NB: Protokol ASCII v memcached je pomalejší než binární a standardní nástroje pro konzistentní hashování klíčů fungují pouze s binárním protokolem. Ale pro konkrétní případ to neznamená žádný problém.

Je hotovo: stačí přepnout na protokol ASCII a vše začne fungovat… Nicméně v tomto případě se zvyklost hledat odpovědi v dokumentaci na php.net se nám vymstila. Správnou odpověď tam nenajdete… pokud se tedy nedostanete na konec, kde v sekci „Uživatelsky přispěné poznámky“ nalezne správná a neudělená odpověď.

Ano, správný název parametru je memcached.sess_binary_protocol. Je třeba jej vypnout, poté začnou sezení fungovat. Zbývá už jen umístit kontejner s mcrouterem do podu s PHP!

Závěr

Takto jsme pouze změnami v infrastruktuře dokázali vyřešit stanovený úkol: problém s odolností memcached byl vyřešen, spolehlivost ukládání cache byla zvýšena. Kromě zřejmých výhod pro aplikaci to poskytlo prostor pro manévrování při práci na platformě: když mají všechny komponenty rezervu, život administrátora se výrazně usnadňuje. Ano, tato metoda má i své nevýhody, může vypadat jako „záplata“, ale pokud šetří peníze, potlačuje problém a nezpůsobuje nové — proč ne?

P.S.

Přečtěte si také náš blog:

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster