Como utilizar as APIs de comunicação do Reliable Services

O Azure Service Fabric como plataforma é completamente agnóstico em relação à comunicação entre serviços. Todos os protocolos e pilhas são aceitáveis, de UDP a HTTP. Cabe ao programador de serviços escolher a forma como os serviços devem comunicar. A arquitetura da aplicação Reliable Services fornece pilhas de comunicação incorporadas, bem como APIs que pode utilizar para criar os seus componentes de comunicação personalizados.

Configurar a comunicação do serviço

A API reliable Services utiliza uma interface simples para comunicação de serviço. Para abrir um ponto final para o seu serviço, basta implementar esta interface:


public interface ICommunicationListener
{
    Task<string> OpenAsync(CancellationToken cancellationToken);

    Task CloseAsync(CancellationToken cancellationToken);

    void Abort();
}

public interface CommunicationListener {
    CompletableFuture<String> openAsync(CancellationToken cancellationToken);

    CompletableFuture<?> closeAsync(CancellationToken cancellationToken);

    void abort();
}

Em seguida, pode adicionar a implementação do serviço de escuta de comunicação ao devolvê-la numa substituição de método de classe baseada em serviço.

Para serviços sem estado:

public class MyStatelessService : StatelessService
{
    protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
    {
        ...
    }
    ...
}
public class MyStatelessService extends StatelessService {

    @Override
    protected List<ServiceInstanceListener> createServiceInstanceListeners() {
        ...
    }
    ...
}

Para serviços com monitorização de estado:

    @Override
    protected List<ServiceReplicaListener> createServiceReplicaListeners() {
        ...
    }
    ...
public class MyStatefulService : StatefulService
{
    protected override IEnumerable<ServiceReplicaListener> CreateServiceReplicaListeners()
    {
        ...
    }
    ...
}

Em ambos os casos, devolve uma coleção de serviços de escuta. A utilização de vários serviços de escuta permite que o seu serviço ouça em vários pontos finais, potencialmente através de protocolos diferentes. Por exemplo, pode ter um serviço de escuta HTTP e um serviço de escuta WebSocket separado. Pode migrar do não seguro para proteger a comunicação remota ao ativar primeiro ambos os cenários ao ter um serviço de escuta não seguro e um serviço de escuta seguro. Cada serviço de escuta obtém um nome e a coleção resultante de nomes: os pares de endereços são representados como um objeto JSON quando um cliente pede os endereços de escuta de uma instância de serviço ou de uma partição.

Num serviço sem estado, a substituição devolve uma coleção de ServiceInstanceListeners. A ServiceInstanceListener contém uma função para criar uma ICommunicationListener(C#) / CommunicationListener(Java) e dá-lhe um nome. Para serviços com monitorização de estado, a substituição devolve uma coleção de ServiceReplicaListeners. Isto é ligeiramente diferente do seu homólogo sem estado, porque um ServiceReplicaListener tem a opção de abrir uma em ICommunicationListener réplicas secundárias. Não só pode utilizar vários serviços de escuta de comunicação num serviço, como também pode especificar que serviços de escuta aceitam pedidos em réplicas secundárias e quais só ouvem nas réplicas primárias.

Por exemplo, pode ter um ServiceRemotingListener que recebe chamadas RPC apenas em réplicas primárias e um segundo serviço de escuta personalizado que recebe pedidos de leitura em réplicas secundárias através de HTTP:

protected override IEnumerable<ServiceReplicaListener> CreateServiceReplicaListeners()
{
    return new[]
    {
        new ServiceReplicaListener(context =>
            new MyCustomHttpListener(context),
            "HTTPReadonlyEndpoint",
            true),

        new ServiceReplicaListener(context =>
            this.CreateServiceRemotingListener(context),
            "rpcPrimaryEndpoint",
            false)
    };
}

Nota

Ao criar vários serviços de escuta para um serviço, cada serviço de escuta tem de ter um nome exclusivo.

Por fim, descreva os pontos finais necessários para o serviço no manifesto de serviço na secção em pontos finais.

<Resources>
    <Endpoints>
      <Endpoint Name="WebServiceEndpoint" Protocol="http" Port="80" />
      <Endpoint Name="OtherServiceEndpoint" Protocol="tcp" Port="8505" />
    <Endpoints>
</Resources>

O serviço de escuta de comunicação pode aceder aos recursos de ponto final que lhe foram atribuídos a CodePackageActivationContext partir do no ServiceContext. Em seguida, o serviço de escuta pode começar a escutar pedidos quando é aberto.

var codePackageActivationContext = serviceContext.CodePackageActivationContext;
var port = codePackageActivationContext.GetEndpoint("ServiceEndpoint").Port;

CodePackageActivationContext codePackageActivationContext = serviceContext.getCodePackageActivationContext();
int port = codePackageActivationContext.getEndpoint("ServiceEndpoint").getPort();

Nota

Os recursos de ponto final são comuns a todo o pacote de serviço e são alocados pelo Service Fabric quando o pacote de serviço é ativado. Várias réplicas de serviço alojadas no mesmo ServiceHost podem partilhar a mesma porta. Isto significa que o serviço de escuta de comunicação deve suportar a partilha de portas. A forma recomendada de o fazer é o serviço de escuta de comunicação utilizar o ID de partição e o ID da réplica/instância quando gera o endereço de escuta.

Registo de endereços de serviço

Um serviço de sistema denominado Serviço de Nomenclatura é executado em clusters do Service Fabric. O Serviço de Nomenclatura é uma entidade de registo de serviços e respetivos endereços nos quais cada instância ou réplica do serviço está a escutar. Quando o OpenAsync(C#) / openAsync(Java) método de um é ICommunicationListener(C#) / CommunicationListener(Java) concluído, o respetivo valor devolvido é registado no Serviço de Nomenclatura. Este valor devolvido que é publicado no Serviço de Nomenclatura é uma cadeia cujo valor pode ser qualquer coisa. Este valor de cadeia é o que os clientes veem quando pedem um endereço para o serviço do Serviço de Nomenclatura.

public Task<string> OpenAsync(CancellationToken cancellationToken)
{
    EndpointResourceDescription serviceEndpoint = serviceContext.CodePackageActivationContext.GetEndpoint("ServiceEndpoint");
    int port = serviceEndpoint.Port;

    this.listeningAddress = string.Format(
                CultureInfo.InvariantCulture,
                "http://+:{0}/",
                port);

    this.publishAddress = this.listeningAddress.Replace("+", FabricRuntime.GetNodeContext().IPAddressOrFQDN);

    this.webApp = WebApp.Start(this.listeningAddress, appBuilder => this.startup.Invoke(appBuilder));

    // the string returned here will be published in the Naming Service.
    return Task.FromResult(this.publishAddress);
}
public CompletableFuture<String> openAsync(CancellationToken cancellationToken)
{
    EndpointResourceDescription serviceEndpoint = serviceContext.getCodePackageActivationContext.getEndpoint("ServiceEndpoint");
    int port = serviceEndpoint.getPort();

    this.publishAddress = String.format("http://%s:%d/", FabricRuntime.getNodeContext().getIpAddressOrFQDN(), port);

    this.webApp = new WebApp(port);
    this.webApp.start();

    /* the string returned here will be published in the Naming Service.
     */
    return CompletableFuture.completedFuture(this.publishAddress);
}

O Service Fabric fornece APIs que permitem que clientes e outros serviços peçam este endereço por nome de serviço. Isto é importante porque o endereço do serviço não é estático. Os serviços são movidos no cluster para fins de balanceamento de recursos e disponibilidade. Este é o mecanismo que permite aos clientes resolver o endereço de escuta de um serviço.

Nota

Para obter instruções completas sobre como escrever um serviço de escuta de comunicação, veja Service Fabric Web API services with OWIN self-hosting for C#(Serviços de API Web do Service Fabric com alojamento autónomo OWIN para C#), enquanto para Java pode escrever a sua própria implementação de servidor HTTP, veja EchoServer application example (Exemplo de aplicação EchoServer em https://github.com/Azure-Samples/service-fabric-java-getting-started.

Comunicar com um serviço

A API reliable Services fornece as seguintes bibliotecas para escrever clientes que comunicam com serviços.

Resolução do ponto final de serviço

O primeiro passo para a comunicação com um serviço é resolver um endereço de ponto final da partição ou instância do serviço com o qual pretende falar. A ServicePartitionResolver(C#) / FabricServicePartitionResolver(Java) classe utilitária é uma primitiva básica que ajuda os clientes a determinar o ponto final de um serviço no runtime. Na terminologia do Service Fabric, o processo de determinação do ponto final de um serviço é referido como a resolução do ponto final de serviço.

Para ligar a serviços dentro de um cluster, o ServicePartitionResolver pode ser criado com as predefinições. Esta é a utilização recomendada para a maioria das situações:

ServicePartitionResolver resolver = ServicePartitionResolver.GetDefault();
FabricServicePartitionResolver resolver = FabricServicePartitionResolver.getDefault();

Para ligar a serviços num cluster diferente, pode ser criado um ServicePartitionResolver com um conjunto de pontos finais de gateway de cluster. Tenha em atenção que os pontos finais do gateway são apenas pontos finais diferentes para ligar ao mesmo cluster. Por exemplo:

ServicePartitionResolver resolver = new  ServicePartitionResolver("mycluster.cloudapp.azure.com:19000", "mycluster.cloudapp.azure.com:19001");
FabricServicePartitionResolver resolver = new  FabricServicePartitionResolver("mycluster.cloudapp.azure.com:19000", "mycluster.cloudapp.azure.com:19001");

Em alternativa, ServicePartitionResolver pode ser-lhe atribuída uma função para criar um FabricClient para utilizar internamente:

public delegate FabricClient CreateFabricClientDelegate();
public FabricServicePartitionResolver(CreateFabricClient createFabricClient) {
...
}

public interface CreateFabricClient {
    public FabricClient getFabricClient();
}

FabricClient é o objeto utilizado para comunicar com o cluster do Service Fabric para várias operações de gestão no cluster. Isto é útil quando quer ter mais controlo sobre a forma como uma resolução de partições de serviço interage com o cluster. FabricClient Efetua a colocação em cache internamente e é geralmente dispendioso de criar, pelo que é importante reutilizar FabricClient instâncias tanto quanto possível.

ServicePartitionResolver resolver = new  ServicePartitionResolver(() => CreateMyFabricClient());
FabricServicePartitionResolver resolver = new  FabricServicePartitionResolver(() -> new CreateFabricClientImpl());

Em seguida, é utilizado um método de resolução para obter o endereço de um serviço ou uma partição de serviço para serviços particionados.

ServicePartitionResolver resolver = ServicePartitionResolver.GetDefault();

ResolvedServicePartition partition =
    await resolver.ResolveAsync(new Uri("fabric:/MyApp/MyService"), new ServicePartitionKey(), cancellationToken);
FabricServicePartitionResolver resolver = FabricServicePartitionResolver.getDefault();

CompletableFuture<ResolvedServicePartition> partition =
    resolver.resolveAsync(new URI("fabric:/MyApp/MyService"), new ServicePartitionKey());

Um endereço de serviço pode ser resolvido facilmente com um ServicePartitionResolver, mas é necessário mais trabalho para garantir que o endereço resolvido pode ser utilizado corretamente. O cliente tem de detetar se a tentativa de ligação falhou devido a um erro transitório e pode ser repetida (por exemplo, o serviço movido ou está temporariamente indisponível) ou um erro permanente (por exemplo, o serviço foi eliminado ou o recurso pedido já não existe). As réplicas ou instâncias de serviço podem mover-se do nó para o nó em qualquer altura por vários motivos. O endereço de serviço resolvido através de ServicePartitionResolver pode estar obsoleto quando o código do cliente tentar ligar. Nesse caso, novamente, o cliente tem de voltar a resolver o endereço. Fornecer o anterior ResolvedServicePartition indica que a resolução tem de tentar novamente em vez de obter simplesmente um endereço em cache.

Normalmente, o código de cliente não precisa de funcionar diretamente com o ServicePartitionResolver. É criado e transmitido para fábricas de cliente de comunicação na API reliable Services. As fábricas utilizam a resolução internamente para gerar um objeto de cliente que pode ser utilizado para comunicar com serviços.

Clientes de comunicação e fábricas

A biblioteca da fábrica de comunicações implementa um padrão típico de repetição de processamento de falhas que facilita a repetição de ligações a pontos finais de serviço resolvidos. A biblioteca de fábrica fornece o mecanismo de repetição enquanto fornece os processadores de erros.

ICommunicationClientFactory(C#) / CommunicationClientFactory(Java) define a interface base implementada por uma fábrica de cliente de comunicação que produz clientes que podem comunicar com um serviço do Service Fabric. A implementação do CommunicationClientFactory depende da pilha de comunicação utilizada pelo serviço Service Fabric onde o cliente quer comunicar. A API reliable Services fornece um CommunicationClientFactoryBase<TCommunicationClient>. Isto fornece uma implementação base da interface CommunicationClientFactory e executa tarefas comuns a todas as pilhas de comunicação. (Estas tarefas incluem a utilização de um ServicePartitionResolver para determinar o ponto final de serviço). Normalmente, os clientes implementam a classe CommunicationClientFactoryBase abstrata para processar lógicas específicas da pilha de comunicação.

O cliente de comunicação recebe apenas um endereço e utiliza-o para ligar a um serviço. O cliente pode utilizar o protocolo que quiser.

public class MyCommunicationClient : ICommunicationClient
{
    public ResolvedServiceEndpoint Endpoint { get; set; }

    public string ListenerName { get; set; }

    public ResolvedServicePartition ResolvedServicePartition { get; set; }
}
public class MyCommunicationClient implements CommunicationClient {

    private ResolvedServicePartition resolvedServicePartition;
    private String listenerName;
    private ResolvedServiceEndpoint endPoint;

    /*
     * Getters and Setters
     */
}

A fábrica de cliente é a principal responsável pela criação de clientes de comunicação. Para clientes que não mantêm uma ligação persistente, como um cliente HTTP, a fábrica só precisa de criar e devolver o cliente. Outros protocolos que mantêm uma ligação persistente, como alguns protocolos binários, também devem ser validados (ValidateClient(string endpoint, MyCommunicationClient client)) pela fábrica para determinar se a ligação tem de ser recriada.

public class MyCommunicationClientFactory : CommunicationClientFactoryBase<MyCommunicationClient>
{
    protected override void AbortClient(MyCommunicationClient client)
    {
    }

    protected override Task<MyCommunicationClient> CreateClientAsync(string endpoint, CancellationToken cancellationToken)
    {
    }

    protected override bool ValidateClient(MyCommunicationClient clientChannel)
    {
    }

    protected override bool ValidateClient(string endpoint, MyCommunicationClient client)
    {
    }
}
public class MyCommunicationClientFactory extends CommunicationClientFactoryBase<MyCommunicationClient> {

    @Override
    protected boolean validateClient(MyCommunicationClient clientChannel) {
    }

    @Override
    protected boolean validateClient(String endpoint, MyCommunicationClient client) {
    }

    @Override
    protected CompletableFuture<MyCommunicationClient> createClientAsync(String endpoint) {
    }

    @Override
    protected void abortClient(MyCommunicationClient client) {
    }
}

Por fim, um processador de exceções é responsável por determinar que ação tomar quando ocorre uma exceção. As exceções são categorizadas para repetição e não podem ser reativadas.

  • As exceções não retráveis são simplesmente reencainhadas para o autor da chamada.
  • As exceções retráveis são categorizadas ainda mais em transitórias e não transitórias.
    • As exceções transitórias são aquelas que podem simplesmente ser repetidas sem resolver novamente o endereço do ponto final de serviço. Estes irão incluir problemas de rede transitórios ou respostas de erro de serviço que não aquelas que indicam que o endereço do ponto final de serviço não existe.
    • As exceções não transitórias são aquelas que exigem que o endereço do ponto final de serviço seja resolvido novamente. Estas incluem exceções que indicam que não foi possível alcançar o ponto final de serviço, indicando que o serviço foi movido para um nó diferente.

O TryHandleException toma uma decisão sobre uma determinada exceção. Se não souber que decisões tomar sobre uma exceção, deve devolver falso. Se souber que decisão tomar, deve definir o resultado em conformidade e devolver verdadeiro.

class MyExceptionHandler : IExceptionHandler
{
    public bool TryHandleException(ExceptionInformation exceptionInformation, OperationRetrySettings retrySettings, out ExceptionHandlingResult result)
    {
        // if exceptionInformation.Exception is known and is transient (can be retried without re-resolving)
        result = new ExceptionHandlingRetryResult(exceptionInformation.Exception, true, retrySettings, retrySettings.DefaultMaxRetryCount);
        return true;


        // if exceptionInformation.Exception is known and is not transient (indicates a new service endpoint address must be resolved)
        result = new ExceptionHandlingRetryResult(exceptionInformation.Exception, false, retrySettings, retrySettings.DefaultMaxRetryCount);
        return true;

        // if exceptionInformation.Exception is unknown (let the next IExceptionHandler attempt to handle it)
        result = null;
        return false;
    }
}
public class MyExceptionHandler implements ExceptionHandler {

    @Override
    public ExceptionHandlingResult handleException(ExceptionInformation exceptionInformation, OperationRetrySettings retrySettings) {

        /* if exceptionInformation.getException() is known and is transient (can be retried without re-resolving)
         */
        result = new ExceptionHandlingRetryResult(exceptionInformation.getException(), true, retrySettings, retrySettings.getDefaultMaxRetryCount());
        return true;


        /* if exceptionInformation.getException() is known and is not transient (indicates a new service endpoint address must be resolved)
         */
        result = new ExceptionHandlingRetryResult(exceptionInformation.getException(), false, retrySettings, retrySettings.getDefaultMaxRetryCount());
        return true;

        /* if exceptionInformation.getException() is unknown (let the next ExceptionHandler attempt to handle it)
         */
        result = null;
        return false;

    }
}

Juntar tudo

Com um ICommunicationClient(C#) / CommunicationClient(Java), ICommunicationClientFactory(C#) / CommunicationClientFactory(Java)e IExceptionHandler(C#) / ExceptionHandler(Java) criado em torno de um protocolo de comunicação, um ServicePartitionClient(C#) / FabricServicePartitionClient(Java) encapsula tudo e fornece o ciclo de resolução de endereços de partição de serviço e processamento de falhas em torno destes componentes.

private MyCommunicationClientFactory myCommunicationClientFactory;
private Uri myServiceUri;

var myServicePartitionClient = new ServicePartitionClient<MyCommunicationClient>(
    this.myCommunicationClientFactory,
    this.myServiceUri,
    myPartitionKey);

var result = await myServicePartitionClient.InvokeWithRetryAsync(async (client) =>
   {
      // Communicate with the service using the client.
   },
   CancellationToken.None);

private MyCommunicationClientFactory myCommunicationClientFactory;
private URI myServiceUri;

FabricServicePartitionClient myServicePartitionClient = new FabricServicePartitionClient<MyCommunicationClient>(
    this.myCommunicationClientFactory,
    this.myServiceUri,
    myPartitionKey);

CompletableFuture<?> result = myServicePartitionClient.invokeWithRetryAsync(client -> {
      /* Communicate with the service using the client.
       */
   });

Passos seguintes