Използване на mcrouter за хоризонтално мащабиране на memcached

Използване на mcrouter за хоризонтално мащабиране на memcached

Разработването на високонатоварени проекти на който и да е език изисква специален подход и използването на специални инструменти, но когато става въпрос за приложения на PHP, ситуацията може да стане толкова влошена, че да трябва да разработите, напр. собствен сървър за приложения. В тази бележка ще говорим за познатата болка с разпределеното съхранение на сесии и кеширането на данни в memcached и как решихме тези проблеми в един „отделен“ проект.

Героят на събитието е PHP приложение, базирано на рамката symfony 2.3, което изобщо не е включено в бизнес плановете за актуализиране. В допълнение към съвсем стандартното съхранение на сесии, този проект използва пълноценно политика за „кеширане на всичко“. в memcached: отговори на заявки към базата данни и API сървъри, различни флагове, ключалки за синхронизиране на изпълнението на код и много други. В такава ситуация повредата на memcached става фатална за работата на приложението. Освен това загубата на кеш води до сериозни последици: СУБД започва да се пука по шевовете, API услугите започват да забраняват заявки и т.н. Стабилизирането на ситуацията може да отнеме десетки минути, като през това време услугата ще бъде ужасно бавна или напълно недостъпна.

Трябваше да осигурим способността за хоризонтално мащабиране на приложението с малко усилия, т.е. с минимални промени в изходния код и запазена пълна функционалност. Направете кеша не само устойчив на повреди, но и се опитайте да сведете до минимум загубата на данни от него.

Какво не е наред със самия memcached?

Като цяло разширението memcached за PHP поддържа разпределени данни и съхранение на сесии веднага. Механизмът за последователно хеширане на ключове ви позволява равномерно да поставяте данни на много сървъри, адресирайки уникално всеки конкретен ключ към конкретен сървър от групата, а вградените инструменти за преодоляване на отказ осигуряват висока наличност на услугата за кеширане (но, за съжаление, няма данни).

Нещата са малко по-добри със съхранението на сесии: можете да конфигурирате memcached.sess_number_of_replicas, в резултат на което данните ще се съхраняват на няколко сървъра едновременно, а в случай на повреда на една memcached инстанция, данните ще бъдат прехвърлени от други. Въпреки това, ако сървърът се върне онлайн без данни (както обикновено се случва след рестартиране), някои от ключовете ще бъдат преразпределени в негова полза. Всъщност това ще означава загуба на данни от сесията, тъй като няма начин да „отидете“ на друга реплика в случай на пропуск.

Стандартните библиотечни инструменти са насочени основно към хоризонтални мащабиране: те ви позволяват да увеличите кеша до гигантски размери и да осигурите достъп до него от код, хостван на различни сървъри. В нашата ситуация обаче обемът на съхраняваните данни не надвишава няколко гигабайта и производителността на един или два възела е напълно достатъчна. Съответно, единствените полезни стандартни инструменти биха могли да бъдат да се гарантира наличността на memcached, като същевременно се поддържа поне един екземпляр на кеша в работно състояние. Въпреки това не беше възможно да се възползваме дори от тази възможност... Тук си струва да припомним древността на рамката, използвана в проекта, поради което беше невъзможно приложението да работи с набор от сървъри. Да не забравяме и загубата на данни от сесията: окото на клиента потрепна от масовото излизане на потребители.

В идеалния случай се изискваше репликация на записи в memcached и заобикалящи реплики в случай на грешка или грешка. Помогна ни да реализираме тази стратегия mcrouter.

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 документация в wiki и проблеми на проекта (включително затворени), представляващи цял склад от различни конфигурации.

Изграждане и стартиране на 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 >

Търсенето на текста на грешката не даде никакви резултати, но за заявката „microuter php„На преден план беше най-старият нерешен проблем на проекта – липса на подкрепа memcached двоичен протокол.

NB: ASCII протоколът в memcached е по-бавен от двоичния и стандартните средства за последователно хеширане на ключ работят само с двоичния протокол. Но това не създава проблеми за конкретен случай.

Номерът е в чантата: всичко, което трябва да направите, е да преминете към ASCII протокола и всичко ще работи.... В този случай обаче навикът да се търсят отговори в документация на php.net изиграха жестока шега. Няма да намерите правилния отговор там... освен ако, разбира се, не превъртите до края, където в секцията „Бележки, предоставени от потребителя“ ще бъде верен и несправедливо отрицателен отговор.

Да, правилното име на опцията е memcached.sess_binary_protocol. Той трябва да бъде деактивиран, след което сесиите ще започнат да работят. Всичко, което остава, е да поставите контейнера с mcrouter в под с PHP!

Заключение

По този начин, само с инфраструктурни промени успяхме да разрешим проблема: проблемът с толерантността към грешки в memcached беше разрешен и надеждността на съхранението на кеша беше увеличена. В допълнение към очевидните предимства за приложението, това даде възможност за маневриране при работа на платформата: когато всички компоненти имат резерв, животът на администратора е значително опростен. Да, този метод също има своите недостатъци, може да изглежда като "патерица", но ако спестява пари, заравя проблема и не създава нови - защо не?

PS

Прочетете също в нашия блог:

Източник: www.habr.com

Добавяне на нов коментар