
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, . 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
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 . Entre outras coisas 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 arraychildren. Cada elemento deste array é, por sua vez, um manipuladorMissFailoverRoute, que percorrerá cada servidor do pool até receber uma resposta com dados, que serão retornados ao cliente. - Se usássemos exclusivamente
MissFailoverRoutecom 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 usoAllSyncRoutepara 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.AllMajorityRoutenã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 и (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 , cuja configuração será semelhante a esta:
NB: As listagens fornecidas no artigo são publicadas no repositório .
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' ]()
... 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 }}
}
}
}
}()
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 ““Em primeiro plano estava o problema não resolvido mais antigo do projeto - 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 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 .
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:
- "Pratique com dapp" (usando symfony-demo como exemplo): и ;
- «".
Fonte: habr.com
