Gerenciamento de cluster no Orleans
O Orleans fornece gerenciamento de cluster por meio de um protocolo de associação interno, que às vezes, chamamos de Associação ao cluster. A meta desse protocolo é que todos os silos (servidores do Orleans) entrem em um acordo em relação ao conjunto de silos ativos no momento, detectem os silos com falha e permitam que novos silos ingressem no cluster.
O protocolo depende de um serviço externo para fornecer uma abstração de IMembershipTable. IMembershipTable é uma tabela durável simples que usamos para duas finalidades. Primeiro, ela é usada como um ponto de encontro para que os silos encontrem uns aos outros e para que os clientes do Orleans encontrem silos. Em segundo lugar, ela é usada para armazenar a exibição de associação atual (lista de silos ativos) e ajuda a coordenar o contrato na exibição de associação.
Atualmente, temos seis implementações do IMembershipTable: com base em de Armazenamento de Tabelas do Azure, do Azure Cosmos DB, ADO.NET (PostgreSQL, MySQL/MariaDB, SQL Server, Oracle), Apache ZooKeeper, Cônsul IO, do AWS DynamoDB, do MongoDB, Redis, apache Cassandrae uma implementação na memória para desenvolvimento.
Além da IMembershipTable, cada silo participa de um protocolo de associação ponto a ponto totalmente distribuído que detecta silos com falha e chega a um acordo sobre um conjunto de silos ativos. Descrevemos a implementação interna do protocolo de associação do Orleansabaixo.
O protocolo de associação
Após a inicialização, cada silo adiciona uma entrada para si mesmo em uma tabela compartilhada bem conhecida, usando uma implementação de IMembershipTable. Uma combinação de identidade de silo (
ip:port:epoch
) e ID de implantação de serviço (ID do cluster) é usada como chaves exclusivas na tabela. Época é apenas o tempo em tiques quando esse silo foi iniciado e, como tal,ip:port:epoch
é garantido como exclusivo em determinada implantação do Orleans.Os silos monitoram uns aos outros diretamente, por meio de investigações de aplicação ("você está vivo"
heartbeats
as sondas são enviadas como mensagens diretas de um silo para outro, pelos mesmos soquetes TCP pelos quais os silos se comunicam. Dessa forma, as investigações se correlacionam totalmente com problemas reais de rede e integridade do servidor. Cada silo investiga um conjunto configurável de outros silos. Um silo escolhe quem investigar calculando hashes consistentes na identidade de outros silos, formando um anel virtual de todas as identidades e escolhendo X silos sucessores no anel (essa é uma técnica distribuída bem conhecida chamada hash consistente e é amplamente usada em muitas tabelas de hash distribuídas, como o Chord DHT).Se um silo S não receber Y respostas de investigação de um servidor monitorado P, ele suspeita e grava sua suspeita com carimbo de data/hora na linha de P na IMembershipTable.
Se P tiver mais de Z suspeitas dentro de K segundos, S registrará que P está morto na linha de P e enviará um instantâneo da tabela de associação atual para todos os outros silos. Os silos atualizam a tabela periodicamente, portanto, o instantâneo é uma otimização para reduzir o tempo necessário para que todos os silos aprendam sobre a nova visualização da associação.
Em mais detalhes:
A suspeita é gravada na IMembershipTable, em uma coluna especial na linha correspondente a P. Quando S suspeita de P, ele escreve: "no momento TTT S suspeitou de P".
Uma suspeita não é suficiente para declarar P como morto. Você precisa de Z suspeitas de diferentes silos em uma janela de tempo configurável T, normalmente 3 minutos, para declarar P como morto. A suspeita é gravada usando o controle de simultaneidade otimista fornecido pela IMembershipTable.
O silo suspeito S lê a linha de P.
Se
S
for o último a suspeitar (já houve Z-1 a suspeitar dentro de um período de T, como escrito na coluna suspeita), S decide declarar P como morto. Nesse caso, S adiciona-se à lista de quem já suspeitou e também grava na coluna Status de P que P está morto.Caso contrário, se S não for o último a suspeitar, S apenas se adicionará à coluna de quem suspeitou.
Em ambos os casos, o write-back usa o número de versão ou a ETag que foi lida, ou seja, as atualizações dessa linha são serializadas. Caso a gravação tenha falhado devido à incompatibilidade de versão/ETag, S tenta novamente (ler novamente e tentar gravar, a menos que P já tenha sido marcado como morto).
De maneira geral, essa sequência de "leitura, modificação local, write-back" é uma transação. No entanto, não estamos necessariamente usando transações de armazenamento para fazer isso. O código "Transação" é executado localmente em um servidor e usamos simultaneidade otimista fornecida pela IMembershipTable para garantir o isolamento e a atomicidade.
Cada silo lê periodicamente toda a tabela de associação para sua implantação. Dessa forma, os silos aprendem sobre o ingresso de novos silos e sobre outros silos sendo declarados mortos.
Transmissão de instantâneo: para reduzir a frequência de leituras periódicas da tabela, toda vez que um silo grava na tabela (suspeita, nova entrada, etc.), ele envia um instantâneo do estado atual da tabela para todos os outros silos. Como a tabela de membros é consistente e versionada de forma monótona, cada atualização produz um instantâneo com uma versão única que pode ser compartilhado com segurança. Isso permite a propagação imediata de alterações de membros sem aguardar o ciclo de leitura periódico. A leitura periódica ainda é mantida como um mecanismo de recuperação caso a distribuição de instantâneos falhe.
Exibições de associação ordenada: o protocolo de associação garante que todas as configurações de associação sejam ordenadas de forma totalmente global. Essa ordenação oferece dois benefícios principais:
Conectividade Garantida: Quando um novo silo se junta ao cluster, ele deve validar a conectividade bidirecional com todos os outros silos ativos. Se qualquer silo existente não responder (potencialmente indicando um problema de conectividade de rede), o novo silo não poderá ingressar. Isso garante a conectividade completa entre todos os silos no cluster no momento da inicialização. Consulte a observação sobre IAmAlive abaixo para obter uma exceção no caso de recuperação de desastres.
Atualizações de diretório consistentes: protocolos de nível mais alto, como o diretório distribuído de granularidade, precisam que todos os silos tenham uma visão consistente e monotônica da associação. Isso permite uma resolução mais inteligente de ativações de granularidade duplicadas. Para obter mais detalhes, consulte a documentação sobre diretório de granularidade.
Detalhes de implementação:
O IMembershipTable requer atualizações atômicas para garantir uma ordem total global de alterações:
- As implementações devem atualizar as entradas da tabela (lista de silos) e o número de versão atomicamente
- Isso pode ser feito usando transações de banco de dados (como no SQL Server) ou operações atômicas de comparação e troca usando ETags (como no Armazenamento de Tabelas do Azure)
- O mecanismo específico depende dos recursos do sistema de armazenamento subjacente
Uma linha de versão de associação especial na tabela acompanha as alterações:
- Cada gravação na tabela (suspeitas, declarações de morte, entradas) incrementa esse número de versão.
- Todas as gravações são serializadas por essa linha por meio de atualizações atômicas
- A versão monotonicamente crescente garante uma ordenação total de todas as mudanças de associação.
Quando o silo S atualiza o status do silo P:
- S primeiro lê o estado da tabela mais recente
- Em uma única operação atômica, ela atualiza a linha de P e incrementa o número da versão
- Se a atualização atômica falhar (por exemplo, devido a modificações simultâneas), a operação será tentada novamente com recuo exponencial.
considerações de escalabilidade:
A serialização todas as gravações através da linha de versão pode afetar a escalabilidade devido ao aumento de contenção. O protocolo foi comprovado em produção com até 200 silos, mas pode enfrentar desafios além de mil silos. Para implantações muito grandes, outras partes do Orleans (messaging, grain directory, hosting) permanecem escalonáveis mesmo que as atualizações de associação se tornem um gargalo.
configuração padrão: a configuração padrão foi ajustada manualmente durante o uso de produção no Azure. Por padrão: cada silo é monitorado por outros três silos, duas suspeitas são suficientes para declarar um silo morto, suspeitas apenas dos últimos três minutos (caso contrário, estão desatualizadas). sondas são enviadas a cada dez segundos e você precisaria perder três sondas para suspeitar de um silo.
Automonitoramento: o detector de falhas incorpora ideias da pesquisa Lifeguard da Hashicorp (papel, conversa, blog) para melhorar a estabilidade do cluster durante eventos catastróficos em que uma grande parte do cluster sofre falha parcial. O componente
LocalSiloHealthMonitor
pontua a integridade de cada silo usando várias heurísticas:- Status ativo na tabela de associação
- Nenhuma suspeita de outros silos
- Respostas de investigação bem-sucedidas recentes
- Solicitações de investigação recentes recebidas
- Capacidade de resposta do pool de threads (itens de trabalho em execução dentro de 1 segundo)
- Precisão do temporizador (executando com uma precisão de até 3 segundos em relação ao agendamento)
A pontuação de integridade de um silo afeta seus tempos limite de investigação: silos não íntegros (pontuação 1-8) aumentaram os tempos limite em comparação com silos saudáveis (pontuação 0). Isso tem dois benefícios:
- Dá mais tempo para que as investigações tenham êxito quando a rede ou o sistema estiver sob estresse
- Torna mais provável que silos não íntegros sejam eliminados antes que possam votar incorretamente para eliminar silos íntegros.
Isso é especialmente útil durante cenários como o esgotamento do pool de threads, onde nós lentos podem suspeitar incorretamente de nós íntegros simplesmente por não conseguirem processar as respostas com rapidez suficiente.
Investigação indireta: outro recurso inspirado no Lifeguard que melhora a precisão da detecção de falhas ao reduzir a chance de que um silo comprometido ou particionado declare incorretamente que um silo íntegro está morto. Quando um silo de monitoramento tem duas tentativas de investigação restantes para um silo de destino antes de votar para declará-lo morto, ele realiza uma investigação indireta:
- O silo de monitoramento seleciona aleatoriamente outro silo como intermediário e solicita que ele investigue o alvo
- O intermediário tenta entrar em contato com o silo de destino
- Se o destino não responder dentro do período de tempo limite, o intermediário enviará uma confirmação negativa
- Se o silo de monitoramento receber um reconhecimento negativo do intermediário e o intermediário se declarar íntegro (por meio do automonitoramento, conforme descrito acima), o silo de monitoramento lança um voto para declarar o alvo inativo.
- Com a configuração padrão de dois votos necessários, um reconhecimento negativo de uma investigação indireta conta como os dois votos, permitindo uma declaração mais rápida de silos inativos quando a falha é confirmada por várias perspectivas.
Aplicação de detecção perfeita de falhas: quando o silo é declarado morto na tabela, ele é considerado morto por todos, mesmo que não esteja morto (apenas particionado temporariamente ou mensagens de pulsação tenham sido perdidas). Todo mundo para de se comunicar com ele e, uma vez que descobre que está morto, (lendo seu novo status da tabela) ele comete suicídio e encerra seu processo. Como resultado, deve haver uma infraestrutura pronta para reiniciar o silo como um novo processo (um novo número de época é gerado no início). Quando ele é hospedado no Azure, isso acontece automaticamente. Quando isso não acontecer, outra infraestrutura é necessária, como um Serviço Windows configurado para reiniciar automaticamente em caso de falha ou uma implantação no Kubernetes.
O que acontecerá se a tabela não estiver acessível por algum tempo:
Quando o serviço de armazenamento está indisponível ou há problemas de comunicação com ele, o protocolo Orleans NÃO declara silos como inativos por engano. Os silos operacionais continuarão funcionando sem problemas. No entanto, Orleans não será capaz de declarar um silo inativo (se detectar que algum silo está inativo por meio de investigações sem resposta, ele não será capaz de gravar esse fato na tabela) e também não será capaz de permitir a junção de novos silos. Portanto, a integridade sofrerá, mas não a precisão. O particionamento da tabela nunca fará com que Orleans declare o silo como inativo por engano. Além disso, no caso de uma partição de rede parcial (se alguns silos puderem acessar a tabela e outros não), pode acontecer que Orleans declare um silo inativo como inativo, mas levará algum tempo até que todos os outros silos saibam sobre isso. Portanto, a detecção pode atrasar, mas Orleans nunca declarará um silo como inativo incorretamente devido à indisponibilidade da tabela.
IAmAlive escreve para diagnóstico e recuperação de desastres:
Além das pulsações enviadas entre os silos, cada silo atualiza periodicamente um carimbo de data/hora "Estou vivo" em sua linha na tabela. Isso serve a duas finalidades:
- Para diagnóstico, ele fornece aos administradores do sistema uma maneira simples de verificar a vida útil do cluster e determinar quando um silo estava ativo pela última vez. O carimbo de data/hora normalmente é atualizado a cada 5 minutos.
- Para disaster recovery, se um silo não tiver atualizado seu carimbo de data/hora por um determinado número de períodos (configurado via
NumMissedTableIAmAliveLimit
), ele será ignorado por novos silos durante as verificações de conectividade de inicialização, permitindo que o cluster se recupere de cenários em que os silos caíram sem a realização da limpeza adequada.
Tabela de associação
Como já mencionado, IMembershipTable é usada como um ponto de encontro para que os silos encontrem uns aos outros e os clientes do Orleans encontrem silos, e também ajuda a coordenar o acordo na exibição de associação. O repositório principal Orleans contém implementações para muitos sistemas, como Armazenamento de Tabelas do Azure, Azure Cosmos DB, PostgreSQL, MySQL/MariaDB, SQL Server, Apache ZooKeeper, E/S do Consul, Apache Cassandra, MongoDB, Redis, AWS DynamoDB e uma implementação na memória para desenvolvimento.
Armazenamento de Tabelas do Azure: nessa implementação, usamos a ID de implantação do Azure como chave de partição e a identidade do silo (
ip:port:epoch
) como chave de linha. Juntas, elas garantem uma chave exclusiva para cada silo. Para o controle de simultaneidade, usamos o controle de simultaneidade otimista com base em ETags de Tabela do Azure. Sempre que lemos da tabela, armazenamos a ETag para cada linha de leitura e usamos essa ETag quando tentamos gravar novamente. As ETags são atribuídas e verificadas automaticamente pelo serviço Tabela do Azure em cada gravação. Para transações de várias linhas, utilizamos o suporte a transações em lote fornecidas pela tabela do Azure, o que garante transações serializáveis em linhas com a mesma chave de partição.SQL Server: nesta implementação, a ID de implantação configurada é usada para distinguir entre implantações e quais silos pertencem a quais implantações. A identidade do silo é definida como uma combinação de
deploymentID, ip, port, epoch
nas tabelas e colunas apropriadas. O back-end relacional usa controle de simultaneidade otimista e transações, de forma parecida com o procedimento de uso de ETags na implementação da Tabela do Azure. A implementação relacional espera que o mecanismo de banco de dados gere a ETag usada. No caso do SQL Server, no SQL Server 2000, a ETag gerada é adquirida de uma chamada paraNEWID()
. No SQL Server 2005 e em versões posteriores, ROWVERSION é usado. Orleans e grava ETags relacionais como marcasVARBINARY(16)
opacas e as armazena na memória como cadeias de caracteres codificadas de base64. O Orleans dá suporte a inserções de várias linhas usandoUNION ALL
(para Oracle, incluindo DUAL), que atualmente é usado para inserir dados de estatísticas. A implementação exata e a lógica para SQL Server podem ser vistas em CreateOrleansTables_SqlServer.sql.Apache ZooKeeper: nessa implementação, usamos a ID de implantação configurada como um nó raiz e a identidade do silo (
ip:port@epoch
) como seu nó filho. Juntos, eles garantem um caminho exclusivo para cada silo. Para o controle de simultaneidade, usamos o controle de simultaneidade otimista com base na versão do nó. Sempre que lemos do nó raiz da implantação, armazenamos a versão para cada nó de silo de leitura filho e usamos essa versão quando tentamos gravar novamente. Sempre que os dados de um nó são alterados, o número de versão é aumentado atomicamente pelo serviço ZooKeeper. Para transações de várias linhas, utilizamos o método multi, que garante transações serializáveis em nós de silo com o mesmo nó da ID de implantação pai.Consul IO: usamos o repositório Chave/Valor do Consul para implementar a tabela de associação. Confira Consul-Deployment para obter mais detalhes.
DynamoDB AWS: nessa implementação, usamos a ID de Implantação do cluster como a Chave de Partição e a Identidade de Silo (
ip-port-generation
) como o RangeKey que está fazendo a unidade de registro. A simultaneidade otimista é feita pelo atributoETag
com a realização de gravações condicionais no DynamoDB. A lógica de implementação é bastante semelhante à do Armazenamento de Tabelas do Azure.Apache Cassandra – Nesta implementação, usamos a composição da ID do Serviço e da ID do Cluster como chave de partição, e a identidade do Silo (
ip:port:epoch
) como chave de linha. Juntas, elas garantem uma chave exclusiva para cada linha. Para controle de simultaneidade, usamos o controle otimista com base em uma versão de coluna estática usando uma Transação Leve. Esta coluna de versão é compartilhada para todas as linhas na partição/cluster, portanto, fornece o número de versão incremental consistente para a tabela de associação de cada cluster. Não há transações de várias linhas nesta implementação.Emulação na memória para configuração de desenvolvimento. Usamos um sistema especial de granularidade para essa implementação. Essa granularidade reside em um silo primário designado, que é usado apenas para uma configuração de desenvolvimento. Em qualquer silo primário de uso real de produção, isso não é necessário.
Raciocínio de design
Uma pergunta natural que pode ser feita é por que não depender completamente do Apache ZooKeeper ou etcd para a implementação de associação de cluster, possivelmente pelo uso do suporte pronto do ZooKeeper na associação de grupo com nós efêmeros? Por que nos preocupamos em implementar nosso protocolo de associação? Existiam principalmente três motivos:
Implantação/hospedagem na nuvem:
Zookeeper não é um serviço hospedado. Isso significa que, no ambiente de nuvem do Orleans, os clientes teriam que implantar/executar/gerenciar sua instância de um cluster ZK. Esse é apenas mais um fardo desnecessário, que não queríamos impor aos nossos clientes. Usando a Tabela do Azure, contamos com um serviço gerenciado hospedado, o que torna a vida do nosso cliente muito mais simples. Basicamente, na nuvem, use a nuvem como uma plataforma, não como uma infraestrutura. Por outro lado, na execução local e no gerenciamento dos servidores, contar com o ZK como implementação da opção é uma opção viável da IMembershipTable.
Detecção de falha direta:
ao usar a associação de grupo do ZK com nós efêmeros, a detecção de falha é executada entre os servidores do Orleans (clientes ZK) e os servidores ZK. Isso pode não necessariamente se correlacionar com os problemas de rede reais entre os servidores do Orleans. Nosso desejo era que a detecção de falha refletisse com precisão o estado intracluster da comunicação. Especificamente, em nosso design, se um silo do Orleans não puder se comunicar com a IMembershipTable, ele não será considerado morto e poderá continuar funcionando. Ao contrário disso, se usássemos a associação de grupo do ZK com nós efêmeros, uma desconexão de um servidor ZK poderia fazer com que um silo do Orleans (cliente ZK) fosse declarado morto, mas ele poderia estar vivo e totalmente funcional.
Portabilidade e flexibilidade:
Como parte da filosofia do Orleans, não queremos impor uma forte dependência de alguma tecnologia específica, mas, sim, ter um design flexível em que diferentes componentes possam ser facilmente alternados com diferentes implementações. Essa é exatamente a finalidade a que a abstração IMembershipTable serve.
Propriedades do protocolo de associação
Pode lidar com qualquer número de falhas:
nosso algoritmo pode lidar com qualquer número de falhas (ou seja, f<=n), incluindo a reinicialização completa do cluster. Isso contrasta com soluções "tradicionais" baseadas em Paxos, que exigem um quorum, que geralmente é uma maioria. Vimos em situações de produção quando mais da metade dos silos estavam inativos. Nosso sistema permaneceu funcional; já uma associação baseada em Paxos não seria capaz de fazer progressos.
O tráfego para a tabela é muito leve:
As investigações reais vão diretamente entre os servidores e não para a tabela. Isso geraria muito tráfego e seria menos preciso do ponto de vista da detecção de falhas: se um silo não pudesse chegar à tabela, ele perderia a gravação da sua pulsação eu estou vivo e outros o matariam.
Precisão ajustável X integridade:
embora você não possa ter detecção de falha ao mesmo tempo perfeita e precisa, geralmente se deseja uma capacidade de compensação de precisão (não quero declarar um silo que está vivo como morto) com integridade (deseja declarar morto um silo que está realmente morto o mais rápido possível). Os votos configuráveis para declarar mortes e as investigações perdidas permitem a negociação entre esses dois. Para saber mais, confira Yale University: detectores de falhas de Ciência da Computação.
Dimensionar:
O protocolo pode lidar com milhares e provavelmente até dezenas de milhares de servidores. Isso contrasta com soluções tradicionais baseadas em Paxos, como protocolos de comunicação de grupo, que são conhecidos por não dimensionar além de dezenas.
Diagnóstico:
a tabela também é muito conveniente para diagnóstico e solução de problemas. Os administradores do sistema podem encontrar instantaneamente na tabela a lista atual de silos ativos e ver o histórico de todos os silos mortos e suspeitos. Isso é especialmente útil para diagnosticar problemas.
Por que precisamos de armazenamento persistente confiável para implementação da IMembershipTable:
Usamos o armazenamento persistente para o IMembershipTable para duas finalidades. Primeiro, ela é usada como um ponto de encontro para que os silos encontrem uns aos outros e para que os clientes do Orleans encontrem silos. Em segundo lugar, usamos o armazenamento confiável para nos ajudar a coordenar o contrato na exibição de associação. Embora executemos a detecção de falhas diretamente ponto a ponto entre os silos, armazenamos a exibição de associação no armazenamento confiável e usamos o mecanismo de controle de simultaneidade fornecido por esse armazenamento para chegar a um acordo sobre quem está vivo e quem está morto. Dessa forma, nosso protocolo terceiriza o difícil problema do consenso distribuído para a nuvem. Nisso, utilizamos totalmente o poder da plataforma de nuvem subjacente, usando-a verdadeiramente como PaaS (Plataforma como Serviço).
O IAmAlive direto grava na tabela somente para diagnóstico:
Além das pulsações que são enviadas entre os silos, cada silo também atualiza periodicamente uma coluna "Estou vivo" em sua linha na tabela. Esta coluna "Estou vivo" é usada apenas para solução de problemas e diagnóstico manual e não é usada pelo próprio protocolo de associação. Geralmente, ela é gravada em uma frequência muito menor (uma vez a cada 5 minutos) e serve como uma ferramenta muito útil para os administradores do sistema verificarem a vida útil do cluster ou descobrirem facilmente quando o silo estava vivo pela última vez.
Confirmações
Gostaríamos de reconhecer a contribuição de Alex Kogan para o design e a implementação da primeira versão deste protocolo. Esse trabalho foi feito como parte de um estágio de verão na Microsoft Research no verão de 2011.
A implementação do