
Разработването на високонатоварени проекти на който и да е език изисква специален подход и използването на специални инструменти, но когато става въпрос за приложения на PHP, ситуацията може да стане толкова влошена, че да трябва да разработите, напр. . В тази бележка ще говорим за познатата болка с разпределеното съхранение на сесии и кеширането на данни в memcached и как решихме тези проблеми в един „отделен“ проект.
Героят на събитието е PHP приложение, базирано на рамката symfony 2.3, което изобщо не е включено в бизнес плановете за актуализиране. В допълнение към съвсем стандартното съхранение на сесии, този проект използва пълноценно политика за „кеширане на всичко“. в memcached: отговори на заявки към базата данни и API сървъри, различни флагове, ключалки за синхронизиране на изпълнението на код и много други. В такава ситуация повредата на memcached става фатална за работата на приложението. Освен това загубата на кеш води до сериозни последици: СУБД започва да се пука по шевовете, API услугите започват да забраняват заявки и т.н. Стабилизирането на ситуацията може да отнеме десетки минути, като през това време услугата ще бъде ужасно бавна или напълно недостъпна.
Трябваше да осигурим способността за хоризонтално мащабиране на приложението с малко усилия, т.е. с минимални промени в изходния код и запазена пълна функционалност. Направете кеша не само устойчив на повреди, но и се опитайте да сведете до минимум загубата на данни от него.
Какво не е наред със самия memcached?
Като цяло разширението memcached за PHP поддържа разпределени данни и съхранение на сесии веднага. Механизмът за последователно хеширане на ключове ви позволява равномерно да поставяте данни на много сървъри, адресирайки уникално всеки конкретен ключ към конкретен сървър от групата, а вградените инструменти за преодоляване на отказ осигуряват висока наличност на услугата за кеширане (но, за съжаление, няма данни).
Нещата са малко по-добри със съхранението на сесии: можете да конфигурирате memcached.sess_number_of_replicas, в резултат на което данните ще се съхраняват на няколко сървъра едновременно, а в случай на повреда на една memcached инстанция, данните ще бъдат прехвърлени от други. Въпреки това, ако сървърът се върне онлайн без данни (както обикновено се случва след рестартиране), някои от ключовете ще бъдат преразпределени в негова полза. Всъщност това ще означава загуба на данни от сесията, тъй като няма начин да „отидете“ на друга реплика в случай на пропуск.
Стандартните библиотечни инструменти са насочени основно към хоризонтални мащабиране: те ви позволяват да увеличите кеша до гигантски размери и да осигурите достъп до него от код, хостван на различни сървъри. В нашата ситуация обаче обемът на съхраняваните данни не надвишава няколко гигабайта и производителността на един или два възела е напълно достатъчна. Съответно, единствените полезни стандартни инструменти биха могли да бъдат да се гарантира наличността на memcached, като същевременно се поддържа поне един екземпляр на кеша в работно състояние. Въпреки това не беше възможно да се възползваме дори от тази възможност... Тук си струва да припомним древността на рамката, използвана в проекта, поради което беше невъзможно приложението да работи с набор от сървъри. Да не забравяме и загубата на данни от сесията: окото на клиента потрепна от масовото излизане на потребители.
В идеалния случай се изискваше репликация на записи в memcached и заобикалящи реплики в случай на грешка или грешка. Помогна ни да реализираме тази стратегия .
mcrouter
Това е memcached рутер, разработен от Facebook за решаване на неговите проблеми. Поддържа текстовия протокол memcached, който позволява мащабиране на memcached инсталации до безумни размери. Подробно описание на mcrouter можете да намерите в . Наред с други неща може да направи това, от което се нуждаем:
- репликиран запис;
- направете резервно връщане към други сървъри в групата, ако възникне грешка.
Захващам се за работа!
конфигурация на 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 функция ще върне грешка и ще генерира известие.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 протокола и всичко ще работи.... В този случай обаче навикът да се търсят отговори в изиграха жестока шега. Няма да намерите правилния отговор там... освен ако, разбира се, не превъртите до края, където в секцията „Бележки, предоставени от потребителя“ ще бъде верен и .
Да, правилното име на опцията е memcached.sess_binary_protocol. Той трябва да бъде деактивиран, след което сесиите ще започнат да работят. Всичко, което остава, е да поставите контейнера с mcrouter в под с PHP!
Заключение
По този начин, само с инфраструктурни промени успяхме да разрешим проблема: проблемът с толерантността към грешки в memcached беше разрешен и надеждността на съхранението на кеша беше увеличена. В допълнение към очевидните предимства за приложението, това даде възможност за маневриране при работа на платформата: когато всички компоненти имат резерв, животът на администратора е значително опростен. Да, този метод също има своите недостатъци, може да изглежда като "патерица", но ако спестява пари, заравя проблема и не създава нови - защо не?
PS
Прочетете също в нашия блог:
- „Практика с dapp“ (използвайки symfony-demo като пример): и ;
- «".
Източник: www.habr.com
