S'utilitza mcrouter per escalar el memcached horitzontalment

S'utilitza mcrouter per escalar el memcached horitzontalment

El desenvolupament de projectes de gran càrrega en qualsevol idioma requereix un enfocament especial i l'ús d'eines especials, però quan es tracta d'aplicacions en PHP, la situació pot agreujar-se tant que cal desenvolupar, per exemple, propi servidor d'aplicacions. En aquesta nota parlarem del dolor familiar amb l'emmagatzematge de sessions distribuïts i la memòria cau de dades a memcached i com hem resolt aquests problemes en un projecte "de sala".

L'heroi de l'ocasió és una aplicació PHP basada en el framework symfony 2.3, que no està gens inclosa en els plans de negoci a actualitzar. A més de l'emmagatzematge de sessions bastant estàndard, aquest projecte va fer un ús complet política "emmagatzemar-ho tot a la memòria cau". a memcached: respostes a les sol·licituds a la base de dades i als servidors d'API, diversos indicadors, bloquejos per a sincronitzar l'execució de codi i molt més. En aquesta situació, una avaria de memcached esdevé fatal per al funcionament de l'aplicació. A més, la pèrdua de memòria cau comporta greus conseqüències: el DBMS comença a esclatar, els serveis de l'API comencen a prohibir les sol·licituds, etc. Estabilitzar la situació pot trigar desenes de minuts, i durant aquest temps el servei serà terriblement lent o completament indisponible.

Havíem de proporcionar la capacitat d'escalar horitzontalment l'aplicació amb poc esforç, és a dir amb canvis mínims al codi font i la funcionalitat completa conservada. Feu que la memòria cau no només sigui resistent als errors, sinó que també intenteu minimitzar la pèrdua de dades.

Què hi ha de dolent amb memcached?

En general, l'extensió memcached per a PHP admet dades distribuïdes i emmagatzematge de sessions fora de la caixa. El mecanisme per a la codificació coherent de clau us permet col·locar dades de manera uniforme en molts servidors, adreçant de manera única cada clau específica a un servidor específic del grup, i les eines de migració per error integrades garanteixen una alta disponibilitat del servei de memòria cau (però, malauradament, no hi ha dades).

Les coses són una mica millor amb l'emmagatzematge de la sessió: podeu configurar-lo memcached.sess_number_of_replicas, com a conseqüència de la qual cosa les dades s'emmagatzemaran en diversos servidors alhora, i en cas de fallada d'una instància de memcache, les dades es transferiran d'altres. Tanmateix, si el servidor torna a estar en línia sense dades (com sol passar després d'un reinici), algunes de les claus es redistribuiran al seu favor. De fet això significarà pèrdua de dades de sessió, ja que no hi ha manera d'"anar" a una altra rèplica en cas de fallar.

Les eines de biblioteca estàndard estan destinades principalment a horitzontal escalat: permeten augmentar la memòria cau a mides gegantines i proporcionar-hi accés des del codi allotjat en diferents servidors. Tanmateix, en la nostra situació, el volum de dades emmagatzemades no supera diversos gigabytes i el rendiment d'un o dos nodes és suficient. En conseqüència, les úniques eines estàndard útils podrien ser garantir la disponibilitat de memcached mantenint almenys una instància de memòria cau en condicions de funcionament. Tanmateix, no s'ha pogut aprofitar ni tan sols aquesta oportunitat... Aquí val la pena recordar l'antiguitat del framework utilitzat en el projecte, motiu pel qual era impossible que l'aplicació funcionés amb un conjunt de servidors. Tampoc ens oblidem de la pèrdua de dades de sessió: l'ull del client es va contraure per la sortida massiva dels usuaris.

Idealment era necessari replicació de registres en memcache i rèpliques de salt en cas d'error o error. Ens va ajudar a implementar aquesta estratègia mcrouter.

mcrouter

Aquest és un encaminador memcached desenvolupat per Facebook per resoldre els seus problemes. Admet el protocol de text memcached, que permet instal·lacions de memcache a escala a proporcions boges. Es pot trobar una descripció detallada de mcrouter a aquest anunci. Entre altres coses àmplia funcionalitat pot fer el que necessitem:

  • rèplica de registre;
  • feu un retorn a altres servidors del grup si es produeix un error.

Posa't al cap!

configuració de mcrouter

Aniré directament a la configuració:

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

Per què tres piscines? Per què es repeteixen els servidors? Anem a esbrinar com funciona.

  • En aquesta configuració, mcrouter selecciona el camí al qual s'enviarà la sol·licitud en funció de l'ordre de petició. El noi li diu això OperationSelectorRoute.
  • Les sol·licituds GET van al gestor RandomRouteque selecciona aleatòriament un grup o ruta entre objectes de matriu children. Cada element d'aquesta matriu és al seu torn un controlador MissFailoverRoute, que passarà per cada servidor de la piscina fins a rebre una resposta amb dades, que seran retornades al client.
  • Si fem servir exclusivament MissFailoverRoute amb un grup de tres servidors, llavors totes les sol·licituds arribarien primer a la primera instància de memcache, i la resta rebrien sol·licituds de manera residual quan no hi ha dades. Aquest enfocament portaria a càrrega excessiva al primer servidor de la llista, per això es va decidir generar tres pools amb adreces en diferents seqüències i seleccionar-los aleatòriament.
  • Totes les altres sol·licituds (i això és un registre) es processen mitjançant AllMajorityRoute. Aquest gestor envia sol·licituds a tots els servidors del grup i espera respostes d'almenys N/2 + 1 d'ells. De l'ús AllSyncRoute per a les operacions d'escriptura es va haver d'abandonar, ja que aquest mètode requereix una resposta positiva de Tot servidors del grup; en cas contrari, tornarà SERVER_ERROR. Tot i que mcrouter afegirà les dades a la memòria cau disponibles, la funció PHP de trucada retornarà un error i generarà un avís. AllMajorityRoute no és tan estricte i permet posar fora de servei fins a la meitat de les unitats sense els problemes descrits anteriorment.

El principal desavantatge Aquest esquema és que si realment no hi ha dades a la memòria cau, per a cada sol·licitud del client s'executaran realment N sol·licituds a memcached - per tothom servidors a la piscina. Podem reduir el nombre de servidors a les agrupacions, per exemple, a dos: sacrificant la fiabilitat de l'emmagatzematge, aconseguimоmés velocitat i menys càrrega de sol·licituds a claus que falten.

NB: També podeu trobar enllaços útils per aprendre mcrouter documentació a la wiki и qüestions del projecte (incloses les tancades), que representen tot un magatzem de diverses configuracions.

Construir i executar mcrouter

La nostra aplicació (i el mateix memcached) s'executa a Kubernetes; per tant, mcrouter també es troba allà. Per muntatge de contenidors fem servir werf, la configuració de la qual serà així:

NB: Els llistats que es donen a l'article es publiquen al repositori 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)

... i dibuixa-ho Quadre de timó. L'interessant és que només hi ha un generador de configuració basat en el nombre de rèpliques (si algú té una opció més lacònica i elegant, que la compartiu als comentaris):

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

El despleguem a l'entorn de prova i comprovem:

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

La cerca del text de l'error no va donar cap resultat, però la consulta "microuter php"Al capdavant estava el problema més antic sense resoldre del projecte: manca de suport protocol binari memcached.

NB: El protocol ASCII a memcached és més lent que el binari, i els mitjans estàndard de hash de clau coherent només funcionen amb el protocol binari. Però això no crea problemes per a un cas concret.

El truc està a la bossa: només cal canviar al protocol ASCII i tot funcionarà.... Tanmateix, en aquest cas, l'hàbit de buscar respostes en documentació a php.net va fer una broma cruel. No hi trobareu la resposta correcta... tret que, per descomptat, us desplaceu fins al final, on a la secció "Notes aportades per l'usuari" serà fidel i resposta injustament negativa.

Sí, el nom correcte de l'opció és memcached.sess_binary_protocol. S'ha de desactivar, després de la qual cosa les sessions començaran a funcionar. Només queda posar el contenidor amb mcrouter en un pod amb PHP!

Conclusió

Així, només amb canvis d'infraestructura vam poder resoldre el problema: s'ha resolt el problema amb la tolerància a errors de memcached i s'ha augmentat la fiabilitat de l'emmagatzematge de la memòria cau. A més dels avantatges evidents per a l'aplicació, això donava marge de maniobra a l'hora de treballar a la plataforma: quan tots els components tenen una reserva, la vida de l'administrador es simplifica enormement. Sí, aquest mètode també té els seus inconvenients, pot semblar una "muleta", però si estalvia diners, enterra el problema i no en provoca de nous, per què no?

PS

Llegeix també al nostre blog:

Font: www.habr.com

Afegeix comentari