Udostępnij za pośrednictwem


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 sposobie komunikowania się usług. 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żna dodać implementację odbiornika komunikacji, zwracając ją w metodzie 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. Użycie wielu odbiorników umożliwia usłudze nasłuchiwanie wielu punktów 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 odbiornika niezabezpieczonego, 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ę do utworzenia 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 jego bezstanowego odpowiednika, ponieważ ServiceReplicaListener istnieje opcja otwierania replik ICommunicationListener 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 w replikach podstawowych.

Na przykład można mieć właściwość ServiceRemotingListener, która pobiera wywołania RPC tylko w replikach podstawowych, a drugi odbiornik niestandardowy, 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 uzyskiwać dostęp do zasobów punktu końcowego przydzielonych do niego z poziomu CodePackageActivationContext elementu w elemecie ServiceContext. Odbiornik może następnie rozpocząć nasłuchiwanie żądań po 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 obiekcie ServiceHost może współdzielić ten sam port. Oznacza to, że odbiornik komunikacji powinien obsługiwać udostępnianie portów. Zalecanym sposobem jest użycie przez odbiornik komunikacji identyfikatora partycji i identyfikatora repliki/wystąpienia podczas generowania adresu nasłuchiwania.

Rejestracja adresu usługi

Usługa systemowa o nazwie Usługa nazewnictwa 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. Po zakończeniu OpenAsync(C#) / openAsync(Java)ICommunicationListener(C#) / CommunicationListener(Java) metody zwracana wartość zostanie zarejestrowana w usłudze Nazewnictwa. Ta zwracana wartość opublikowana w usłudze Naming Service to ciąg, 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

Pełny przewodnik po sposobie pisania odbiornika komunikacji można znaleźć w temacie Service Fabric Web API services with OWIN self-hosting for C#, natomiast w przypadku języka Java można napisać własną implementację serwera HTTP, zobacz Przykład aplikacji EchoServer w temacie 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 do 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 to podstawowy element pierwotny, 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 nawiązać połączenie 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 nawiązać połączenie z usługami w innym klastrze, można utworzyć usługę ServicePartitionResolver z zestawem 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. 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 tworzenia elementu 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 zazwyczaj 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());

Następnie metoda rozpoznawania jest 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 liczba prac, aby upewnić się, że rozpoznany adres może być prawidłowo używany. Klient musi wykryć, czy próba połączenia nie powiodła się z powodu błędu przejściowego i może zostać ponowiona (np. usługa została przeniesiona lub jest tymczasowo niedostępna) lub stały błąd (np. usługa została usunięta lub żądany zasób już nie istnieje). Wystąpienia usługi lub repliki mogą przenosić się z węzła do węzła w dowolnym momencie z wielu powodów. Adres usługi rozwiązany za pośrednictwem usługi ServicePartitionResolver może być nieaktualny przez czas próby nawiązania połączenia z kodem klienta. W takim przypadku klient musi ponownie rozpoznać adres. Podanie poprzedniego ResolvedServicePartition wskazuje, że narzędzie rozpoznawania musi spróbować ponownie, a nie po prostu pobrać buforowany adres.

Zazwyczaj kod klienta nie musi działać bezpośrednio z usługą ServicePartitionResolver. Jest on tworzony i przekazywany do fabryk klientów komunikacji w interfejsie API usług Reliable Services. Fabryki używają narzędzia rozpoznawania wewnętrznego 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 dostarczania procedur obsługi błędów.

ICommunicationClientFactory(C#) / CommunicationClientFactory(Java) definiuje interfejs podstawowy zaimplementowany przez fabrykę klienta komunikacji, która tworzy klientów, którzy mogą komunikować się z usługą Service Fabric. Implementacja klasy CommunicationClientFactory zależy od stosu komunikacji używanego przez usługę Service Fabric, gdzie klient chce się komunikować. Interfejs API usług Reliable Services udostępnia element CommunicationClientFactoryBase<TCommunicationClient>. Zapewnia to 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 zwykle 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ązywania 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 przede wszystkim odpowiedzialna za tworzenie klientów komunikacyjnych. W przypadku klientów, którzy nie utrzymują trwałego połączenia, takiego jak klient HTTP, fabryka musi tworzyć i zwracać 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ę w celu ustalenia, 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 do podjęcia w przypadku wystąpienia wyjątku. Wyjątki są kategoryzowane w celu ponawiania próby i nie można ich ponowić.

  • Wyjątki nienależące do ponawiania po prostu ponownie są przywracane do elementu wywołującego.
  • wyjątki z możliwością ponawiania są dalej podzielone na przejściowe i nieprzejmienne.
    • Wyjątki przejściowe to te, które można po prostu ponowić bez ponownego rozpoznawania adresu punktu końcowego usługi. Obejmują one 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 te, które wymagają ponownego rozpoznania adresu punktu końcowego usługi. Należą do nich wyjątki wskazujące, że nie można osiągnąć punktu końcowego usługi, co oznacza, ż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, powinna 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 wszystkie ServicePartitionClient(C#) / FabricServicePartitionClient(Java) opakowuje je razem i zapewnia pętlę rozpoznawania adresów partycji 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