
Распрацоўка высоканагружаных праектаў на любой мове патрабуе асаблівага падыходу і прымянення спецыяльных інструментаў, але калі гаворка заходзіць аб прыкладаннях на PHP, сітуацыя можа абвастрыцца настолькі, што даводзіцца распрацоўваць, да прыкладу, . У дадзенай нататцы гаворка пойдзе пра знаёмую ўсім боль з размеркаваным захоўваннем сесій і кэшаванні дадзеных у memcached і пра тое, як мы вырашалі гэтыя праблемы ў адным «падапечным» праекце.
Вінаваты імпрэзы — дадатак на PHP, якое базуецца на фрэймворке symfony 2.3, абнаўляць які ў планы бізнэсу зусім не ўваходзіць. Апроч цалкам стандартнага захоўвання сесій у гэтым праекце на ўсю моц выкарыстоўвалася палітыка «кэшавання ўсяго» у memcached: адказаў на запыты да БД і API-сервераў, розных сцягоў, блакіровак для сінхранізацыі выканання кода і шмат чаго іншага. У такой сітуацыі паломка memcached становіцца фатальнай для працы прыкладання. У дадатак, страта кэша вядзе да сур'ёзных наступстваў: СКБД пачынае трашчаць па швах, API-сэрвісы – баніць запыты і г.д. Стабілізацыя сітуацыі можа заняць дзясяткі хвілін, а ў гэты час сэрвіс будзе жудасна тармазіць ці зусім стане недаступным.
Нам спатрэбілася забяспечыць магчымасць гарызантальнага маштабавання прыкладання малой крывёй, г.зн. з мінімальнымі зменамі зыходнага кода і поўным захаваннем функцыянальнасці. Зрабіць кэш не толькі ўстойлівым да адмоваў, але і паспрабаваць мінімізаваць страты дадзеных з яго.
Што не так з самім memcached?
Наогул, пашырэнне memcached для PHP "са скрынкі" падтрымлівае размеркаванае захоўванне дадзеных і сесій. Механізм кансістэнтнага хэшавання ключоў дазваляе раўнамерна размяшчаць дадзеныя на шматлікіх серверах, адназначна адрасуючы кожны пэўны ключ вызначанаму серверу з групы, а ўбудаваныя сродкі failover'а забяспечваюць высокую даступнасць сэрвісу кэшавання (але, нажаль, не дадзеных).
З захоўваннем сесій справы ідуць крыху лепш: можна наладзіць memcached.sess_number_of_replicas, у выніку чаго дадзеныя будуць захоўвацца адразу на некалькі сервераў, а ў выпадку адмовы аднаго асобніка memcached дадзеныя будуць аддавацца з іншых. Аднак, калі сервер вернецца ў лад без дадзеных (як звычайна бывае пасля рэстарту), частка ключоў будзе пераразмеркавана ў яго карысць. Фактычна гэта будзе азначаць страту дадзеных сесіі, бо няма магчымасці «схадзіць» у іншую рэпліку ў выпадку промаху.
Стандартныя сродкі бібліятэкі накіраваны, у асноўным, менавіта на гарызантальнае маштабаванне: яны дазваляюць павялічыць кэш да гіганцкіх памераў і забяспечыць доступ да яго з кода, размешчанага на розных серверах. Аднак у нашай сітуацыі аб'ём захоўваемых дадзеных не перавышае некалькіх гігабайт, ды і прадукцыйнасці аднаго-двух вузлоў суцэль хапае. Адпаведна, з карыснага штатныя сродкі маглі б толькі забяспечыць даступнасць memcached пры захаванні хаця б аднаго асобніка кэша ў працоўным стане. Зрэшты, нават гэтай магчымасцю скарыстацца не атрымалася… Тут варта нагадаць пра старажытнасць фрэймворка, скарыстанага ў праекце, з-за чаго прымусіць працаваць прыкладанне з пулам сервераў ніяк не ўдавалася. Не будзем таксама забываць аб стратах дадзеных сесій: ад масавага разлагінвання карыстачоў у замоўца тузалася вока.
У ідэале патрабавалася рэплікацыя запісы ў memcached і абыход рэплік у выпадку промаху ці памылкі. Рэалізаваць гэтую стратэгію нам дапамог .
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 могуць таксама апынуцца. и (у тым ліку і зачыненыя), уяўлялыя цэлая скарбніца розных канфігурацый.
Зборка і запуск mcrouter
Дадатак (і сам memcached) у нас працуе ў Kubernetes - адпаведна, там жа месца і mcrouter. Для зборкі кантэйнера мы выкарыстоўваем , канфіг для якога будзе выглядаць наступным чынам:
NB: Лістынгі, прыведзеныя ў артыкуле, апублікаваны ў рэпазітары. .
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' ]()
… і накідваем Дыяграма руля. З цікавага - тут толькі генератар канфіга ад колькасці рэплік (Калі ў каго -небудзь ёсць больш лаканічны і элегантны варыянт, падзяліцеся ім у каментарах):
{{- $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 }}
}
}
}
}()
Выкочваем у тэставае асяроддзе і правяраем:
# 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 >Пошук па тэксце памылкі выніку не даў, аднак на запыт» у першых шэрагах значылася найстарэйшая незакрытая праблема праекта бінарнага пратакола memcached.
NB: Пратакол ASCII у Memcached павольней, чым двайковы, і стандартныя сродкі паслядоўнага клавішы хэшавання толькі з двайковым пратаколам. Але праблемаў для канкрэтнага выпадку гэта не стварае.
Справа ў капелюшы: засталося толькі пераключыцца на ASCII-пратакол і ўсё запрацуе…. Аднак у дадзеным выпадку звычка шукаць адказы ў згуляла злы жарт. Правільнага адказу вы там не знойдзеце… калі, вядома, не дагартаеце да канца, дзе ў секцыі "User contributed notes" будзе верны і .
Так, правільная назва опцыі memcached.sess_binary_protocol. Яе неабходна адключыць, пасля чаго сесіі пачнуць працаваць. Засталося толькі пакласці кантэйнер з mcrouter у pod з PHP!
Заключэнне
Такім чынам, аднымі толькі інфраструктурнымі зменамі нам атрымалася вырашыць пастаўленую задачу: пытанне з адмоваўстойлівасцю memcached вырашана, надзейнасць захоўвання кэша падвышаная. Апроч відавочных плюсаў для прыкладання гэта дало прастору для манеўру пры правядзенні прац над платформай: калі ўсе кампаненты маюць рэзерв, жыццё адміністратара моцна спрашчаецца. Так, гэты метад мае і свае недахопы, можа выглядаць «мыліцай», але калі ён эканоміць грошы, хавае праблему і не выклікае новых - чаму б і не?
PS
Чытайце таксама ў нашым блогу:
- "Практыка з dapp" (на прыкладзе symfony-demo): и ;
- «.
Крыніца: habr.com
