Usando mcrouter para escalar o memcached horizontalmente

Usando mcrouter para escalar o memcached horizontalmente

Desenvolver proxectos de alta carga en calquera idioma require un enfoque especial e o uso de ferramentas especiais, pero cando se trata de aplicacións en PHP, a situación pode agravarse tanto que hai que desenvolver, por exemplo, servidor de aplicacións propio. Nesta nota falaremos sobre a dor familiar co almacenamento de sesións distribuídas e o almacenamento en caché de datos en memcached e sobre como resolvemos estes problemas nun proxecto "ward".

O heroe da ocasión é unha aplicación PHP baseada no framework symfony 2.3, que non está en absoluto incluída nos plans de negocio a actualizar. Ademais do almacenamento de sesións bastante estándar, este proxecto fixo un uso completo de política de "almacenar todo en caché". en memcached: respostas ás solicitudes á base de datos e aos servidores de API, varias marcas, bloqueos para sincronizar a execución de código e moito máis. Nesta situación, unha avaría de memcached vólvese fatal para o funcionamento da aplicación. Ademais, a perda da caché leva a graves consecuencias: o DBMS comeza a explotar, os servizos da API comezan a prohibir as solicitudes, etc. Estabilizar a situación pode levar decenas de minutos, e durante este tempo o servizo será terriblemente lento ou completamente non dispoñible.

Necesitabamos proporcionar a capacidade de escalar horizontalmente a aplicación con pouco esforzo, é dicir. con cambios mínimos no código fonte e a funcionalidade completa preservada. Fai que a caché non só sexa resistente aos fallos, senón que tamén intente minimizar a perda de datos.

Que hai de malo co propio memcached?

En xeral, a extensión memcached para PHP admite datos distribuídos e almacenamento de sesións fóra da caixa. O mecanismo para o hash de clave coherente permítelle colocar datos uniformemente en moitos servidores, dirixindo de forma única cada chave específica a un servidor específico do grupo, e as ferramentas de conmutación por fallo integradas garanten unha alta dispoñibilidade do servizo de caché (pero, desafortunadamente, ningún dato).

As cousas son un pouco mellor co almacenamento da sesión: podes configurar memcached.sess_number_of_replicas, como resultado do cal os datos almacenaranse en varios servidores á vez, e no caso de producirse un fallo dunha instancia de memcache, os datos transferiranse doutros. Non obstante, se o servidor volve estar en liña sen datos (como adoita suceder despois dun reinicio), algunhas das claves serán redistribuídas ao seu favor. De feito isto significará perda de datos da sesión, xa que non hai forma de "ir" a outra réplica en caso de fallar.

As ferramentas de biblioteca estándar están dirixidas principalmente a horizontal escalado: permítenche aumentar a caché a tamaños xigantescos e proporcionar acceso a ela desde código aloxado en diferentes servidores. Non obstante, na nosa situación, o volume de datos almacenados non supera varios gigabytes e o rendemento dun ou dous nodos é suficiente. En consecuencia, as únicas ferramentas estándar útiles poderían ser garantir a dispoñibilidade de memcached mantendo polo menos unha instancia de caché en condicións de funcionamento. Porén, non foi posible aproveitar nin sequera esta oportunidade... Aquí convén lembrar a antigüidade do framework empregado no proxecto, polo que foi imposible que a aplicación funcionara cun pool de servidores. Non esquezamos tampouco a perda de datos da sesión: o ollo do cliente torceu a causa da saída masiva dos usuarios.

O ideal era necesario replicación de rexistros en memcached e omisión de réplicas en caso de erro ou erro. Axudounos a implementar esta estratexia mcrouter.

mcrouter

Este é un enrutador memcached desenvolvido por Facebook para resolver os seus problemas. Soporta o protocolo de texto memcached, que permite instalacións de memcache a escala a proporcións dementes. Pódese atopar unha descrición detallada de mcrouter en este anuncio. Entre outras cousas ampla funcionalidade pode facer o que necesitamos:

  • rexistro replicado;
  • faga unha alternativa a outros servidores do grupo se se produce un erro.

Ponse ao traballo!

configuración mcrouter

Vou directamente á configuración:

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

Por que tres piscinas? Por que se repiten os servidores? Imos descubrir como funciona.

  • Nesta configuración, mcrouter selecciona o camiño ao que se enviará a solicitude en función do comando de solicitude. O tipo dille isto OperationSelectorRoute.
  • As solicitudes GET van ao controlador RandomRouteque selecciona aleatoriamente un grupo ou ruta entre os obxectos da matriz children. Cada elemento desta matriz é á súa vez un controlador MissFailoverRoute, que pasará por cada servidor do pool ata que reciba unha resposta con datos, que serán devoltos ao cliente.
  • Se usamos exclusivamente MissFailoverRoute cun grupo de tres servidores, todas as solicitudes chegarían primeiro á primeira instancia de memcache e o resto recibirían as solicitudes de forma residual cando non haxa datos. Tal enfoque levaría a carga excesiva no primeiro servidor da lista, polo que se decidiu xerar tres pools con enderezos en secuencias diferentes e seleccionalos aleatoriamente.
  • Todas as demais solicitudes (e este é un rexistro) son procesadas mediante AllMajorityRoute. Este controlador envía solicitudes a todos os servidores do grupo e espera respostas de polo menos N/2 + 1 deles. Do uso AllSyncRoute para as operacións de escritura houbo que abandonar, xa que este método require unha resposta positiva de Todo servidores do grupo; se non, volverá SERVER_ERROR. Aínda que mcrouter engadirá os datos ás cachés dispoñibles, a función de chamada PHP devolverá un erro e xerará aviso. AllMajorityRoute non é tan estrito e permite que ata a metade das unidades se poñan fóra de servizo sen os problemas descritos anteriormente.

Principal desvantaxe Este esquema é que se realmente non hai datos na caché, entón por cada solicitude do cliente executaranse N solicitudes a memcached - para para todos servidores na piscina. Podemos reducir o número de servidores en pools, por exemplo, a dous: sacrificar a fiabilidade do almacenamento, conseguimosоmaior velocidade e menos carga das solicitudes ás chaves que faltan.

NB: Tamén podes atopar ligazóns útiles para aprender mcrouter documentación en wiki и cuestións do proxecto (incluíndo os pechados), que representan todo un almacén de varias configuracións.

Construír e executar mcrouter

A nosa aplicación (e o propio memcached) execútase en Kubernetes; polo tanto, mcrouter tamén se atopa alí. Para montaxe de recipientes usamos werf, cuxa configuración será así:

NB: As listaxes que aparecen no artigo publícanse no repositorio 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)

... e esbozao Gráfico de timón. O interesante é que só hai un xerador de configuración baseado no número de réplicas (se alguén ten unha opción máis lacónica e elegante, compártala nos comentarios):

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

Lanzámolo ao ambiente de proba e comprobamos:

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

A busca do texto do erro non deu ningún resultado, pero a consulta "microuter php"En primeiro plano estaba o problema máis antigo sen resolver do proxecto - falta de apoio protocolo binario memcached.

NB: O protocolo ASCII en memcached é máis lento que o binario, e os medios estándar de hash de clave consistente só funcionan co protocolo binario. Pero isto non crea problemas para un caso específico.

O truco está na bolsa: só tes que cambiar ao protocolo ASCII e todo funcionará.... Non obstante, neste caso, o costume de buscar respostas en documentación en php.net fixo unha broma cruel. Non atoparás alí a resposta correcta... a non ser que, por suposto, te despraces ata o final, onde está na sección "Notas aportadas polo usuario" será fiel e resposta inxustamente negativa.

Si, o nome da opción correcta é memcached.sess_binary_protocol. Debe estar desactivado, despois de que as sesións comezarán a funcionar. Todo o que queda é poñer o contedor con mcrouter nun pod con PHP.

Conclusión

Así, con só cambios na infraestrutura puidemos resolver o problema: o problema coa tolerancia a fallos de memcached resolveuse e aumentou a fiabilidade do almacenamento da caché. Ademais das vantaxes obvias para a aplicación, isto deu marxe de manobra cando se traballa na plataforma: cando todos os compoñentes teñen unha reserva, a vida do administrador simplifícase moito. Si, este método tamén ten os seus inconvenientes, pode parecer unha "muleta", pero se aforra diñeiro, enterra o problema e non causa outros novos, por que non?

PS

Lea tamén no noso blog:

Fonte: www.habr.com

Engadir un comentario