Usando o mcrouter para dimensionar o memcached horizontalmente

Usando o mcrouter para dimensionar o memcached horizontalmente

Desenvolver projetos de alta carga em qualquer linguagem requer uma abordagem especial e o uso de ferramentas especiais, mas quando se trata de aplicações em PHP a situação pode ficar tão agravada que você precisa desenvolver, por exemplo, próprio servidor de aplicativos. Nesta nota falaremos sobre a dor familiar do armazenamento de sessões distribuídas e do cache de dados no memcached e como resolvemos esses problemas em um projeto “ward”.

O herói da ocasião é um aplicativo PHP baseado no framework symfony 2.3, que não está incluído nos planos de negócios para atualização. Além do armazenamento de sessão bastante padrão, este projeto fez uso total de Política de "armazenamento em cache de tudo" no memcached: respostas a solicitações ao banco de dados e servidores API, diversos flags, bloqueios para sincronização de execução de código e muito mais. Em tal situação, uma falha no memcached torna-se fatal para o funcionamento do aplicativo. Além disso, a perda de cache leva a consequências graves: o SGBD começa a estourar, os serviços de API começam a proibir solicitações, etc. A estabilização da situação pode levar dezenas de minutos e, durante esse período, o serviço ficará terrivelmente lento ou completamente indisponível.

Precisávamos fornecer a capacidade de dimensionar horizontalmente o aplicativo com pouco esforço, ou seja com alterações mínimas no código-fonte e funcionalidade total preservada. Torne o cache não apenas resistente a falhas, mas também tente minimizar a perda de dados dele.

O que há de errado com o próprio memcached?

Em geral, a extensão memcached para PHP oferece suporte imediato para dados distribuídos e armazenamento de sessão. O mecanismo de hashing de chave consistente permite que você coloque dados uniformemente em muitos servidores, endereçando exclusivamente cada chave específica para um servidor específico do grupo, e ferramentas de failover integradas garantem alta disponibilidade do serviço de cache (mas, infelizmente, sem dados).

As coisas ficam um pouco melhores com o armazenamento de sessão: você pode configurar memcached.sess_number_of_replicas, como resultado os dados serão armazenados em vários servidores ao mesmo tempo e, em caso de falha de uma instância do memcached, os dados serão transferidos de outras. No entanto, se o servidor voltar a ficar online sem dados (como normalmente acontece após uma reinicialização), algumas das chaves serão redistribuídas a seu favor. Na verdade isso significará perda de dados da sessão, já que não há como “ir” para outra réplica em caso de erro.

As ferramentas de biblioteca padrão destinam-se principalmente a horizontal escalonamento: eles permitem aumentar o cache para tamanhos gigantescos e fornecer acesso a ele a partir de código hospedado em diferentes servidores. Porém, em nossa situação, o volume de dados armazenados não ultrapassa vários gigabytes, e o desempenho de um ou dois nós é suficiente. Conseqüentemente, as únicas ferramentas padrão úteis poderiam ser garantir a disponibilidade do memcached enquanto mantém pelo menos uma instância de cache em condições de funcionamento. Porém, não foi possível aproveitar nem mesmo esta oportunidade... Vale aqui lembrar a antiguidade do framework utilizado no projeto, por isso foi impossível fazer a aplicação funcionar com um pool de servidores. Não vamos esquecer também a perda de dados da sessão: os olhos do cliente tremeram com o logout massivo dos usuários.

Idealmente, era necessário replicação de registros no memcached e ignorando réplicas em caso de erro ou erro. Ajudou-nos a implementar esta estratégia mcrouter.

mcrouter

Este é um roteador memcached desenvolvido pelo Facebook para resolver seus problemas. Ele suporta o protocolo de texto memcached, que permite dimensionar instalações memcached em proporções insanas. Uma descrição detalhada do mcrouter pode ser encontrada em este anúncio. Entre outras coisas ampla funcionalidade ele pode fazer o que precisamos:

  • replicar registro;
  • faça fallback para outros servidores do grupo se ocorrer um erro.

Pela causa!

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 piscinas? Por que os servidores são repetidos? Vamos descobrir como isso funciona.

  • Nesta configuração, o mcrouter seleciona o caminho para o qual a solicitação será enviada com base no comando request. O cara conta isso para ele OperationSelectorRoute.
  • Solicitações GET vão para o manipulador RandomRouteque seleciona aleatoriamente um pool ou rota entre objetos de array children. Cada elemento deste array é, por sua vez, um manipulador MissFailoverRoute, que percorrerá cada servidor do pool até receber uma resposta com dados, que serão retornados ao cliente.
  • Se usássemos exclusivamente MissFailoverRoute com um pool de três servidores, todas as solicitações chegariam primeiro à primeira instância do memcached e o restante receberia solicitações de forma residual quando não houvesse dados. Tal abordagem levaria a carga excessiva no primeiro servidor da lista, optou-se então por gerar três pools com endereços em sequências diferentes e selecioná-los aleatoriamente.
  • Todas as outras solicitações (e este é um registro) são processadas usando AllMajorityRoute. Este manipulador envia solicitações a todos os servidores do pool e aguarda respostas de pelo menos N/2 + 1 deles. De uso AllSyncRoute para operações de gravação teve que ser abandonado, uma vez que este método requer uma resposta positiva do todos servidores do grupo - caso contrário, ele retornará SERVER_ERROR. Embora o mcrouter adicione os dados aos caches disponíveis, a função de chamada do PHP retornará um erro e irá gerar aviso. AllMajorityRoute não é tão rigoroso e permite que até metade das unidades sejam retiradas de serviço sem os problemas descritos acima.

Principal menos Este esquema é que, se realmente não houver dados no cache, então, para cada solicitação do cliente, N solicitações ao memcached serão realmente executadas - para para todos servidores no pool. Podemos reduzir o número de servidores em pools, por exemplo, para dois: sacrificando a confiabilidade do armazenamento, obtemosоmaior velocidade e menos carga de solicitações de chaves ausentes.

NB: Você também pode encontrar links úteis para aprender mcrouter documentação no wiki и problemas do projeto (inclusive fechados), representando todo um armazém de diversas configurações.

Construindo e executando o mcrouter

Nosso aplicativo (e o próprio memcached) é executado no Kubernetes - portanto, o mcrouter também está localizado lá. Para montagem de contêiner nós usamos bem, cuja configuração será semelhante a esta:

NB: As listagens fornecidas no artigo são publicadas 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: 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 esboce Gráfico do leme. O interessante é que só existe um gerador de configuração baseado no número de réplicas (se alguém tiver uma opção mais lacônica 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 -}}
{{- /* Заполняем  массив двумя копиями серверов: "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)

Nós o implementamos 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 busca pelo texto do erro não deu nenhum resultado, mas sim pela consulta “mcrouter php“Em primeiro plano estava o problema não resolvido mais antigo do projeto - falta de suporte protocolo binário memcached.

NB: O protocolo ASCII no memcached é mais lento que o binário, e os meios padrão de hashing de chave consistente funcionam apenas com o protocolo binário. Mas isto não cria problemas para um caso específico.

O truque está na bolsa: basta mudar para o protocolo ASCII e tudo funcionará.... Porém, neste caso, o hábito de procurar respostas em documentação em php.net fez uma piada cruel. Você não encontrará a resposta correta lá... a menos, é claro, que você role até o final, onde na seção "Notas contribuídas pelo usuário" será fiel e resposta injustamente rejeitada.

Sim, o nome correto da opção é memcached.sess_binary_protocol. Deve ser desabilitado, após o que as sessões começarão a funcionar. Só falta colocar o container com mcrouter em um pod com PHP!

Conclusão

Assim, apenas com mudanças de infraestrutura conseguimos resolver o problema: o problema com a tolerância a falhas do memcached foi resolvido e a confiabilidade do armazenamento em cache foi aumentada. Além das vantagens óbvias para o aplicativo, isso deu margem de manobra no trabalho na plataforma: quando todos os componentes têm reserva, a vida do administrador fica bastante simplificada. Sim, este método também tem os seus inconvenientes, pode parecer uma “muleta”, mas se poupa dinheiro, enterra o problema e não provoca novos - porque não?

PS

Leia também em nosso blog:

Fonte: habr.com

Adicionar um comentário