Usamos o mcrouter para escalabilidade horizontal do memcached

Usamos o mcrouter para escalabilidade horizontal do memcached

O desenvolvimento de projetos altamente escaláveis em qualquer linguagem requer uma abordagem especial e a aplicação de ferramentas específicas, mas quando se trata de aplicações em PHP, a situação pode se agravar a ponto de ser necessário desenvolver, por exemplo, um servidor de aplicativos próprio. Neste artigo, discutiremos a dor familiar de armazenar sessões de maneira distribuída e o cache de dados no memcached, e como resolvemos esses problemas em um projeto 'sob nossa tutela'.

O protagonista é uma aplicação em PHP, baseada no framework symfony 2.3, cuja atualização não está nos planos de negócios. Além do armazenamento de sessões bastante padrão, este projeto usava amplamente a política de 'cache de tudo' no memcached: respostas a solicitações de bancos de dados e servidores de API, vários flags, bloqueios para sincronização de execução de código e muito mais. Nessa situação, a quebra do memcached se torna fatal para o funcionamento da aplicação. Além disso, a perda de cache leva a sérias consequências: o SGBD começa a apresentar falhas, serviços de API bloqueiam solicitações, e assim por diante. A estabilização da situação pode levar dezenas de minutos, e durante esse tempo o serviço ficará extremamente lento ou até mesmo se tornará inacessível.

Precisávamos garantir a possibilidade de escalabilidade horizontal da aplicação com o mínimo de esforço, ou seja, com alterações mínimas no código-fonte e total preservação da funcionalidade. Fazer com que o cache não apenas fosse resistente a falhas, mas também tentar minimizar a perda de dados dele.

O que está errado com o próprio memcached?

De maneira geral, a extensão do memcached para PHP 'pronta para uso' suporta armazenamento distribuído de dados e sessões. O mecanismo de hashing consistente de chaves permite distribuir dados uniformemente entre vários servidores, endereçando cada chave específica a um servidor determinado dentro do grupo, e os recursos embutidos de failover garantem alta disponibilidade do serviço de cache (mas, infelizmente, não dos dados).

. Com o armazenamento de sessões, as coisas são um pouco melhores: é possível configurar memcached.sess_number_of_replicas, onde os dados serão armazenados simultaneamente em vários servidores, e em caso de falha de uma instância do memcached, os dados serão fornecidos por outras. No entanto, se o servidor retornar ao funcionamento sem dados (como geralmente acontece após um reinício), parte das chaves será redistribuída em seu favor. Isso significará perda dos dados da sessão, já que não há como

As ferramentas padrão da biblioteca são direcionadas, principalmente, para a escalabilidade : elas permitem expandir o cache para tamanhos gigantescos e garantir acesso ao código hospedado em diferentes servidores. No entanto, na nossa situação, o volume de dados armazenados não ultrapassa alguns gigabytes, e a performance de uma ou duas instâncias é mais do que suficiente. Assim, as ferramentas disponíveis poderiam apenas garantir a disponibilidade do memcached mantendo pelo menos uma instância do cache em funcionamento. No entanto, nem mesmo essa possibilidade foi aproveitada... É importante lembrar da antiguidade do framework utilizado no projeto, que dificultou a operação do aplicativo em um pool de servidores. E não podemos esquecer da perda dos dados das sessões: o cliente ficou com os olhos atentos devido ao logout em massa dos usuários.

Idealmente, precisaríamos da replicação da gravação no memcached e da bypassing das réplicas em caso de falha ou erro. Para implementar essa estratégia, contamos com mcrouter.

mcrouter

É um roteador para memcached, desenvolvido pelo Facebook para resolver seus problemas. Ele suporta o protocolo de texto do memcached, que permite escalar instalações de memcached para tamanhos impressionantes. Uma descrição detalhada do mcrouter pode ser encontrada em este anúncio. Além de outras amplas funcionalidades , ele pode fazer exatamente o que precisamos:

  • replicar gravações;
  • fazer fallback para outros servidores do grupo em caso de erro.

Vamos lá!

Configuração do mcrouter

Vou direto para a configuração:

{
 "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 três pools? Por que há servidores repetidos? Vamos entender como isso funciona.

  • Nesta configuração, o mcrouter escolhe o caminho pelo qual a requisição será enviada com base no comando de solicitação. Isso é informado por meio do tipo OperationSelectorRoute.
  • As requisições GET vão para o manipulador RandomRoute, que escolhe aleatoriamente um pool ou rota entre os objetos do array children. Cada elemento desse array, por sua vez, é um manipulador MissFailoverRoute, que percorre cada servidor no pool até receber uma resposta com dados, que será retornada ao cliente.
  • Se estivéssemos usando exclusivamente MissFailoverRoute um pool com três servidores, todas as requisições iriam primeiro para a primeira instância do memcached, enquanto as outras receberiam requisições a posteriori, quando os dados estivessem ausentes. Essa abordagem levaria a uma carga excessiva no primeiro servidor da lista, portanto, decidiu-se gerar três pools com endereços em sequências diferentes e escolhê-los aleatoriamente.
  • Todas as outras requisições (ou seja, de gravação) são processadas através do AllMajorityRoute. Este manipulador envia requisições para todos os servidores do pool e aguarda respostas de pelo menos N/2 + 1 deles. O uso de AllSyncRoute para operações de gravação teve que ser abandonado, pois esse método requer uma resposta positiva dos todas servidores do grupo — caso contrário, ele retornará SERVER_ERROR. Embora neste caso o mcrouter armazene os dados nos caches disponíveis, a função PHP que chamou retornará um erro e gerará um aviso. AllMajorityRoute não é tão rigoroso e permite a exclusão de até metade dos nós sem os problemas descritos acima.

A principal desvantagem desse esquema é que se os dados realmente não estão no cache, para cada requisição do cliente efetivamente seria executado N requisições ao memcached — para todos os servidores no pool. Podemos reduzir o número de servidores nos pools, por exemplo, para dois: ao sacrificar a confiabilidade do armazenamento, obteremos ummaiormaior velocidade e menor carga de solicitações para chaves ausentes.

NB: Links úteis para aprender sobre mcrouter também podem incluir documentação no wiki e questões do projeto (incluindo as fechadas), representando um verdadeiro tesouro de várias configurações.

Compilação e execução do mcrouter

O aplicativo (e o próprio memcached) funciona no Kubernetes - portanto, mcrouter também está lá. Para compilação do contêiner utilizamos werf, cuja configuração será semelhante a seguinte:

NB: Os listagens fornecidos no artigo estão publicados no repositório 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: Instalar pré-requisitos
   apt:
     name: [ 'apt-transport-https', 'tzdata', 'locales' ]
     update_cache: yes
 - name: Adicionar chave APT do mcrouter
   apt_key:
     url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
 - name: Adicionar repositório do mcrouter
   apt_repository:
     repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
     filename: mcrouter
     update_cache: yes
 - name: Definir fuso horário
   timezone:
     name: "Europe/Moscow"
 - name: Garantir que uma localidade exista
   locale_gen:
     name: en_US.UTF-8
     state: present
 install:
 - name: Instalar mcrouter
   apt:
     name: [ 'mcrouter' ]

(werf.yaml)

… e esboçando Chart Helm. De interessante - aqui está apenas o gerador de configuração baseado na quantidade de réplicas (se alguém tiver uma versão mais concisa e elegante - compartilhe nos comentários):

{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- 
/* Preenchendo o array com duas cópias dos servidores: "0 1 2 0 1 2" */ -}}
{{- range until 2 -}}
 {{- range $i, $_ := until $count -}}
 {{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
 {{- end -}}
{{- end -}}
{{- 
/* Deslocando pelo array, obtemos N segmentos: "[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|poold" $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)

Implantamos no ambiente de teste e verificamos:

# 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 pesquisa pelo texto do erro não deu resultado, mas pela consulta “mcrouter php” apareceu no topo um problema antigo e não resolvido do projeto - falta de suporte ao protocolo binário do memcached.

NB: O protocolo ASCII no memcached é mais lento do que o binário, e as ferramentas padrão de hash consistente de chaves funcionam apenas com o protocolo binário. Mas isso não cria problemas para o caso específico.

É isso aí: basta mudar para o protocolo ASCII e tudo funcionará... No entanto, neste caso, o hábito de procurar respostas na documentação do php.net brincou uma má peça. A resposta correta não será encontrada lá... a menos que você role até o final, onde na seção «Notas contribuintes de usuários» haverá uma resposta correta e injustamente negativa..

Sim, o nome correto da opção é memcached.sess_binary_protocol. É necessário desabilitá-la, após o que as sessões começarão a funcionar. Basta colocar o contêiner com mcrouter no pod com PHP!

Conclusão

Assim, com apenas mudanças na infraestrutura, conseguimos resolver a tarefa proposta: a questão da tolerância a falhas do memcached foi resolvida, e a confiabilidade do armazenamento em cache foi aumentada. Além dos benefícios óbvios para o aplicativo, isso deu espaço para manobras durante os trabalhos na plataforma: quando todos os componentes têm reserva, a vida do administrador se torna muito mais fácil. Sim, esse método tem suas desvantagens, pode parecer um “gambi” (gambiarra), mas se economiza dinheiro, oculta o problema e não gera novos — por que não?

P.S.

Confira também em nosso blog:

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster