Serviços de partição fiáveis do Service Fabric

Este artigo fornece uma introdução aos conceitos básicos de particionamento de serviços confiáveis do Azure Service Fabric. O particionamento permite o armazenamento de dados nas máquinas locais para que os dados e a computação possam ser dimensionados juntos.

Gorjeta

Um exemplo completo do código neste artigo está disponível no GitHub.

Criação de partições

O particionamento não é exclusivo do Service Fabric. Na verdade, é um padrão central de construção de serviços escaláveis. Em um sentido mais amplo, podemos pensar no particionamento como um conceito de divisão de estado (dados) e computação em unidades acessíveis menores para melhorar a escalabilidade e o desempenho. Uma forma bem conhecida de particionamento é o particionamento de dados, também conhecido como fragmentação.

Serviços sem estado do Partition Service Fabric

Para serviços sem monitoração de estado, você pode pensar que uma partição é uma unidade lógica que contém uma ou mais instâncias de um serviço. A Figura 1 mostra um serviço sem estado com cinco instâncias distribuídas em um cluster usando uma partição.

Serviço sem estado

Existem realmente dois tipos de soluções de serviço sem estado. O primeiro é um serviço que persiste seu estado externamente, por exemplo, em um banco de dados no Banco de Dados SQL do Azure (como um site que armazena as informações e os dados da sessão). O segundo são serviços somente de computação (como uma calculadora ou miniatura de imagem) que não gerenciam nenhum estado persistente.

Em ambos os casos, o particionamento de um serviço sem estado é um cenário muito raro - a escalabilidade e a disponibilidade são normalmente alcançadas pela adição de mais instâncias. A única vez que você deseja considerar várias partições para instâncias de serviço sem monitoração de estado é quando precisa atender a solicitações de roteamento especiais.

Como exemplo, considere um caso em que os usuários com IDs em um determinado intervalo só devem ser atendidos por uma instância de serviço específica. Outro exemplo de quando você pode particionar um serviço sem estado é quando você tem um back-end verdadeiramente particionado (por exemplo, um banco de dados fragmentado no Banco de dados SQL) e deseja controlar qual instância de serviço deve gravar no fragmento de banco de dados - ou executar outro trabalho de preparação dentro do serviço sem estado que requer as mesmas informações de particionamento usadas no back-end. Esses tipos de cenários também podem ser resolvidos de maneiras diferentes e não exigem necessariamente particionamento de serviço.

O restante deste passo a passo se concentra em serviços com monitoração de estado.

Serviços com estado do Partition Service Fabric

O Service Fabric facilita o desenvolvimento de serviços com monitoração de estado escaláveis, oferecendo uma maneira de primeira classe de particionar o estado (dados). Conceitualmente, você pode pensar em uma partição de um serviço stateful como uma unidade de escala altamente confiável por meio de réplicas distribuídas e balanceadas entre os nós de um cluster.

O particionamento no contexto dos serviços com estado do Service Fabric refere-se ao processo de determinar que uma determinada partição de serviço é responsável por uma parte do estado completo do serviço. (Como mencionado anteriormente, uma partição é um conjunto de réplicas). Uma grande coisa sobre o Service Fabric é que ele coloca as partições em nós diferentes. Isso permite que eles cresçam até o limite de recursos de um nó. À medida que as necessidades de dados aumentam, as partições crescem e o Service Fabric reequilibra as partições entre nós. Isso garante o uso eficiente contínuo dos recursos de hardware.

Para dar um exemplo, digamos que você comece com um cluster de 5 nós e um serviço configurado para ter 10 partições e um destino de três réplicas. Nesse caso, o Service Fabric equilibraria e distribuiria as réplicas pelo cluster - e você acabaria com duas réplicas primárias por nó. Se agora você precisar expandir o cluster para 10 nós, o Service Fabric reequilibrará as réplicas primárias em todos os 10 nós. Da mesma forma, se você reduzir para 5 nós, o Service Fabric reequilibrará todas as réplicas nos 5 nós.

A Figura 2 mostra a distribuição de 10 partições antes e depois do dimensionamento do cluster.

Serviço com estado

Como resultado, a expansão é alcançada uma vez que as solicitações de clientes são distribuídas entre computadores, o desempenho geral do aplicativo é melhorado e a contenção no acesso a partes de dados é reduzida.

Planejar o particionamento

Antes de implementar um serviço, você deve sempre considerar a estratégia de particionamento necessária para dimensionar. Existem diferentes maneiras, mas todas elas se concentram no que o aplicativo precisa alcançar. Para o contexto deste artigo, vamos considerar alguns dos aspetos mais importantes.

Uma boa abordagem é pensar na estrutura do Estado que precisa ser dividida, como o primeiro passo.

Vamos dar um exemplo simples. Se você fosse criar um serviço para uma pesquisa em todo o condado, você poderia criar uma partição para cada cidade do condado. Então, você poderia armazenar os votos para cada pessoa na cidade na partição que corresponde a essa cidade. A Figura 3 ilustra um conjunto de pessoas e a cidade em que residem.

Partição simples

Como a população das cidades varia muito, você pode acabar com algumas partições que contêm muitos dados (por exemplo, Seattle) e outras partições com muito pouco estado (por exemplo, Kirkland). Então, qual é o impacto de ter partições com quantidades desiguais de estado?

Se você pensar no exemplo novamente, você pode facilmente ver que a partição que detém os votos para Seattle receberá mais tráfego do que a de Kirkland. Por padrão, o Service Fabric garante que haja aproximadamente o mesmo número de réplicas primárias e secundárias em cada nó. Assim, você pode acabar com nós que contêm réplicas que servem mais tráfego e outros que servem menos tráfego. De preferência, evite pontos quentes e frios como este em um aglomerado.

Para evitar isso, você deve fazer duas coisas, do ponto de vista do particionamento:

  • Tente particionar o estado para que ele seja distribuído uniformemente em todas as partições.
  • Carga de relatório de cada uma das réplicas para o serviço. (Para obter informações sobre como, confira este artigo em Métricas e Carga). O Service Fabric fornece a capacidade de relatar a carga consumida pelos serviços, como quantidade de memória ou número de registros. Com base nas métricas relatadas, o Service Fabric deteta que algumas partições estão servindo cargas mais altas do que outras e reequilibra o cluster movendo réplicas para nós mais adequados, para que, em geral, nenhum nó fique sobrecarregado.

Às vezes, você não pode saber quantos dados estarão em uma determinada partição. Portanto, uma recomendação geral é fazer as duas coisas - primeiro, adotando uma estratégia de particionamento que distribua os dados uniformemente pelas partições e, segundo, relatando a carga. O primeiro método evita situações descritas no exemplo de votação, enquanto o segundo ajuda a suavizar diferenças temporárias no acesso ou carga ao longo do tempo.

Outro aspeto do planejamento de partições é escolher o número correto de partições para começar. Do ponto de vista do Service Fabric, não há nada que impeça você de começar com um número maior de partições do que o previsto para o seu cenário. Na verdade, assumir o número máximo de partições é uma abordagem válida.

Em casos raros, você pode acabar precisando de mais partições do que escolheu inicialmente. Como não é possível alterar a contagem de partições após o fato, você precisaria aplicar algumas abordagens avançadas de partição, como a criação de uma nova instância de serviço do mesmo tipo de serviço. Você também precisaria implementar alguma lógica do lado do cliente que roteie as solicitações para a instância de serviço correta, com base no conhecimento do lado do cliente que o código do cliente deve manter.

Outra consideração para o planejamento de particionamento são os recursos disponíveis do computador. Como o estado precisa ser acessado e armazenado, você é obrigado a seguir:

  • Limites de largura de banda da rede
  • Limites de memória do sistema
  • Limites de armazenamento em disco

Então, o que acontece se você encontrar restrições de recursos em um cluster em execução? A resposta é que você pode simplesmente dimensionar o cluster para acomodar os novos requisitos.

O guia de planejamento de capacidade oferece orientação sobre como determinar quantos nós seu cluster precisa.

Introdução ao particionamento

Esta seção descreve como começar a particionar seu serviço.

O Service Fabric oferece três esquemas de partição:

  • Particionamento variado (também conhecido como UniformInt64Partition).
  • Particionamento nomeado. Os aplicativos que usam esse modelo geralmente têm dados que podem ser bucketed, dentro de um conjunto limitado. Alguns exemplos comuns de campos de dados usados como chaves de partição nomeadas seriam regiões, códigos postais, grupos de clientes ou outros limites de negócios.
  • Particionamento singleton. As partições singleton são normalmente usadas quando o serviço não requer nenhum roteamento adicional. Por exemplo, os serviços sem estado usam esse esquema de particionamento por padrão.

Os esquemas de particionamento Named e Singleton são formas especiais de partições variadas. Por padrão, os modelos do Visual Studio para o Service Fabric usam particionamento variado, pois é o mais comum e útil. O restante deste artigo se concentra no esquema de particionamento variado.

Esquema de particionamento variado

Isso é usado para especificar um intervalo inteiro (identificado por uma chave baixa e uma chave alta) e um número de partições (n). Ele cria n partições, cada uma responsável por um subintervalo não sobreposto do intervalo geral de chaves de partição. Por exemplo, um esquema de particionamento variado com uma chave baixa de 0, uma chave alta de 99 e uma contagem de 4 criaria quatro partições, como mostrado abaixo.

Particionamento de intervalo

Uma abordagem comum é criar um hash com base numa chave exclusiva dentro do conjunto de dados. Alguns exemplos comuns de chaves seriam um número de identificação do veículo (VIN), um ID de funcionário ou uma cadeia de caracteres exclusiva. Ao utilizar esta chave exclusiva, geraria um código de hash e um módulo de intervalo de chaves, para utilizar como a chave. Você pode especificar os limites superior e inferior do intervalo de chaves permitido.

Selecionar um algoritmo hash

Uma parte importante do hashing é selecionar o algoritmo hash. Uma consideração a ter é se o objetivo é agrupar chaves semelhantes próximas umas das outras (hashing sensível a local) ou se uma atividade deve ser distribuída amplamente entre todas as partições (hashing de distribuição), que é o mais comum.

As características de um bom algoritmo hash de distribuição são ser fácil de calcular, ter poucas colisões e distribuir as chaves de forma uniforme. Um bom exemplo de um algoritmo hash eficiente é o algoritmo hash FNV-1.

Um bom recurso para a escolha do algoritmo do código hash é a página da Wikipédia sobre os algoritmos hash.

Crie um serviço com monitoração de estado com várias partições

Vamos criar seu primeiro serviço stateful confiável com várias partições. Neste exemplo, você criará um aplicativo muito simples onde deseja armazenar todos os sobrenomes que começam com a mesma letra na mesma partição.

Antes de escrever qualquer código, você precisa pensar sobre as partições e chaves de partição. Você precisa de 26 partições (uma para cada letra do alfabeto), mas e as teclas baixa e alta? Como queremos literalmente ter uma partição por letra, podemos usar 0 como a chave baixa e 25 como a chave alta, pois cada letra é sua própria chave.

Nota

Trata-se de um cenário simplificado, uma vez que, na realidade, a distribuição seria desigual. Os sobrenomes que começam com as letras "S" ou "M" são mais comuns do que os que começam com "X" ou "Y".

  1. Abra o arquivo do>Visual Studio>Novo>projeto.

  2. Na caixa de diálogo Novo Projeto, escolha o aplicativo Service Fabric.

  3. Chame o projeto de "AlphabetPartitions".

  4. Na caixa de diálogo Criar um serviço, escolha Serviço com estado e chame-o de "Alphabet.Processing".

  5. Defina o número de partições. Abra o arquivo ApplicationManifest.xml localizado na pasta ApplicationPackageRoot do projeto AlphabetPartitions e atualize o parâmetro Processing_PartitionCount para 26, conforme mostrado abaixo.

    <Parameter Name="Processing_PartitionCount" DefaultValue="26" />
    

    Você também precisa atualizar as propriedades LowKey e HighKey do elemento StatefulService no ApplicationManifest.xml conforme mostrado abaixo.

    <Service Name="Alphabet.Processing">
      <StatefulService ServiceTypeName="Alphabet.ProcessingType" TargetReplicaSetSize="[Processing_TargetReplicaSetSize]" MinReplicaSetSize="[Processing_MinReplicaSetSize]">
        <UniformInt64Partition PartitionCount="[Processing_PartitionCount]" LowKey="0" HighKey="25" />
      </StatefulService>
    </Service>    
    
  6. Para que o serviço seja acessível, abra um ponto de extremidade em uma porta adicionando o elemento endpoint de ServiceManifest.xml (localizado na pasta PackageRoot) para o serviço Alphabet.Processing conforme mostrado abaixo:

    <Endpoint Name="ProcessingServiceEndpoint" Port="8089" Protocol="http" Type="Internal" />
    

    Agora o serviço está configurado para ouvir um ponto de extremidade interno com 26 partições.

  7. Em seguida, você precisa substituir o CreateServiceReplicaListeners() método da classe Processing.

    Nota

    Para este exemplo, assumimos que você está usando um simples HttpCommunicationListener. Para obter mais informações sobre comunicação de serviço confiável, consulte O modelo de comunicação de serviço confiável.

  8. Um padrão recomendado para a URL que uma réplica escuta é o seguinte formato: {scheme}://{nodeIp}:{port}/{partitionid}/{replicaid}/{guid}. Portanto, você deseja configurar seu ouvinte de comunicação para ouvir nos endpoints corretos e com esse padrão.

    Várias réplicas desse serviço podem ser hospedadas no mesmo computador, portanto, esse endereço precisa ser exclusivo para a réplica. É por isso que o ID da partição + o ID da réplica estão no URL. HttpListener pode ouvir em vários endereços na mesma porta, desde que o prefixo da URL seja exclusivo.

    O GUID extra existe para um caso avançado em que réplicas secundárias também escutam solicitações somente leitura. Quando esse for o caso, você deseja garantir que um novo endereço exclusivo seja usado ao fazer a transição do primário para o secundário para forçar os clientes a resolver novamente o endereço. '+' é usado como o endereço aqui para que a réplica ouça em todos os hosts disponíveis (IP, FQDN, localhost, etc.) O código abaixo mostra um exemplo.

    protected override IEnumerable<ServiceReplicaListener> CreateServiceReplicaListeners()
    {
         return new[] { new ServiceReplicaListener(context => this.CreateInternalListener(context))};
    }
    private ICommunicationListener CreateInternalListener(ServiceContext context)
    {
    
         EndpointResourceDescription internalEndpoint = context.CodePackageActivationContext.GetEndpoint("ProcessingServiceEndpoint");
         string uriPrefix = String.Format(
                "{0}://+:{1}/{2}/{3}-{4}/",
                internalEndpoint.Protocol,
                internalEndpoint.Port,
                context.PartitionId,
                context.ReplicaOrInstanceId,
                Guid.NewGuid());
    
         string nodeIP = FabricRuntime.GetNodeContext().IPAddressOrFQDN;
    
         string uriPublished = uriPrefix.Replace("+", nodeIP);
         return new HttpCommunicationListener(uriPrefix, uriPublished, this.ProcessInternalRequest);
    }
    

    Também vale a pena notar que o URL publicado é ligeiramente diferente do prefixo do URL de escuta. O URL de escuta é fornecido para HttpListener. A URL publicada é a URL publicada no Serviço de Nomenclatura do Service Fabric, que é usada para descoberta de serviço. Os clientes solicitarão esse endereço por meio desse serviço de descoberta. O endereço que os clientes obtêm precisa ter o IP ou FQDN real do nó para se conectar. Portanto, você precisa substituir '+' pelo IP ou FQDN do nó, como mostrado acima.

  9. A última etapa é adicionar a lógica de processamento ao serviço, conforme mostrado abaixo.

    private async Task ProcessInternalRequest(HttpListenerContext context, CancellationToken cancelRequest)
    {
        string output = null;
        string user = context.Request.QueryString["lastname"].ToString();
    
        try
        {
            output = await this.AddUserAsync(user);
        }
        catch (Exception ex)
        {
            output = ex.Message;
        }
    
        using (HttpListenerResponse response = context.Response)
        {
            if (output != null)
            {
                byte[] outBytes = Encoding.UTF8.GetBytes(output);
                response.OutputStream.Write(outBytes, 0, outBytes.Length);
            }
        }
    }
    private async Task<string> AddUserAsync(string user)
    {
        IReliableDictionary<String, String> dictionary = await this.StateManager.GetOrAddAsync<IReliableDictionary<String, String>>("dictionary");
    
        using (ITransaction tx = this.StateManager.CreateTransaction())
        {
            bool addResult = await dictionary.TryAddAsync(tx, user.ToUpperInvariant(), user);
    
            await tx.CommitAsync();
    
            return String.Format(
                "User {0} {1}",
                user,
                addResult ? "successfully added" : "already exists");
        }
    }
    

    ProcessInternalRequest lê os valores do parâmetro de cadeia de caracteres de consulta usado para chamar a partição e chama AddUserAsync para adicionar o sobrenome ao dicionário dictionaryconfiável.

  10. Vamos adicionar um serviço sem estado ao projeto para ver como você pode chamar uma partição específica.

    Este serviço serve como uma interface web simples que aceita o sobrenome como um parâmetro de cadeia de caracteres de consulta, determina a chave de partição e a envia para o serviço Alphabet.Processing para processamento.

  11. Na caixa de diálogo Criar um serviço, escolha Serviço sem estado e chame-o de "Alphabet.Web", conforme mostrado abaixo.

    Captura de ecrã do serviço sem estado.

  12. Atualize as informações do ponto de extremidade no ServiceManifest.xml do serviço Alphabet.WebApi para abrir uma porta, conforme mostrado abaixo.

    <Endpoint Name="WebApiServiceEndpoint" Protocol="http" Port="8081"/>
    
  13. Você precisa retornar uma coleção de ServiceInstanceListeners na classe Web. Novamente, você pode optar por implementar um HttpCommunicationListener simples.

    protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
    {
        return new[] {new ServiceInstanceListener(context => this.CreateInputListener(context))};
    }
    private ICommunicationListener CreateInputListener(ServiceContext context)
    {
        // Service instance's URL is the node's IP & desired port
        EndpointResourceDescription inputEndpoint = context.CodePackageActivationContext.GetEndpoint("WebApiServiceEndpoint")
        string uriPrefix = String.Format("{0}://+:{1}/alphabetpartitions/", inputEndpoint.Protocol, inputEndpoint.Port);
        var uriPublished = uriPrefix.Replace("+", FabricRuntime.GetNodeContext().IPAddressOrFQDN);
        return new HttpCommunicationListener(uriPrefix, uriPublished, this.ProcessInputRequest);
    }
    
  14. Agora você precisa implementar a lógica de processamento. O HttpCommunicationListener chama ProcessInputRequest quando uma solicitação chega. Então, vamos em frente e adicionar o código abaixo.

    private async Task ProcessInputRequest(HttpListenerContext context, CancellationToken cancelRequest)
    {
        String output = null;
        try
        {
            string lastname = context.Request.QueryString["lastname"];
            char firstLetterOfLastName = lastname.First();
            ServicePartitionKey partitionKey = new ServicePartitionKey(Char.ToUpper(firstLetterOfLastName) - 'A');
    
            ResolvedServicePartition partition = await this.servicePartitionResolver.ResolveAsync(alphabetServiceUri, partitionKey, cancelRequest);
            ResolvedServiceEndpoint ep = partition.GetEndpoint();
    
            JObject addresses = JObject.Parse(ep.Address);
            string primaryReplicaAddress = (string)addresses["Endpoints"].First();
    
            UriBuilder primaryReplicaUriBuilder = new UriBuilder(primaryReplicaAddress);
            primaryReplicaUriBuilder.Query = "lastname=" + lastname;
    
            string result = await this.httpClient.GetStringAsync(primaryReplicaUriBuilder.Uri);
    
            output = String.Format(
                    "Result: {0}. <p>Partition key: '{1}' generated from the first letter '{2}' of input value '{3}'. <br>Processing service partition ID: {4}. <br>Processing service replica address: {5}",
                    result,
                    partitionKey,
                    firstLetterOfLastName,
                    lastname,
                    partition.Info.Id,
                    primaryReplicaAddress);
        }
        catch (Exception ex) { output = ex.Message; }
    
        using (var response = context.Response)
        {
            if (output != null)
            {
                output = output + "added to Partition: " + primaryReplicaAddress;
                byte[] outBytes = Encoding.UTF8.GetBytes(output);
                response.OutputStream.Write(outBytes, 0, outBytes.Length);
            }
        }
    }
    

    Vamos percorrê-lo passo a passo. O código lê a primeira letra do parâmetro lastname de cadeia de caracteres de consulta em um char. Em seguida, ele determina a chave de partição para essa letra subtraindo o valor hexadecimal do valor hexadecimal da A primeira letra dos sobrenomes.

    string lastname = context.Request.QueryString["lastname"];
    char firstLetterOfLastName = lastname.First();
    ServicePartitionKey partitionKey = new ServicePartitionKey(Char.ToUpper(firstLetterOfLastName) - 'A');
    

    Lembre-se, para este exemplo, estamos usando 26 partições com uma chave de partição por partição. Em seguida, obtemos a partição partition de serviço para essa chave usando o ResolveAsync servicePartitionResolver método no objeto. servicePartitionResolver é definida como

    private readonly ServicePartitionResolver servicePartitionResolver = ServicePartitionResolver.GetDefault();
    

    O ResolveAsync método usa o URI de serviço, a chave de partição e um token de cancelamento como parâmetros. O URI do serviço para o serviço de processamento é fabric:/AlphabetPartitions/Processing. Em seguida, obtemos o ponto final da partição.

    ResolvedServiceEndpoint ep = partition.GetEndpoint()
    

    Finalmente, criamos a URL do ponto de extremidade mais a querystring e chamamos o serviço de processamento.

    JObject addresses = JObject.Parse(ep.Address);
    string primaryReplicaAddress = (string)addresses["Endpoints"].First();
    
    UriBuilder primaryReplicaUriBuilder = new UriBuilder(primaryReplicaAddress);
    primaryReplicaUriBuilder.Query = "lastname=" + lastname;
    
    string result = await this.httpClient.GetStringAsync(primaryReplicaUriBuilder.Uri);
    

    Uma vez que o processamento é feito, escrevemos a saída de volta.

  15. O último passo é testar o serviço. O Visual Studio usa parâmetros de aplicativo para implantação local e na nuvem. Para testar o serviço com 26 partições localmente, você precisa atualizar o Local.xml arquivo na pasta ApplicationParameters do projeto AlphabetPartitions, conforme mostrado abaixo:

    <Parameters>
      <Parameter Name="Processing_PartitionCount" Value="26" />
      <Parameter Name="WebApi_InstanceCount" Value="1" />
    </Parameters>
    
  16. Depois de concluir a implantação, você pode verificar o serviço e todas as suas partições no Gerenciador do Service Fabric.

    Captura de tela do Service Fabric Explorer

  17. Em um navegador, você pode testar a lógica de particionamento digitando http://localhost:8081/?lastname=somename. Você verá que cada sobrenome que começa com a mesma letra está sendo armazenado na mesma partição.

    Captura de tela do navegador

A solução completa do código usado neste artigo está disponível aqui: https://github.com/Azure-Samples/service-fabric-dotnet-getting-started/tree/classic/Services/AlphabetPartitions.

Próximos passos

Saiba mais sobre os serviços do Service Fabric: