Partilhar via


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

Este artigo fornece uma introdução aos conceitos básicos de criação de partições de serviços fiáveis do Azure Service Fabric. A criação de partições permite o armazenamento de dados nas máquinas locais para que os dados e a computação possam ser dimensionados em conjunto.

Dica

Está disponível um exemplo completo do código neste artigo no GitHub.

Criação de partições

A criação de partições não é exclusiva do Service Fabric. Na verdade, é um padrão fundamental de construção de serviços dimensionáveis. Num sentido mais amplo, podemos pensar na criação de partições como um conceito de dividir o estado (dados) e calcular em unidades mais pequenas e acessíveis para melhorar a escalabilidade e o desempenho. Uma forma conhecida de criação de partições é a criação de partições de dados, também conhecida como fragmentação.

Partition Service Fabric stateless services (Serviços sem estado do Service Fabric de Partição)

Para serviços sem estado, 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 por um cluster com 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 mantém o seu estado externamente, por exemplo, numa base de dados na Base de Dados SQL do Azure (como um site que armazena as informações e os dados da sessão). O segundo são os serviços apenas de computação (como uma calculadora ou miniatura de imagens) que não gerem nenhum estado persistente.

Em ambos os casos, a criação de partições de um serviço sem estado é um cenário muito raro: normalmente, a escalabilidade e a disponibilidade são obtidas ao adicionar mais instâncias. A única altura em que pretende considerar múltiplas partições para instâncias de serviço sem estado é quando precisa de cumprir pedidos de encaminhamento especiais.

Por exemplo, considere um caso em que os utilizadores com IDs num determinado intervalo só devem ser servidos por uma instância de serviço específica. Outro exemplo de quando pode particionar um serviço sem estado é quando tem um back-end verdadeiramente particionado (por exemplo, uma base de dados fragmentada em Base de Dados SQL) e quer controlar que instância de serviço deve escrever na partição horizontal da base de dados ou realizar outros trabalhos de preparação no serviço sem estado que necessitem das mesmas informações de criação de partições utilizadas no back-end. Estes tipos de cenários também podem ser resolvidos de diferentes formas e não requerem necessariamente a criação de partições de serviços.

O resto destas instruções centra-se nos serviços com estado.

Partition Service Fabric stateful services

O Service Fabric facilita o desenvolvimento de serviços com estado dimensionável ao oferecer uma forma de primeira classe para o estado de partição (dados). Conceptualmente, pode pensar numa partição de um serviço com estado como uma unidade de escala altamente fiável através de réplicas distribuídas e equilibradas nos nós de um cluster.

A criação de partições no contexto dos serviços com estado do Service Fabric refere-se ao processo de determinação de que uma determinada partição de serviço é responsável por uma parte do estado completo do serviço. (Conforme mencionado anteriormente, uma partição é um conjunto de réplicas). Um excelente aspeto do Service Fabric é que coloca as partições em nós diferentes. Isto permite-lhes crescer até ao limite de recursos de um nó. À medida que as necessidades de dados aumentam, as partições aumentam e o Service Fabric reequilibrar as partições entre nós. Isto garante a utilização eficiente contínua dos recursos de hardware.

Para lhe dar um exemplo, digamos que começa 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. Neste caso, o Service Fabric equilibraria e distribuiria as réplicas pelo cluster e acabaria com duas réplicas primárias por nó. Se agora precisar de aumentar horizontalmente 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 reduzir verticalmente 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 de dimensionar o cluster.

Serviço com estado

Como resultado, o aumento horizontal é alcançado, uma vez que os pedidos dos clientes são distribuídos por computadores, o desempenho geral da aplicação é melhorado e a contenção no acesso a segmentos de dados é reduzida.

Planear a criação de partições

Antes de implementar um serviço, deve sempre considerar a estratégia de criação de partições necessária para aumentar horizontalmente. Existem diferentes formas, mas todas elas focam-se no que a aplicação precisa de alcançar. Para o contexto deste artigo, vamos considerar alguns dos aspetos mais importantes.

Uma boa abordagem é pensar na estrutura do estado que precisa de ser particionada, como o primeiro passo.

Vejamos um exemplo simples. Se criasse um serviço para uma sondagem a nível municipal, poderia criar uma partição para cada cidade do concelho. Em seguida, pode armazenar os votos de 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

À medida que a população das cidades varia bastante, 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). Qual é o impacto de ter partições com quantidades de estado irregulares?

Se voltar a pensar no exemplo, pode ver facilmente que a partição que contém os votos para Seattle obterá mais tráfego do que a de Kirkland. Por predefinição, o Service Fabric garante que existe o mesmo número de réplicas primárias e secundárias em cada nó. Assim, 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, deve evitar pontos quentes e frios como este num cluster.

Para evitar isto, deve efetuar duas ações, do ponto de vista da criação de partições:

  • Tente particionar o estado para que seja distribuído uniformemente por todas as partições.
  • Reporte a carga de cada uma das réplicas do serviço. (Para obter informações sobre como, consulte este artigo sobre Métricas e Carga). O Service Fabric fornece a capacidade de comunicar a carga consumida pelos serviços, como a quantidade de memória ou o número de registos. Com base nas métricas comunicadas, o Service Fabric deteta que algumas partições estão a servir cargas mais elevadas do que outras e reequilibrar o cluster ao mover réplicas para nós mais adequados, de modo a que o nó global esteja sobrecarregado.

Por vezes, não pode saber a quantidade de dados numa determinada partição. Assim, uma recomendação geral é fazer ambos- primeiro, adotando uma estratégia de criação de partições que espalha os dados uniformemente entre as partições e a segunda, através da carga de relatórios. O primeiro método impede situações descritas no exemplo de votação, enquanto o segundo ajuda a suavizar as diferenças temporárias de acesso ou carga ao longo do tempo.

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

Em casos raros, pode acabar por precisar de mais partições do que escolheu inicialmente. Como não pode alterar a contagem de partições após o facto, teria de aplicar algumas abordagens de partição avançadas, como criar uma nova instância de serviço do mesmo tipo de serviço. Também teria de implementar alguma lógica do lado do cliente que encaminhe os pedidos para a instância de serviço correta, com base no conhecimento do lado do cliente que o código do cliente tem de manter.

Outra consideração para o planeamento da criação de partições são os recursos de computador disponíveis. Uma vez que o estado tem de ser acedido e armazenado, é obrigado a seguir:

  • Limites de largura de banda de rede
  • Limites de memória do sistema
  • Limites de armazenamento de discos

O que acontece se encontrar restrições de recursos num cluster em execução? A resposta é que pode simplesmente aumentar horizontalmente o cluster para acomodar os novos requisitos.

O guia de planeamento de capacidade oferece orientações sobre como determinar quantos nós o cluster precisa.

Introdução à criação de partições

Esta secção descreve como começar a criar partições do seu serviço.

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

  • Criação de partições entre intervalos (também conhecida como UniformInt64Partition).
  • Criação de partições com nome. Normalmente, as aplicações que utilizam este modelo têm dados que podem ser registados num conjunto vinculado. Alguns exemplos comuns de campos de dados utilizados como chaves de partição nomeadas seriam regiões, códigos postais, grupos de clientes ou outros limites empresariais.
  • Criação de partições singleton. Normalmente, as partições singleton são utilizadas quando o serviço não necessita de qualquer encaminhamento adicional. Por exemplo, os serviços sem estado utilizam este esquema de criação de partições por predefinição.

Os esquemas de criação de partições Nomeadas e Singleton são formas especiais de partições de intervalo. Por predefinição, os modelos do Visual Studio para o Service Fabric utilizam a criação de partições em intervalos, uma vez que é a mais comum e útil. O resto deste artigo centra-se no esquema de criação de partições entre intervalos.

Esquema de criação de partições entre intervalos

Isto é utilizado para especificar um intervalo de números inteiros (identificado por uma chave baixa e uma chave alta) e uma série de partições (n). Cria n partições, cada uma responsável por uma suborganizar não sobreposta do intervalo de chaves de partição global. Por exemplo, um esquema de criação de partições num intervalo com uma chave baixa de 0, uma chave alta de 99 e uma contagem de 4 criaria quatro partições, conforme mostrado abaixo.

Criação de partições 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 de veículo (VIN), um ID de funcionário ou uma cadeia 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. 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.

Criar um serviço com estado com várias partições

Vamos criar o seu primeiro serviço com estado fiável com várias partições. Neste exemplo, vai criar uma aplicação muito simples onde pretende armazenar todos os apelidos que começam com a mesma letra na mesma partição.

Antes de escrever qualquer código, tem de pensar nas partições e nas chaves de partição. Precisa de 26 partições (uma para cada letra no alfabeto), mas e as teclas baixas e altas? Como literalmente queremos ter uma partição por letra, podemos usar 0 como a tecla baixa e 25 como a chave alta, uma vez que cada letra é a sua própria chave.

Nota

Este é um cenário simplificado, uma vez que, na realidade, a distribuição seria desigual. Os apelidos 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 oNovo>Projetodo Ficheiro> do Visual Studio>.

  2. Na caixa de diálogo Novo Projeto , selecione a aplicação Service Fabric.

  3. Chame o projeto de "AlphabetPartitions".

  4. Na caixa de diálogo Criar um Serviço , selecione Serviço com Estado e chame-lhe "Alphabet.Processing".

  5. Defina o número de partições. Abra o ficheiro 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" />
    

    Também tem de 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 esteja acessível, abra um ponto final numa porta ao adicionar o elemento de ponto final do 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 escutar um ponto final interno com 26 partições.

  7. Em seguida, tem de substituir o CreateServiceReplicaListeners() método da classe Processamento.

    Nota

    Para este exemplo, partimos do princípio de que está a utilizar um HttpCommunicationListener simples. Para obter mais informações sobre a comunicação de serviço fiável, veja O modelo de comunicação reliable Service.

  8. Um padrão recomendado para o URL em que uma réplica escuta é o seguinte formato: {scheme}://{nodeIp}:{port}/{partitionid}/{replicaid}/{guid}. Por isso, quer configurar o seu serviço de escuta de comunicação para escutar os pontos finais corretos e com este padrão.

    Várias réplicas deste serviço podem estar alojadas no mesmo computador, pelo que este endereço tem de ser exclusivo da réplica. É por isso que o ID da partição + ID de réplica está no URL. O HttpListener pode escutar vários endereços na mesma porta, desde que o prefixo de URL seja exclusivo.

    O GUID adicional está disponível para um caso avançado em que as réplicas secundárias também ouvem pedidos só de leitura. Quando for esse o caso, quer certificar-se de que é utilizado um novo endereço exclusivo ao transitar do principal para o secundário para forçar os clientes a resolver novamente o endereço. "+" é utilizado como o endereço aqui para que a réplica ouça todos os anfitriões 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 de URL de escuta. O URL de escuta é atribuído a HttpListener. O URL publicado é o URL publicado no Service Fabric Naming Service, que é utilizado para a deteção de serviços. Os clientes irão pedir este endereço através desse serviço de deteção. O endereço que os clientes obtêm tem de ter o IP ou FQDN real do nó para se ligarem. Por isso, tem de substituir '+' pelo IP ou FQDN do nó, conforme mostrado acima.

  9. O último passo é 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 consulta utilizado para chamar a partição e chamadas AddUserAsync para adicionar o apelido ao dicionário dictionaryfiável .

  10. Vamos adicionar um serviço sem estado ao projeto para ver como pode chamar uma determinada partição.

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

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

    Captura de ecrã do serviço sem estado .

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

    <Endpoint Name="WebApiServiceEndpoint" Protocol="http" Port="8081"/>
    
  13. Tem de devolver uma coleção de ServiceInstanceListeners na web da classe. Mais uma vez, 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, tem de implementar a lógica de processamento. O HttpCommunicationListener chama ProcessInputRequest quando um pedido é recebido. Por isso, vamos 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 guiá-lo passo a passo. O código lê a primeira letra do parâmetro lastname da cadeia de consulta num caráter. Em seguida, determina a chave de partição desta letra ao subtrair o valor hexadecimal do A valor hexadecimal da primeira letra dos apelidos.

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

    Lembre-se de que, neste exemplo, estamos a utilizar 26 partições com uma chave de partição por partição. Em seguida, obtemos a partição partition de serviço para esta chave com o ResolveAsync método no servicePartitionResolver objeto. servicePartitionResolver é definido como

    private readonly ServicePartitionResolver servicePartitionResolver = ServicePartitionResolver.GetDefault();
    

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

    ResolvedServiceEndpoint ep = partition.GetEndpoint()
    

    Por fim, criamos o URL do ponto final e a linha de consulta 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);
    

    Assim que o processamento estiver concluído, voltamos a escrever o resultado.

  15. O último passo é testar o serviço. O Visual Studio utiliza parâmetros de aplicação para implementação local e na cloud. Para testar o serviço com 26 partições localmente, tem de atualizar o Local.xml ficheiro na pasta ApplicationParameters do projeto AlphabetPartitions, conforme mostrado abaixo:

    <Parameters>
      <Parameter Name="Processing_PartitionCount" Value="26" />
      <Parameter Name="WebApi_InstanceCount" Value="1" />
    </Parameters>
    
  16. Assim que terminar a implementação, pode verificar o serviço e todas as partições no Service Fabric Explorer.

    Service Fabric Explorer captura de ecrã

  17. Num browser, pode testar a lógica de criação de partições ao introduzir http://localhost:8081/?lastname=somename. Verá que cada apelido que começa com a mesma letra está a ser armazenado na mesma partição.

    Captura de ecrã do browser

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

Passos seguintes

Saiba mais sobre os serviços do Service Fabric: