Wzorzec kolejki priorytetowej

Azure Service Bus

Priorytetyzacja żądań wysyłanych do usługi, tak aby żądania o wyższym priorytecie były odbierane i przetwarzane znacznie szybciej niż te o niższym priorytecie. Ten wzorzec jest przydatny w aplikacjach, które oferują poszczególnym klientom różne gwarancje dotyczące poziomu usług.

Kontekst i problem

Aplikacje mogą delegować określone zadania do innych usług, na przykład w celu wykonywania przetwarzania w tle lub integrowania z innymi aplikacjami lub usługami. W chmurze kolejka komunikatów zazwyczaj służy do delegowania zadań w celu przetwarzania w tle. W wielu przypadkach kolejność odbierania żądań przez usługę nie jest ważna. W niektórych przypadkach konieczna jest priorytetyzacja określonych żądań. Te żądania powinny być przetwarzane wcześniej niż żądania o niższym priorytcie, które zostały wcześniej wysłane przez aplikację.

Rozwiązanie

Kolejka zazwyczaj jest strukturą typu pierwszy na początku, pierwszy na wyjęcie (FIFO), a odbiorcy zazwyczaj odbierają komunikaty w tej samej kolejności, w której są publikowane w kolejce. Jednak niektóre kolejki komunikatów obsługują komunikaty priorytetowe. Aplikacja publikującą komunikat może przypisać priorytet. Komunikaty w kolejce są automatycznie zmieniane tak, aby te, które mają wyższy priorytet, zostały odebrane przed tymi, które mają niższy priorytet. Na tym diagramie przedstawiono proces:

Diagram ilustrujący mechanizm kolejkowania obsługujący priorytetyzację komunikatów.

Uwaga

Większość implementacji kolejek komunikatów obsługuje wielu użytkowników. (Zobacz Wzorzec konkurujących odbiorców). Liczbę procesów konsumenckich można skalować w górę i w dół na podstawie zapotrzebowania.

W systemach, które nie obsługują kolejek komunikatów opartych na priorytetach, alternatywnym rozwiązaniem jest zachowanie oddzielnej kolejki dla każdego priorytetu. Aplikacja jest odpowiedzialna za publikowanie komunikatów w odpowiedniej kolejce. Każda kolejka może mieć oddzielną pulę odbiorców. Kolejki o wyższym priorytcie mogą mieć większą pulę użytkowników, którzy działają na szybszym sprzęcie niż kolejki o niższym priorycie. Na tym diagramie przedstawiono użycie oddzielnych kolejek komunikatów dla każdego priorytetu:

Diagram ilustrujący użycie oddzielnych kolejek komunikatów dla każdego priorytetu.

Odmianą tej strategii jest zaimplementowanie pojedynczej puli odbiorców, którzy najpierw sprawdzają komunikaty w kolejkach o wysokim priorytcie i dopiero po tym rozpoczęciu pobierania komunikatów z kolejek o niższym priorytcie. Istnieją pewne semantyczne różnice między rozwiązaniem, które używa pojedynczej puli procesów odbiorców (z pojedynczą kolejką, która obsługuje komunikaty o różnych priorytetach lub z wieloma kolejkami, które obsługują komunikaty o jednym priorytcie), a rozwiązaniem, które używa wielu kolejek z oddzielną pulą dla każdej kolejki.

W podejściu z jedną pulą komunikaty o wyższym priorytecie są zawsze odbierane i przetwarzane przed komunikatami o niższym priorytecie. Teoretycznie komunikaty o niskim priorytcie mogą być stale zastępowane i nigdy nie mogą być przetwarzane. W podejściu z wieloma pulami komunikaty o niższym priorytecie są zawsze przetwarzane, ale nie tak szybko, jak komunikaty o wyższym priorytecie (w zależności od względnego rozmiaru pul i dostępnych dla nich zasobów).

Korzystanie z mechanizmu kolejkowania priorytetowego może zapewnić następujące korzyści:

  • Umożliwia to aplikacjom spełnienie wymagań biznesowych, które wymagają priorytetyzacji dostępności lub wydajności, takich jak oferowanie różnych poziomów usług różnym grupom klientów.

  • Może pomóc zminimalizować koszty operacyjne. Jeśli używasz podejścia z jedną kolejką, możesz skalować z powrotem liczbę odbiorców, jeśli zajdzie taka potrzeba. Komunikaty o wysokim priorytcie są nadal przetwarzane jako pierwsze (chociaż prawdopodobnie wolniej) i komunikaty o niższym priorytcie mogą być opóźnione dłużej. W przypadku zaimplementowania podejścia z wieloma kolejkami komunikatów z oddzielnymi pulami odbiorców dla każdej kolejki można zmniejszyć pulę odbiorców dla kolejek o niższym priorytecie. Przetwarzanie niektórych kolejek o bardzo niskim priorycie można nawet wstrzymać, zatrzymując wszystkich użytkowników, którzy nasłuchują komunikatów w tych kolejkach.

  • Podejście oparte na kolejce z wieloma komunikatami może pomóc zmaksymalizować wydajność i skalowalność aplikacji, dzieląc komunikaty na podstawie wymagań dotyczących przetwarzania. Można na przykład określić priorytety zadań krytycznych, tak aby były obsługiwane przez odbiorniki uruchamiane natychmiast, a mniej ważne zadania w tle mogą być obsługiwane przez odbiorniki, które mają być uruchamiane w czasie, gdy są mniej zajęte.

Kwestie wymagające rozważenia

Podczas podejmowania decyzji o zaimplementowaniu tego wzorca należy wziąć pod uwagę następujące kwestie:

  • Zdefiniuj priorytety w kontekście rozwiązania. Na przykład komunikat o wysokim priorytcie można zdefiniować jako komunikat, który powinien zostać przetworzony w ciągu 10 sekund. Zidentyfikuj wymagania dotyczące obsługi elementów o wysokim priorytcie oraz zasoby, które należy przydzielić, aby spełnić kryteria.

  • Zdecyduj, czy wszystkie elementy o wysokim priorytekcie muszą być przetwarzane przed wszystkimi elementami o niższym priorytcie. Jeśli komunikaty są przetwarzane przez jedną pulę odbiorców, należy zapewnić mechanizm, który może wywłaszać i zawiesić zadanie obsługujące komunikat o niskim priorytcie, jeśli komunikat o wyższym priorytcie wchodzi do kolejki.

  • W podejściu do wielu kolejek, jeśli używasz pojedynczej puli procesów odbiorców, które nasłuchują we wszystkich kolejkach, a nie dedykowanej puli odbiorców dla każdej kolejki, konsument musi zastosować algorytm, który gwarantuje, że zawsze obsługuje komunikaty z kolejek o wyższym priorytecie przed komunikatami z kolejek o niższym priorytecie.

  • Monitoruj szybkość przetwarzania w kolejkach o wysokim i niskim priorytcie, aby upewnić się, że komunikaty w tych kolejkach są przetwarzane według oczekiwanych stawek.

  • Jeśli musisz zagwarantować, że komunikaty o niskim priorytecie zostaną przetworzone, zaimplementuj podejście do wielu kolejek komunikatów z wieloma pulami odbiorców. Alternatywnie w kolejce obsługującej priorytetyzację komunikatów można dynamicznie zwiększyć priorytet komunikatu w kolejce w miarę starzenia się. Ta metoda zależy jednak od kolejki komunikatów oferującej tę funkcję.

  • Strategia używania oddzielnych kolejek na podstawie priorytetu komunikatów jest zalecana dla systemów, które mają kilka dobrze zdefiniowanych priorytetów.

  • System może logicznie określać priorytety komunikatów. Na przykład zamiast jawnego komunikatu o wysokim i niskim priorytcie można wyznaczyć komunikaty jako "płacącego klienta" lub "niepłacącego klienta". System może następnie przydzielić więcej zasobów do przetwarzania komunikatów od płacących klientów.

  • Może istnieć koszt finansowy i koszt przetwarzania skojarzony z sprawdzaniem kolejki dla komunikatu. Na przykład niektóre systemy obsługi komunikatów komercyjnych pobierają niewielką opłatę za każdym razem, gdy komunikat jest publikowany lub pobierany, a za każdym razem, gdy kolejka jest odpytywana o komunikaty. Ten koszt zwiększa się podczas sprawdzania wielu kolejek.

  • Możesz dynamicznie dostosować rozmiar puli odbiorców na podstawie długości kolejki obsługiwanej przez pulę. Aby uzyskać więcej informacji, zobacz Wskazówki dotyczące skalowania automatycznego.

Kiedy używać tego wzorca

Ten wzorzec jest przydatny w scenariuszach, w których:

  • System musi obsługiwać wiele zadań o różnych priorytetach.

  • Różni użytkownicy lub dzierżawy powinni być obsługiwani z różnymi priorytetami.

Projekt obciążenia

Architekt powinien ocenić, w jaki sposób wzorzec kolejki priorytetowej może być używany w projekcie obciążenia, aby sprostać celom i zasadom opisanym w filarach platformy Azure Well-Architected Framework. Na przykład:

Filar Jak ten wzorzec obsługuje cele filaru
Decyzje projektowe dotyczące niezawodności pomagają obciążeniu stać się odporne na awarię i zapewnić, że zostanie przywrócony do w pełni funkcjonalnego stanu po wystąpieniu awarii. Oddzielenie elementów na podstawie priorytetu biznesowego umożliwia skoncentrowanie wysiłków związanych z niezawodnością na najbardziej krytycznej pracy.

- RE:02 Przepływy krytyczne
- RE:07 Zadania w tle
Wydajność pomagawydajnie sprostać zapotrzebowaniu dzięki optymalizacjom skalowania, danych, kodu. Oddzielenie elementów na podstawie priorytetu biznesowego umożliwia skoncentrowanie wysiłków związanych z wydajnością w najbardziej czasochłonnej pracy.

- PE:09 Przepływy krytyczne

Podobnie jak w przypadku każdej decyzji projektowej, należy rozważyć wszelkie kompromisy w stosunku do celów innych filarów, które mogą zostać wprowadzone przy użyciu tego wzorca.

Przykład

Platforma Azure nie udostępnia mechanizmu kolejkowania, który natywnie obsługuje automatyczne określanie priorytetów komunikatów za pośrednictwem sortowania. Jednak udostępnia ona tematy usługi Azure Service Bus, subskrypcje usługi Service Bus, które obsługują mechanizm kolejkowania, który zapewnia filtrowanie komunikatów, oraz szereg elastycznych funkcji, które sprawiają, że platforma Azure jest idealna dla większości implementacji kolejek priorytetowych.

Rozwiązanie platformy Azure może zaimplementować temat usługi Service Bus, do którego aplikacja może publikować komunikaty, tak samo jak w przypadku publikowania ich w kolejce. Komunikaty mogą zawierać metadane w postaci niestandardowych właściwości zdefiniowanych przez aplikację. Subskrypcje usługi Service Bus można skojarzyć z tematem, a subskrypcje mogą filtrować komunikaty na podstawie ich właściwości. Gdy aplikacja wysyła komunikat do tematu, komunikat jest kierowany do odpowiedniej subskrypcji, w której odbiorca może go odczytać. Procesy konsumenta mogą pobierać komunikaty z subskrypcji przy użyciu tej samej semantyki, której będą używać z kolejką komunikatów. (Subskrypcja jest kolejką logiczną). Na tym diagramie pokazano, jak zaimplementować kolejkę priorytetową przy użyciu tematów i subskrypcji usługi Service Bus:

Diagram przedstawiający sposób implementowania kolejki priorytetu przy użyciu tematów i subskrypcji usługi Service Bus.

Na powyższym diagramie aplikacja tworzy kilka komunikatów i przypisuje właściwość niestandardową o nazwie Priority w każdym komunikacie. Priority ma wartość High lub Low. Aplikacja publikuje te komunikaty w temacie. Temat zawiera dwie skojarzone subskrypcje, które filtruje komunikaty na Priority podstawie właściwości. Jedna subskrypcja akceptuje komunikaty z właściwością ustawioną Priority na High. Druga akceptuje komunikaty z właściwością ustawioną Priority na Low. Pula odbiorców odczytuje komunikaty z każdej subskrypcji. Subskrypcja o wysokim priorytcie ma większą pulę, a użytkownicy ci mogą działać na bardziej zaawansowanych komputerach, które mają więcej dostępnych zasobów niż komputery w puli o niskim priorytcie.

W tym przykładzie nie ma żadnych specjalnych informacji o oznaczeniu komunikatów o wysokim i niskim priorytcie. Są to po prostu etykiety, które są określane jako właściwości w każdym komunikacie. Są one używane do kierowania komunikatów do określonej subskrypcji. Jeśli potrzebne są dodatkowe priorytety, stosunkowo łatwo jest utworzyć więcej subskrypcji i pul procesów konsumenckich do obsługi tych priorytetów.

Rozwiązanie PriorityQueue w usłudze GitHub jest oparte na tym podejściu. To rozwiązanie zawiera projekty funkcji platformy Azure o nazwach PriorityQueueConsumerHigh i PriorityQueueConsumerLow. Te projekty funkcji platformy Azure integrują się z usługą Service Bus za pośrednictwem wyzwalaczy i powiązań. Łączą się z różnymi subskrypcjami zdefiniowanymi w ServiceBusTrigger programie i reagują na przychodzące komunikaty.

public static class PriorityQueueConsumerHighFn
{
    [FunctionName("HighPriorityQueueConsumerFunction")]
    public static void Run(
      [ServiceBusTrigger("messages", "highPriority", Connection = "ServiceBusConnection")]string highPriorityMessage,
      ILogger log)
    {
        log.LogInformation($"C# ServiceBus topic trigger function processed message: {highPriorityMessage}");
    }
}

Jako administrator możesz skonfigurować liczbę wystąpień funkcji w usłudze aplikacja systemu Azure Service, na które można skalować w poziomie. Można to zrobić, konfigurując opcję Wymuszaj limit skalowania w poziomie w witrynie Azure Portal, ustawiając maksymalny limit skalowania w poziomie dla każdej funkcji. Zazwyczaj trzeba mieć więcej wystąpień PriorityQueueConsumerHigh funkcji niż PriorityQueueConsumerLow funkcja. Ta konfiguracja gwarantuje, że komunikaty o wysokim priorytcie są odczytywane z kolejki szybciej niż komunikaty o niskim priorytcie.

Inny projekt , PriorityQueueSenderzawiera funkcję platformy Azure wyzwalaną czasowo, która jest skonfigurowana do uruchamiania co 30 sekund. Ta funkcja integruje się z usługą Service Bus za pośrednictwem powiązania wyjściowego i wysyła partie komunikatów o niskim i wysokim priorytcie do IAsyncCollector obiektu. Gdy funkcja publikuje komunikaty do tematu skojarzonego z subskrypcjami używanymi przez PriorityQueueConsumerHigh funkcje i PriorityQueueConsumerLow , określa priorytet przy użyciu właściwości niestandardowej Priority , jak pokazano poniżej:

public static class PriorityQueueSenderFn
{
    [FunctionName("PriorityQueueSenderFunction")]
    public static async Task Run(
        [TimerTrigger("0,30 * * * * *")] TimerInfo myTimer,
        [ServiceBus("messages", Connection = "ServiceBusConnection")] IAsyncCollector<ServiceBusMessage> collector)
    {
        for (int i = 0; i < 10; i++)
        {
            var messageId = Guid.NewGuid().ToString();
            var lpMessage = new ServiceBusMessage() { MessageId = messageId };
            lpMessage.ApplicationProperties["Priority"] = Priority.Low;
            lpMessage.Body = BinaryData.FromString($"Low priority message with Id: {messageId}");
            await collector.AddAsync(lpMessage);

            messageId = Guid.NewGuid().ToString();
            var hpMessage = new ServiceBusMessage() { MessageId = messageId };
            hpMessage.ApplicationProperties["Priority"] = Priority.High;
            hpMessage.Body = BinaryData.FromString($"High priority message with Id: {messageId}");
            await collector.AddAsync(hpMessage);
        }
    }
}

Następne kroki

Podczas implementowania tego wzorca przydatne mogą być następujące zasoby:

  • Przykład przedstawiający ten wzorzec w usłudze GitHub.

  • Asynchroniczne podstawy obsługi komunikatów. Usługa konsumenta przetwarzająca żądanie może być zmuszona wysłać odpowiedź do wystąpienia aplikacji, które opublikowało żądanie. Ten artykuł zawiera informacje o strategiach, których można użyć do implementowania komunikatów żądań/odpowiedzi.

  • Wskazówki dotyczące skalowania automatycznego. Czasami można skalować rozmiar puli procesów konsumenckich, które obsługują kolejkę na podstawie długości kolejki. Ta strategia może pomóc zwiększyć wydajność, szczególnie w przypadku pul obsługujących komunikaty o wysokim priorytcie.

Podczas implementowania tego wzorca mogą być przydatne następujące wzorce:

  • Wzorzec konkurujących odbiorców. Aby zwiększyć przepływność kolejek, można zaimplementować wielu odbiorców, którzy nasłuchują równolegle w tej samej kolejce i przetwarzają zadania. Ci konsumenci konkurują o komunikaty, ale tylko jeden powinien być w stanie przetworzyć każdy komunikat. Ten artykuł zawiera więcej informacji na temat korzyści i wad wdrażania tego podejścia.

  • Wzorzec ograniczania przepływności. Ograniczanie przepływności można zaimplementować za pomocą kolejek. Możesz użyć komunikatów priorytetowych, aby upewnić się, że żądania z krytycznych aplikacji lub aplikacji uruchamianych przez klientów o wysokiej wartości mają pierwszeństwo przed żądaniami z mniej ważnych aplikacji.