Partilhar via


Como usar 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 desenvolvedor de serviços escolher como os serviços devem se comunicar. A estrutura do aplicativo Reliable Services fornece pilhas de comunicação internas, bem como APIs que você pode usar para criar seus componentes de comunicação personalizados.

Configurar a comunicação de serviço

A API de Serviços Confiáveis usa uma interface simples para comunicação de serviços. Para abrir um endpoint 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, você pode adicionar sua implementação de ouvinte de comunicação retornando-a em uma 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 monitoração de estado:

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

Em ambos os casos, você retorna uma coleção de ouvintes. O uso de vários ouvintes permite que seu serviço ouça em vários endpoints, potencialmente usando protocolos diferentes. Por exemplo, você pode ter um ouvinte HTTP e um ouvinte WebSocket separado. Você pode migrar de comunicação remota não segura para segura habilitando primeiro ambos os cenários tendo um ouvinte não seguro e um ouvinte seguro. Cada ouvinte recebe um nome, e a coleção resultante de nomes : os pares de endereços são representados como um objeto JSON quando um cliente solicita os endereços de escuta para uma instância de serviço ou uma partição.

Em um serviço sem estado, a substituição retorna uma coleção de ServiceInstanceListeners. A ServiceInstanceListener contém uma função para criar um ICommunicationListener(C#) / CommunicationListener(Java) e dá-lhe um nome. Para serviços com monitoração de estado, a substituição retorna uma coleção de ServiceReplicaListeners. Isso é ligeiramente diferente de sua contraparte sem estado, porque um ServiceReplicaListener tem uma opção para abrir um ICommunicationListener em réplicas secundárias. Você não só pode usar vários ouvintes de comunicação em um serviço, mas também pode especificar quais ouvintes aceitam solicitações em réplicas secundárias e quais escutam apenas em réplicas primárias.

Por exemplo, você pode ter um ServiceRemotingListener que recebe chamadas RPC somente em réplicas primárias e um segundo ouvinte personalizado que recebe solicitações de leitura em réplicas secundárias por 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 ouvintes para um serviço, cada ouvinte deve receber um nome exclusivo.

Por fim, descreva os pontos de extremidade necessários para o serviço no manifesto de serviço na seção sobre pontos de extremidade.

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

O ouvinte de comunicação pode acessar os recursos de ponto de extremidade alocados a ele a CodePackageActivationContext partir do .ServiceContext O ouvinte pode então começar a ouvir solicitações 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 de extremidade 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 hospedadas no mesmo ServiceHost podem compartilhar a mesma porta. Isso significa que o ouvinte de comunicação deve suportar o compartilhamento de portas. A maneira recomendada de fazer isso é que o ouvinte de comunicação use o ID da partição e o ID da réplica/instância quando gerar o endereço de escuta.

Registo da morada de serviço

Um serviço do sistema chamado Serviço de Nomenclatura é executado em clusters do Service Fabric. O Serviço de Nomenclatura é um registrador de serviços e seus endereços nos quais cada instância ou réplica do serviço está escutando. Quando o OpenAsync(C#) / openAsync(Java) método de um ICommunicationListener(C#) / CommunicationListener(Java) é concluído, seu valor de retorno é registrado no Serviço de Nomenclatura. Esse valor de retorno que é publicado no Serviço de Nomenclatura é uma cadeia de caracteres cujo valor pode ser qualquer coisa. Esse valor de cadeia de caracteres é 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 solicitem esse endereço pelo nome do serviço. Isso é importante porque o endereço do serviço não é estático. Os serviços são movidos no cluster para fins de disponibilidade e balanceamento de recursos. Este é o mecanismo que permite aos clientes resolver o endereço de escuta de um serviço.

Nota

Para obter um passo a passo completo de como escrever um ouvinte de comunicação, consulte Serviços de API Web do Service Fabric com autohospedagem OWIN para C#, enquanto para Java você pode escrever sua própria implementação de servidor HTTP, consulte Exemplo de aplicativo EchoServer em https://github.com/Azure-Samples/service-fabric-java-getting-started.

Comunicar com um serviço

A API de Serviços Confiáveis fornece as seguintes bibliotecas para escrever clientes que se comunicam com serviços.

Resolução de pontos finais de serviço

A primeira etapa para a comunicação com um serviço é resolver um endereço de ponto de extremidade da partição ou instância do serviço com o qual você deseja falar. A ServicePartitionResolver(C#) / FabricServicePartitionResolver(Java) classe de utilitário é uma primitiva básica que ajuda os clientes a determinar o ponto de extremidade de um serviço em tempo de execução. Na terminologia do Service Fabric, o processo de determinação do ponto de extremidade de um serviço é chamado de resolução do ponto de extremidade do serviço.

Para se conectar a serviços dentro de um cluster, ServicePartitionResolver pode ser criado usando as configurações padrão. Este é o uso recomendado para a maioria das situações:

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

Para se conectar a serviços em um cluster diferente, um ServicePartitionResolver pode ser criado com um conjunto de pontos de extremidade de gateway de cluster. Observe que os pontos de extremidade de gateway são apenas pontos de extremidade diferentes para se conectar 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");

Alternativamente, ServicePartitionResolver pode ser dada uma função para criar um FabricClient para usar internamente:

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

public interface CreateFabricClient {
    public FabricClient getFabricClient();
}

FabricClient é o objeto usado para se comunicar com o cluster do Service Fabric para várias operações de gerenciamento no cluster. Isso é útil quando você deseja ter mais controle sobre como um resolvedor de partição de serviço interage com seu cluster. FabricClient executa o cache internamente e geralmente é caro para criar, por isso é importante reutilizar FabricClient instâncias tanto quanto possível.

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

Um método de resolução é usado para recuperar 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 usando um ServicePartitionResolver, mas é necessário mais trabalho para garantir que o endereço resolvido possa ser usado corretamente. Seu cliente precisa detetar se a tentativa de conexão falhou devido a um erro transitório e pode ser repetida (por exemplo, o serviço foi movido ou está temporariamente indisponível) ou um erro permanente (por exemplo, o serviço foi excluído ou o recurso solicitado não existe mais). As instâncias de serviço ou réplicas podem se mover de nó para nó a qualquer momento por vários motivos. O endereço de serviço resolvido através de ServicePartitionResolver pode estar obsoleto no momento em que o código do cliente tenta se conectar. Nesse caso, novamente, o cliente precisa resolver novamente o endereço. Fornecer o anterior ResolvedServicePartition indica que o resolvedor precisa tentar novamente em vez de simplesmente recuperar um endereço armazenado em cache.

Normalmente, o código do cliente não precisa trabalhar diretamente com o ServicePartitionResolver. Ele é criado e passado para fábricas de clientes de comunicação na API de Serviços Confiáveis. As fábricas usam o resolvedor internamente para gerar um objeto cliente que pode ser usado para se comunicar com serviços.

Clientes e fábricas de comunicação

A biblioteca de fábrica de comunicação implementa um padrão típico de repetição de tratamento de falhas que facilita a repetição de conexões com pontos de extremidade de serviço resolvidos. A biblioteca de fábrica fornece o mecanismo de repetição enquanto você fornece os manipuladores de erro.

ICommunicationClientFactory(C#) / CommunicationClientFactory(Java) define a interface base implementada por uma fábrica de cliente de comunicação que produz clientes que podem falar com um serviço do Service Fabric. A implementação do CommunicationClientFactory depende da pilha de comunicação usada pelo serviço Service Fabric onde o cliente deseja se comunicar. A API de Serviços Confiáveis fornece um CommunicationClientFactoryBase<TCommunicationClient>arquivo . Isso fornece uma implementação base da interface CommunicationClientFactory e executa tarefas que são comuns a todas as pilhas de comunicação. (Essas tarefas incluem o uso de um ServicePartitionResolver para determinar o ponto de extremidade do serviço). Os clientes geralmente implementam a classe abstrata CommunicationClientFactoryBase para manipular a lógica que é específica para a pilha de comunicação.

O cliente de comunicação apenas recebe um endereço e usa-o para se conectar a um serviço. O cliente pode usar 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 do cliente é a principal responsável pela criação de clientes de comunicação. Para clientes que não mantêm uma conexão persistente, como um cliente HTTP, a fábrica só precisa criar e retornar o cliente. Outros protocolos que mantêm uma conexão persistente, como alguns protocolos binários, também devem ser validados (ValidateClient(string endpoint, MyCommunicationClient client)) pela fábrica para determinar se a conexão precisa 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) {
    }
}

Finalmente, um manipulador de exceções é responsável por determinar qual ação tomar quando uma exceção ocorre. As exceções são categorizadas em retryable e non retryable.

  • Exceções não retentáveis simplesmente são relançadas de volta para o chamador.
  • As exceções retentáveis são ainda categorizadas 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 de extremidade do serviço. Isso incluirá problemas de rede transitórios ou respostas de erro de serviço diferentes daquelas que indicam que o endereço do ponto de extremidade do serviço não existe.
    • As exceções não transitórias são aquelas que exigem que o endereço do ponto de extremidade do serviço seja resolvido novamente. Isso inclui exceções que indicam que o ponto de extremidade do serviço não pôde ser alcançado, 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 retornar false. Se ele sabe que decisão tomar, ele deve definir o resultado de acordo e retornar 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) construído em torno de um protocolo de comunicação, um ServicePartitionClient(C#) / FabricServicePartitionClient(Java) envolve tudo e fornece o loop de resolução de endereço de partição de serviço e tratamento de falhas em torno desses 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.
       */
   });

Próximos passos