Выкарыстоўваны mcrouter для гарызантальнага маштабавання memcached

Выкарыстоўваны mcrouter для гарызантальнага маштабавання memcached

Распрацоўка высоканагружаных праектаў на любой мове патрабуе асаблівага падыходу і прымянення спецыяльных інструментаў, але калі гаворка заходзіць аб прыкладаннях на PHP, сітуацыя можа абвастрыцца настолькі, што даводзіцца распрацоўваць, да прыкладу, уласны сервер прыкладанняў. У дадзенай нататцы гаворка пойдзе пра знаёмую ўсім боль з размеркаваным захоўваннем сесій і кэшаванні дадзеных у memcached і пра тое, як мы вырашалі гэтыя праблемы ў адным «падапечным» праекце.

Вінаваты імпрэзы — дадатак на PHP, якое базуецца на фрэймворке symfony 2.3, абнаўляць які ў планы бізнэсу зусім не ўваходзіць. Апроч цалкам стандартнага захоўвання сесій у гэтым праекце на ўсю моц выкарыстоўвалася палітыка «кэшавання ўсяго» у memcached: адказаў на запыты да БД і API-сервераў, розных сцягоў, блакіровак для сінхранізацыі выканання кода і шмат чаго іншага. У такой сітуацыі паломка memcached становіцца фатальнай для працы прыкладання. У дадатак, страта кэша вядзе да сур'ёзных наступстваў: СКБД пачынае трашчаць па швах, API-сэрвісы – баніць запыты і г.д. Стабілізацыя сітуацыі можа заняць дзясяткі хвілін, а ў гэты час сэрвіс будзе жудасна тармазіць ці зусім стане недаступным.

Нам спатрэбілася забяспечыць магчымасць гарызантальнага маштабавання прыкладання малой крывёй, г.зн. з мінімальнымі зменамі зыходнага кода і поўным захаваннем функцыянальнасці. Зрабіць кэш не толькі ўстойлівым да адмоваў, але і паспрабаваць мінімізаваць страты дадзеных з яго.

Што не так з самім memcached?

Наогул, пашырэнне memcached для PHP "са скрынкі" падтрымлівае размеркаванае захоўванне дадзеных і сесій. Механізм кансістэнтнага хэшавання ключоў дазваляе раўнамерна размяшчаць дадзеныя на шматлікіх серверах, адназначна адрасуючы кожны пэўны ключ вызначанаму серверу з групы, а ўбудаваныя сродкі failover'а забяспечваюць высокую даступнасць сэрвісу кэшавання (але, нажаль, не дадзеных).

З захоўваннем сесій справы ідуць крыху лепш: можна наладзіць memcached.sess_number_of_replicas, у выніку чаго дадзеныя будуць захоўвацца адразу на некалькі сервераў, а ў выпадку адмовы аднаго асобніка memcached дадзеныя будуць аддавацца з іншых. Аднак, калі сервер вернецца ў лад без дадзеных (як звычайна бывае пасля рэстарту), частка ключоў будзе пераразмеркавана ў яго карысць. Фактычна гэта будзе азначаць страту дадзеных сесіі, бо няма магчымасці «схадзіць» у іншую рэпліку ў выпадку промаху.

Стандартныя сродкі бібліятэкі накіраваны, у асноўным, менавіта на гарызантальнае маштабаванне: яны дазваляюць павялічыць кэш да гіганцкіх памераў і забяспечыць доступ да яго з кода, размешчанага на розных серверах. Аднак у нашай сітуацыі аб'ём захоўваемых дадзеных не перавышае некалькіх гігабайт, ды і прадукцыйнасці аднаго-двух вузлоў суцэль хапае. Адпаведна, з карыснага штатныя сродкі маглі б толькі забяспечыць даступнасць memcached пры захаванні хаця б аднаго асобніка кэша ў працоўным стане. Зрэшты, нават гэтай магчымасцю скарыстацца не атрымалася… Тут варта нагадаць пра старажытнасць фрэймворка, скарыстанага ў праекце, з-за чаго прымусіць працаваць прыкладанне з пулам сервераў ніяк не ўдавалася. Не будзем таксама забываць аб стратах дадзеных сесій: ад масавага разлагінвання карыстачоў у замоўца тузалася вока.

У ідэале патрабавалася рэплікацыя запісы ў memcached і абыход рэплік у выпадку промаху ці памылкі. Рэалізаваць гэтую стратэгію нам дапамог mcrouter.

mcrouter

Гэта роўтэр для memcached, распрацаваны кампаніяй Facebook з мэтай вырашэння яе праблем. Ён падтрымлівае тэкставы пратакол memcached, які дазваляе маштабаваць усталёўкі memcached да вар'яцкіх памераў. Падрабязнае апісанне mcrouter можна знайсці ў гэта аб'ява. Між іншым шырокай функцыянальнасці ён можа тое, што трэба нам:

  • рэпліцыраваць запіс;
  • рабіць fallback на іншыя серверы групы ў выпадку ўзнікнення памылкі.

За справу!

Канфігурацыя mcrouter

Перайду адразу да канфігу:

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

Чаму тры пулы? Чаму паўтараюцца сэрвэры? Давайце разбяромся, як гэта працуе.

  • У дадзенай канфігурацыі mcrouter выбірае шлях, па якім будзе дасланы запыт зыходзячы з каманды запыту. Пра гэта яму гаворыць тып OperationSelectorRoute.
  • GET-запыты трапляюць у апрацоўшчык RandomRoute, які выпадковым чынам выбірае пул ці маршрут сярод аб'ектаў масіва children. Кожны элемент гэтага масіва ў сваю чаргу з'яўляецца апрацоўшчыкам MissFailoverRoute, які пройдзе па кожным серверы ў пуле, пакуль не атрымае адказ з дадзенымі, што і будзе вернуты кліенту.
  • Калі б мы выкарыстоўвалі выключна MissFailoverRoute з пулам з трох сервераў, то ўсе запыты прыходзілі б спачатку на першы асобнік memcached, а астатнія атрымлівалі б запыты па рэшткавым прынцыпе, калі дадзеныя адсутнічаюць. Такі падыход прывёў бы да празмернай нагрузцы першага ў спісе сервера, таму было прынята рашэнне генераваць тры пулы з адрасамі ў розных паслядоўнасцях і выбраць іх выпадкова.
  • Усе астатнія запыты (а гэта запіс) апрацоўваюцца з дапамогай AllMajorityRoute. Дадзены апрацоўшчык адпраўляе запыты на ўсе серверы пула і чакае адказаў, прынамсі, ад N/2 + 1 з іх. Ад выкарыстання AllSyncRoute для аперацый запісу прыйшлося адмовіцца, бо дадзены метад патрабуе дадатнага адказу ад ўсіх сервераў групы - у адваротным выпадку ён верне SERVER_ERROR. Хоць пры гэтым mcrouter і складзе дадзеныя ў даступныя кэшы, але якая выклікае функцыя PHP верне памылку і згенеруе notice. AllMajorityRoute не гэтак строгі і дазваляе выводзіць да паловы вузлоў з эксплуатацыі без вышэйапісаных праблем.

Асноўны мінус гэтай схемы ў тым, што калі дадзеных у кэшы сапраўды няма, то на кожны запыт ад кліента фактычна будзе выканана N запытаў да memcached - да ўсім серверам у пуле. Напрыкладольшую хуткасць і меншую нагрузку ад запытаў да адсутных ключоў.

NB: Карыснымі спасылкамі для вывучэння mcrouter могуць таксама апынуцца. дакументацыя ў wiki и issues праекта (у тым ліку і зачыненыя), уяўлялыя цэлая скарбніца розных канфігурацый.

Зборка і запуск 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: 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)

… і накідваем Дыяграма руля. З цікавага - тут толькі генератар канфіга ад колькасці рэплік (Калі ў каго -небудзь ёсць больш лаканічны і элегантны варыянт, падзяліцеся ім у каментарах):

{{- $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)

Выкочваем у тэставае асяроддзе і правяраем:

# 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 у Memcached павольней, чым двайковы, і стандартныя сродкі паслядоўнага клавішы хэшавання толькі з двайковым пратаколам. Але праблемаў для канкрэтнага выпадку гэта не стварае.

Справа ў капелюшы: засталося толькі пераключыцца на ASCII-пратакол і ўсё запрацуе…. Аднак у дадзеным выпадку звычка шукаць адказы ў дакументацыі на php.net згуляла злы жарт. Правільнага адказу вы там не знойдзеце… калі, вядома, не дагартаеце да канца, дзе ў секцыі "User contributed notes" будзе верны і незаслужана замінусаваны адказ.

Так, правільная назва опцыі memcached.sess_binary_protocol. Яе неабходна адключыць, пасля чаго сесіі пачнуць працаваць. Засталося толькі пакласці кантэйнер з mcrouter у pod з PHP!

Заключэнне

Такім чынам, аднымі толькі інфраструктурнымі зменамі нам атрымалася вырашыць пастаўленую задачу: пытанне з адмоваўстойлівасцю memcached вырашана, надзейнасць захоўвання кэша падвышаная. Апроч відавочных плюсаў для прыкладання гэта дало прастору для манеўру пры правядзенні прац над платформай: калі ўсе кампаненты маюць рэзерв, жыццё адміністратара моцна спрашчаецца. Так, гэты метад мае і свае недахопы, можа выглядаць «мыліцай», але калі ён эканоміць грошы, хавае праблему і не выклікае новых - чаму б і не?

PS

Чытайце таксама ў нашым блогу:

Крыніца: habr.com

Дадаць каментар