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 silo. 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 parecida com No SQL 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 de IMembershipTable: com base no Armazenamento de Tabelas do Azure, no SQL Server, no Apache ZooKeeper, no Consult IO, no DynamoDB AWS e na emulaçã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. Começamos descrevendo a implementação interna do protocolo de associação do Orleans abaixo e, posteriormente, descrevemos a implementação do IMembershipTable.

O protocolo de associação básico

  1. 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 é 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.

  2. Os silos monitoram uns aos outros diretamente, por meio de pings de aplicativo ("você está vivo"). heartbeats Os pings são enviados como mensagens diretas do silo para o silo, pelos mesmos soquetes TCP nos quais os silos se comunicam. Dessa forma, os pings se correlacionam totalmente com problemas reais de rede e integridade do servidor. Cada silo executa ping em um conjunto configurável de outros silos. Um silo escolhe em quem executar ping 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).

  3. Se um silo S não receber Y respostas de ping de servidor monitorado P, ele suspeita e grava sua suspeita com carimbo de data/hora na linha de P na IMembershipTable.

  4. Se P tiver mais de Z suspeitas em K segundos, S gravará que P está morto na linha de P e transmitirá uma solicitação para que todos os silos releiam a tabela de associação (o que eles farão de qualquer maneira periodicamente).

  5. Em mais detalhes:

    1. 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".

    2. 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.

    3. O silo suspeito S lê a linha de P.

    4. 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.

    5. Caso contrário, se S não for o último a suspeitar, S apenas se adicionará à coluna de quem suspeitou.

    6. 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).

    7. De maneira geral, essa sequência de "leitura, modificação local, write-back" é uma transação. No entanto, não estamos 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.

  6. 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.

  7. Configuração: fornecemos uma configuração padrão, que foi ajustada manualmente durante nosso uso de produção no Azure. Atualmente, o padrão é: cada silo é monitorado por três outros silos, duas suspeitas são suficientes para declarar um silo morto, suspeitas apenas dos últimos três minutos (caso contrário, eles estão desatualizados). Os pings são enviados a cada dez segundos e você precisa perder três pings para suspeitar de um silo.

  8. Impondo detecção de Falha Perfeita: é teoricamente possível que um silo seja declarado morto se perder comunicação com outros silos, embora o próprio processo do silo ainda esteja em execução. Para resolver esse problema depois que 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 não, outra infraestrutura é necessária. Por exemplo, um Serviço do Windows configurado para reiniciar automaticamente em caso de falha ou implantação de kubernetes.

  9. Otimização para reduzir a frequência de leituras de tabela periódicas e acelerar todos os silos aprendendo sobre ingresso de novos silos e silos mortos. Sempre que um silo grava alguma coisa na tabela com êxito (suspeita, novo ingresso e outros), ele também transmite a todos os outros silos: “vá reler a tabela agora”. O silo NÃO informa aos outros o que escreveu na tabela (já que essas informações podem estar desatualizadas/erradas), apenas diz a eles para relerem a tabela. Dessa forma, aprendemos muito rapidamente sobre as mudanças de associação sem a necessidade de aguardar o ciclo de leitura periódico completo. Ainda precisamos da leitura periódica, caso a mensagem "releia a tabela" seja perdida.

Propriedades do protocolo de associação básico

  1. 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.

  2. O tráfego para a tabela é muito leve:

    os pings 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.

  3. 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 os pings perdidos permitem a negociação entre esses dois. Para saber mais, confira Yale University: detectores de falhas de Ciência da Computação.

  4. Dimensionar:

    o protocolo básico 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.

  5. 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.

  6. Por que precisamos de armazenamento persistente confiável para implementação da IMembershipTable:

    Usamos armazenamento persistente (tabela do Azure, SQL Server, DynamoDB AWS, Apache ZooKeeper ou Consul IO KV) para IMembershipTable por dois motivos. 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).

  7. 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 pings 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.

  8. 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.

Extensão para ordenar exibições de associação

O protocolo de associação básico descrito acima foi posteriormente estendido para dar suporte a exibições de associação ordenadas. Descreveremos brevemente os motivos dessa extensão e como ela é implementada. A extensão não altera nada no design acima, apenas adiciona a propriedade de que todas as configurações de associação são totalmente ordenadas globalmente.

Por que é útil ordenar exibições de associação?

  • Isso permite serializar o ingresso de novos silos ao cluster. Dessa forma, quando um novo silo ingressa no cluster, ele pode validar a conectividade bidirecional com todos os outros silos que já foram iniciados. Se alguns dos silos já ingressados não responderem (potencialmente indicando um problema de conectividade de rede com o novo silo), o novo silo não poderá ingressar. Isso garante que, pelo menos quando um silo for iniciado, haja conectividade total entre todos os silos no cluster (isso é implementado).

  • Protocolos de nível superior no silo, como o diretório de granularidade distribuída, podem utilizar o fato de que as exibições de associação são ordenadas e usar essas informações para executar uma resolução de ativações duplicadas mais inteligente. Mais especificamente, quando o diretório descobre que duas ativações foram criadas quando a associação estava em fluxo, ele pode decidir desativar a ativação mais antiga que foi criada com base nas informações de associação agora desatualizadas.

Protocolo de associação estendido:

  1. para a implementação desse recurso, utilizamos o suporte a transações em várias linhas fornecidas pela MembershipTable.

  2. Adicionamos uma linha de versão de associação à tabela que rastreia as alterações na tabela.

  3. Quando o silo S deseja gravar declaração de suspeita ou morte para silo P:

    1. S lê o conteúdo da tabela mais recente. Se P já estiver morto, ele não faz nada. Do contrário,
    2. Na mesma transação, gravar as alterações na linha de P, incrementa o número de versão e grava-o novamente na tabela.
    3. Ambas as gravações são condicionadas com ETags.
    4. Se a transação for anulada devido à incompatibilidade de ETag na linha de P ou na linha de versão, ele tenta novamente.
  4. Todas as gravações na tabela modificam e incrementam a linha de versão. Dessa forma, todas as gravações na tabela são serializadas (por meio da serialização das atualizações da linha de versão) e, como os silos incrementam apenas o número de versão, as gravações também são totalmente ordenadas em ordem crescente.

Escalabilidade do protocolo de associação estendido:

na versão estendida do protocolo, todas as gravações são serializadas por meio de uma linha. Isso pode prejudicar potencialmente a escalabilidade do protocolo de gerenciamento de cluster, pois aumenta o risco de conflitos entre gravações de tabela simultâneas. Para atenuar parcialmente esse problema, os silos tentam novamente todas as gravações na tabela usando a retirada exponencial. Observamos que os protocolos estendidos funcionam sem problemas em um ambiente de produção no Azure com até 200 silos. No entanto, achamos que o protocolo pode ter problemas para escalar além de mil silos. Em configurações tão grandes, as atualizações da linha de versão podem ser facilmente desabilitadas, essencialmente mantendo o restante do protocolo de gerenciamento de cluster e desistindo da propriedade de ordenação total. Observe também que nos referimos aqui à escalabilidade do protocolo de gerenciamento de cluster, não ao restante do Orleans. Acreditamos que outras partes do runtime do Orleans (mensagens, diretório distribuído, hospedagem de granularidade, conectividade cliente para gateway) são escalonáveis muito além de centenas de silos.

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. Atualmente, temos seis implementações da IMembershipTable: com base na Tabela do Azure, no SQL Server, no Apache ZooKeeper, no Consul IO, no DynamoDB AWS e na emulação na memória para desenvolvimento.

  1. 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.

  2. 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 para NEWID(). No SQL Server 2005 e em versões posteriores, ROWVERSION é usado. Orleans e grava ETags relacionais como marcas VARBINARY(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 usando UNION 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.

  3. 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.

  4. Consul IO: usamos o repositório Chave/Valor do Consul para implementar a tabela de associação. Confira Consul-Deployment para obter mais detalhes.

  5. 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 atributo ETag com a realização de gravações condicionais no DynamoDB. A lógica de implementação é bastante semelhante à do Armazenamento de Tabelas do Azure.

  6. Emulação na memória para configuração de desenvolvimento. Usamos um sistema especial de granularidade, chamado MembershipTableGrain, 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.

Configuração

O protocolo de associação é configurado por meio do elemento Liveness na seção Globals no arquivo OrleansConfiguration.xml. Os valores padrão foram ajustados em anos de uso de produção no Azure e acreditamos que eles representam boas configurações padrão. Não é necessário, em geral, alterá-los.

Elemento de configuração de exemplo:

<Liveness ProbeTimeout="5s"
    TableRefreshTimeout="10s"
    DeathVoteExpirationTimeout="80s"
    NumMissedProbesLimit="3"
    NumProbedSilos="3"
    NumVotesForDeathDeclaration="2" />

Há quatro tipos de atividade implementados. O tipo do protocolo de atividade é configurado por meio do atributo SystemStoreType do elemento SystemStore na seção Globals no arquivo OrleansConfiguration.xml.

  1. MembershipTableGrain: a tabela de associação é armazenada em uma granularidade no silo primário. Essa é apenas uma configuração de desenvolvimento.
  2. AzureTable: a tabela de associação é armazenada na tabela do Azure.
  3. SqlServer: a tabela de associação é armazenada em um banco de dados relacional.
  4. ZooKeeper: a tabela de associação é armazenada em um conjunto ZooKeeper.
  5. Consul: configurado como repositório de sistema personalizado com MembershipTableAssembly = "OrleansConsulUtils". Confira Consul-Deployment para obter mais detalhes.
  6. DynamoDB: configurado como um repositório de sistema personalizado com MembershipTableAssembly = "OrleansAWSUtils".

Para todos os tipos de atividade, as variáveis de configuração comuns são definidas no elemento Globals.Liveness:

  1. ProbeTimeout: o número de segundos para sondar outros silos a fim de verificar sua atividade ou para que o silo envie mensagens de pulsação "Estou vivo" sobre si mesmo. O padrão é 10 segundos.
  2. TableRefreshTimeout: o número de segundos para buscar atualizações da tabela de associação. O padrão é 60 segundos.
  3. DeathVoteExpirationTimeout: tempo de expiração em segundos do voto de morte na tabela de associação. O padrão é 120 segundos.
  4. NumMissedProbesLimit: o número de mensagens de pulsação "Estou vivo" perdidas de um silo ou número de investigações não respondidas que levam a suspeitar que esse silo está morto. O padrão é 3.
  5. NumProbedSilos: o número de silos que cada silo investiga em relação à atividade. O padrão é 3.
  6. NumVotesForDeathDeclaration: o número de votos não expirados necessários para declarar algum silo como morto (deve ser no máximo NumMissedProbesLimit). O padrão é 2.
  7. UseLivenessGossip: se deve-se usar a otimização de fofocas para acelerar a distribuição de informações de atividade. O padrão é true.
  8. IAmAliveTablePublishTimeout: o número de segundos a serem gravados periodicamente na tabela de associação em que esse silo está ativo. Útil somente para diagnóstico. O padrão é de 5 minutos.
  9. NumMissedTableIAmAliveLimit: o número de atualizações de "estou vivo" perdidas na tabela de um silo que faz com que um aviso seja registrado. Não afeta o protocolo de atividade. O padrão é 2.
  10. MaxJoinAttemptTime: o número de segundos para tentar ingressar em um cluster de silos antes de desistir. O padrão é de 5 minutos.
  11. ExpectedClusterSize: o tamanho esperado de um cluster. Não precisa ser muito preciso, pode ser superestimado. Usado para ajustar o algoritmo de retirada exponencial de tentativas de gravação na tabela do Azure. O padrão é 20.

Raciocínio de design

Uma pergunta natural que pode ser feita é por que não depender completamente do Apache ZooKeeper para a implementação de associação de cluster, possivelmente pelo uso do suporte pronto para uso 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:

  1. Implantação/hospedagem na nuvem:

    o Zookeeper não é um serviço hospedado (pelo menos no momento em que este texto foi escrito em julho de 2015 e, definitivamente, quando implementamos esse protocolo pela primeira vez no verão de 2011, não havia nenhuma versão do Zookeeper em execução como um serviço hospedado por qualquer grande provedor de nuvem). 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.

  2. 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.

  3. 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.

Confirmações

Gostaríamos de reconhecer a contribuição de Alex Kogan para o design e a implementação da primeira versão desse 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 de IMembershipTable baseada em ZooKeeper foi feita por Shay Hazor, a implementação da IMembershipTable SQL foi feita pelo Veikko Eeva, a implementação da IMembershipTable DynamoDB AWS foi feita por Gutemberg Ribeiro e a implementação da IMembershipTable baseada em Consul foi feita por Paul North.