Jak używać interfejsów API komunikacji usług Reliable Services

Usługa Azure Service Fabric jako platforma jest całkowicie niezależna od komunikacji między usługami. Wszystkie protokoły i stosy są dopuszczalne z protokołu UDP do protokołu HTTP. Deweloper usługi decyduje o tym, jak usługi powinny się komunikować. Platforma aplikacji Reliable Services udostępnia wbudowane stosy komunikacji, a także interfejsy API, których można użyć do tworzenia niestandardowych składników komunikacji.

Konfigurowanie komunikacji z usługą

Interfejs API usług Reliable Services używa prostego interfejsu do komunikacji z usługą. Aby otworzyć punkt końcowy usługi, po prostu zaimplementuj ten interfejs:


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();
}

Następnie możesz dodać implementację odbiornika komunikacji, zwracając ją w zastąpieniu metody klasy opartej na usłudze.

W przypadku usług bezstanowych:

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

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

W przypadku usług stanowych:

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

W obu przypadkach zwracasz kolekcję odbiorników. Korzystanie z wielu odbiorników umożliwia usłudze nasłuchiwanie w wielu punktach końcowych, potencjalnie przy użyciu różnych protokołów. Na przykład może istnieć odbiornik HTTP i oddzielny odbiornik protokołu WebSocket. Możesz przeprowadzić migrację z niezabezpieczonego do bezpiecznego komunikacji zdalnie, włączając najpierw oba scenariusze, używając zarówno niezabezpieczonego odbiornika, jak i bezpiecznego odbiornika. Każdy odbiornik otrzymuje nazwę, a wynikowa kolekcja nazw : pary adresów są reprezentowane jako obiekt JSON, gdy klient żąda adresów nasłuchiwania dla wystąpienia usługi lub partycji.

W usłudze bezstanowej zastąpienie zwraca kolekcję elementów ServiceInstanceListeners. Element ServiceInstanceListener zawiera funkcję umożliwiającą utworzenie elementu ICommunicationListener(C#) / CommunicationListener(Java) i nada mu nazwę. W przypadku usług stanowych zastąpienie zwraca kolekcję elementów ServiceReplicaListeners. Różni się to nieco od swojego bezstanowego odpowiednika, ponieważ ServiceReplicaListener istnieje możliwość otwarcia ICommunicationListener obiektu w replikach pomocniczych. Nie tylko można używać wielu odbiorników komunikacji w usłudze, ale można również określić, które odbiorniki akceptują żądania w replikach pomocniczych i które nasłuchiwać tylko na replikach podstawowych.

Na przykład można mieć element ServiceRemotingListener, który pobiera wywołania RPC tylko w replikach podstawowych, a drugi niestandardowy odbiornik, który pobiera żądania odczytu w replikach pomocniczych za pośrednictwem protokołu HTTP:

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

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

Uwaga

Podczas tworzenia wielu odbiorników dla usługi każdy odbiornik musi mieć unikatową nazwę.

Na koniec opisz punkty końcowe wymagane dla usługi w manifeście usługi w sekcji w punktach końcowych.

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

Odbiornik komunikacji może uzyskać dostęp do zasobów punktu końcowego przydzielonych do niego z poziomu CodePackageActivationContext obiektu w pliku ServiceContext. Odbiornik może następnie rozpocząć nasłuchiwanie żądań po jego otwarciu.

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

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

Uwaga

Zasoby punktu końcowego są wspólne dla całego pakietu usługi i są przydzielane przez usługę Service Fabric po aktywowaniu pakietu usługi. Wiele replik usług hostowanych w tym samym hoście usługi może współdzielić ten sam port. Oznacza to, że odbiornik komunikacji powinien obsługiwać udostępnianie portów. Zalecanym sposobem jest użycie identyfikatora partycji i identyfikatora repliki/wystąpienia podczas generowania adresu nasłuchiwania przez odbiornik komunikacji.

Rejestracja adresu usługi

Usługa systemowa o nazwie Nazewnictwo jest uruchamiana w klastrach usługi Service Fabric. Usługa nazewnictwa jest rejestratorem usług i ich adresami, na których nasłuchuje każde wystąpienie lub replika usługi. OpenAsync(C#) / openAsync(Java) Po zakończeniu ICommunicationListener(C#) / CommunicationListener(Java) metody zwracana wartość zostanie zarejestrowana w usłudze Naming Service. Ta zwracana wartość, która zostanie opublikowana w usłudze Naming Service, jest ciągiem, którego wartość może być w ogóle niczym. Ta wartość ciągu jest tym, co klienci widzą, gdy pytają o adres usługi z usługi nazewnictwa.

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);
}

Usługa Service Fabric udostępnia interfejsy API, które umożliwiają klientom i innym usługom monit o ten adres według nazwy usługi. Jest to ważne, ponieważ adres usługi nie jest statyczny. Usługi są przenoszone w klastrze na potrzeby równoważenia zasobów i dostępności. Jest to mechanizm, który umożliwia klientom rozpoznawanie adresu nasłuchiwania dla usługi.

Uwaga

Aby zapoznać się z kompletnym przewodnikiem dotyczącym pisania odbiornika komunikacji, zobacz Service Fabric Web API services with OWIN self-hosting for C# (Usługi internetowego interfejsu API usługi Service Fabric z własnym hostingiem OWIN dla języka C#), natomiast w przypadku języka Java możesz napisać własną implementację serwera HTTP, zobacz przykład aplikacji EchoServer pod adresem https://github.com/Azure-Samples/service-fabric-java-getting-started.

Komunikacja z usługą

Interfejs API usług Reliable Services udostępnia następujące biblioteki do pisania klientów komunikujących się z usługami.

Rozpoznawanie punktu końcowego usługi

Pierwszym krokiem komunikacji z usługą jest rozwiązanie adresu punktu końcowego partycji lub wystąpienia usługi, z którą chcesz porozmawiać. Klasa ServicePartitionResolver(C#) / FabricServicePartitionResolver(Java) narzędzia jest podstawowym elementem pierwotnym, który pomaga klientom określić punkt końcowy usługi w czasie wykonywania. W terminologii usługi Service Fabric proces określania punktu końcowego usługi jest określany jako rozwiązanie punktu końcowego usługi.

Aby połączyć się z usługami w klastrze, można utworzyć usługę ServicePartitionResolver przy użyciu ustawień domyślnych. Jest to zalecane użycie w większości sytuacji:

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

Aby połączyć się z usługami w innym klastrze, element ServicePartitionResolver można utworzyć za pomocą zestawu punktów końcowych bramy klastra. Należy pamiętać, że punkty końcowe bramy są po prostu różnymi punktami końcowymi do łączenia się z tym samym klastrem. Na przykład:

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");

Alternatywnie ServicePartitionResolver można podać funkcję do utworzenia obiektu FabricClient do użycia wewnętrznie:

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

public interface CreateFabricClient {
    public FabricClient getFabricClient();
}

FabricClient to obiekt używany do komunikowania się z klastrem usługi Service Fabric na potrzeby różnych operacji zarządzania w klastrze. Jest to przydatne, gdy chcesz mieć większą kontrolę nad sposobem interakcji narzędzia rozpoznawania partycji usługi z klastrem. FabricClient program wykonuje buforowanie wewnętrznie i jest ogólnie kosztowne do utworzenia, dlatego ważne jest, aby jak najwięcej używać FabricClient wystąpień.

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

Metoda rozpoznawania jest następnie używana do pobierania adresu usługi lub partycji usługi dla usług partycjonowanych.

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());

Adres usługi można łatwo rozpoznać przy użyciu elementu ServicePartitionResolver, ale wymagana jest większa praca w celu zapewnienia poprawnego użycia rozpoznanego adresu. Klient musi wykryć, czy próba połączenia nie powiodła się z powodu błędu przejściowego i może zostać ponowiona (np. przeniesiona usługa lub jest tymczasowo niedostępna) lub trwały błąd (np. usługa została usunięta lub żądany zasób już nie istnieje). Wystąpienia usługi lub repliki mogą poruszać się z węzła do węzła w dowolnym momencie z wielu powodów. Adres usługi rozwiązany za pośrednictwem elementu ServicePartitionResolver może być nieaktualny, gdy kod klienta próbuje nawiązać połączenie. W takim przypadku klient musi ponownie rozpoznać adres. Podanie poprzedniego ResolvedServicePartition oznacza, że narzędzie rozpoznawania musi spróbować ponownie, a nie po prostu pobrać adres buforowany.

Zazwyczaj kod klienta nie musi bezpośrednio pracować z usługą ServicePartitionResolver. Jest on tworzony i przekazywany do fabryk klienta komunikacji w interfejsie API usług Reliable Services. Fabryki używają programu rozpoznawania nazw wewnętrznie do generowania obiektu klienta, który może służyć do komunikowania się z usługami.

Klienci komunikacji i fabryki

Biblioteka fabryki komunikacji implementuje typowy wzorzec ponawiania prób obsługi błędów, który ułatwia ponawianie prób połączeń w celu rozpoznania punktów końcowych usługi. Biblioteka fabryki udostępnia mechanizm ponawiania podczas udostępniania procedur obsługi błędów.

ICommunicationClientFactory(C#) / CommunicationClientFactory(Java) definiuje interfejs podstawowy implementowany przez fabrykę klienta komunikacji, która generuje klientów, którzy mogą komunikować się z usługą Service Fabric. Implementacja elementu CommunicationClientFactory zależy od stosu komunikacji używanego przez usługę Service Fabric, w którym klient chce się komunikować. Interfejs API usług Reliable Services udostępnia element CommunicationClientFactoryBase<TCommunicationClient>. Zapewnia podstawową implementację interfejsu CommunicationClientFactory i wykonuje zadania wspólne dla wszystkich stosów komunikacji. (Te zadania obejmują użycie elementu ServicePartitionResolver w celu określenia punktu końcowego usługi). Klienci zazwyczaj implementują abstrakcyjną klasę CommunicationClientFactoryBase w celu obsługi logiki specyficznej dla stosu komunikacji.

Klient komunikacji po prostu odbiera adres i używa go do nawiązania połączenia z usługą. Klient może używać dowolnego żądanego protokołu.

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
     */
}

Fabryka klienta jest odpowiedzialna przede wszystkim za tworzenie klientów komunikacyjnych. W przypadku klientów, którzy nie utrzymują trwałego połączenia, takiego jak klient HTTP, fabryka musi tylko utworzyć i zwrócić klienta. Inne protokoły, które utrzymują trwałe połączenie, takie jak niektóre protokoły binarne, powinny być również weryfikowane (ValidateClient(string endpoint, MyCommunicationClient client)) przez fabrykę, aby określić, czy połączenie musi zostać ponownie utworzone.

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) {
    }
}

Na koniec program obsługi wyjątków jest odpowiedzialny za określenie akcji, która ma być wykonywana w przypadku wystąpienia wyjątku. Wyjątki są klasyfikowane do ponawiania prób i nie można ich ponowić.

  • Wyjątki niemożliwe do ponawiania po prostu są ponownie przywracane do elementu wywołującego.
  • wyjątki z możliwością ponawiania prób są dalej podzielone na przejściowe i nie przejściowe.
    • Wyjątki przejściowe to te, które można po prostu ponowić bez ponownego rozpoznawania adresu punktu końcowego usługi. Będą one obejmować przejściowe problemy z siecią lub odpowiedzi na błędy usługi inne niż te, które wskazują, że adres punktu końcowego usługi nie istnieje.
    • Wyjątki nie przejściowe to wyjątki, które wymagają ponownego rozpoznania adresu punktu końcowego usługi. Obejmują one wyjątki wskazujące, że nie można uzyskać dostępu do punktu końcowego usługi, co wskazuje, że usługa została przeniesiona do innego węzła.

Podejmuje TryHandleException decyzję o danym wyjątku. Jeśli nie wie, jakie decyzje należy podjąć w sprawie wyjątku, powinien zwrócić wartość false. Jeśli wie, jaką decyzję należy podjąć, należy odpowiednio ustawić wynik i zwrócić wartość true.

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;

    }
}

Zebranie wszystkich elementów

Za pomocą elementu ICommunicationClient(C#) / CommunicationClient(Java), ICommunicationClientFactory(C#) / CommunicationClientFactory(Java)i IExceptionHandler(C#) / ExceptionHandler(Java) utworzonego wokół protokołu komunikacyjnego, ServicePartitionClient(C#) / FabricServicePartitionClient(Java) wszystkie opakowuje go razem i zapewnia pętlę rozpoznawania adresów partycji błędów i obsługi błędów wokół tych składników.

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.
       */
   });

Następne kroki