Skip to main content

Command Palette

Search for a command to run...

Jitter w .NET – jak rozkładać fale obciążeń

Updated
3 min read
Jitter w .NET – jak rozkładać fale obciążeń

Wprowadzenie

Systemy oparte na zdarzeniach często muszą radzić sobie z sytuacją, w której wiele procesów uruchamia się w tym samym momencie. Dzieje się tak na przykład wtedy, gdy zadania są planowane na określone godziny albo gdy klienci jednocześnie ponawiają żądania w przypadku niedostępności usługi.

Jeśli wszystkie zdarzenia wypadają w tych samych chwilach, serwer zostaje zalany falą obciążenia, tworząc gwałtowne piki (spikes). To zjawisko określa się mianem thundering herd problem.

Rozwiązaniem jest jitter – algorytm wprowadzający niewielki, losowy czas oczekiwania przed przetwarzaniem zdarzenia. Dzięki temu nacisk na system rozkłada się w czasie, a fale obciążenia przestają być groźne.

Skąd pochodzi jitter

Termin „jitter” pochodzi z telekomunikacji i oznacza zmienność opóźnienia sygnału. W transmisji danych – na przykład w rozmowie głosowej przez internet – informacje nie docierają w równych odstępach czasu. Jedna próbka dźwięku może przyjść po 20 ms, kolejna po 40 ms, a następna znowu szybciej. Dzieje się tak dlatego, że pakiety w sieci internetowej mogą iść różnymi trasami i napotykać odmienne opóźnienia. Takie zjawisko jest negatywne. Wprowadza dodatkową złożoność i pogarsza jakość transmisji.

W programowaniu jitter, wprowadzany w kontrolowany sposób jest czymś pozytywnym. Celowo implementujemy niewielkie rozchwianie czasów, by uniknąć sytuacji, w której wszystkie procesy wykonują się jednocześnie.

Redukcja skoków obciążenia w systemie monitorowania zużycia energii elektrycznej

Załóżmy, że tworzymy system monitorowania zużycia energii elektrycznej przez nasze urządzenia IoT. Nie mamy potrzeby dostarczania raportów na bieżąco. Zdecydowaliśmy, że każde urządzenie będzie wysyłać nam dane analityczne w nocy. Możemy założyć, że nasza firma odnosi sukcesy i liczba urządzeń u niezależnych klientów przekracza już 100 000. Jak rozłożyć przesyłanie danych do naszego systemu raportowania, tak by efektywnie zarządzić obciążeniem? Wprowadzenie jitter rozwiązuje ten problem.

Implementacja

Zamiast pozwalać wszystkim urządzeniom wysyłać dane dokładnie o północy, rozkładamy je na okno czasowe 00:00 – 01:00. Każde urządzenie dostaje indywidualne opóźnienie w tym przedziale.

Fixed Jitter

Idea: bazowy czas + losowa wartość z przedziału [0 - maxJitter]

/// <summary>
/// Fixed jitter that prevents server overload by spreading energy reports over time.
/// Each call waits for baseDelay plus a random amount up to maxJitter.
/// </summary>
/// <param name="baseDelay">Base wait time (always applied)</param>
/// <param name="maxJitter">Maximum additional random delay</param>
public class EnergyReportFixedJitter(TimeSpan baseDelay, TimeSpan maxJitter)
{
    private readonly TimeSpan baseDelay = baseDelay;
    private readonly TimeSpan maxJitter = maxJitter;
    private readonly Random random = new();

    /// <summary>
    /// Waits for baseDelay + random jitter (0 to maxJitter) before sending.
    /// </summary>
    public async Task SendWithJitterAsync()
    {
        var jitterMilliseconds = random.Next(0, (int)maxJitter.TotalMilliseconds);
        var totalDelay = baseDelay.Add(TimeSpan.FromMilliseconds(jitterMilliseconds));

        await Task.Delay(totalDelay);

        // Sends report now
    }
}

Percentage jitter

Idea: opóźnienie jest proporcją czasu bazowego, np. ±20%.

/// <summary>
/// Percentage jitter that prevents server overload by spreading energy reports over time.
/// Each call waits for baseDelay plus a random percentage of baseDelay.
/// </summary>
/// <param name="baseDelay">Base wait time (always applied)</param>
/// <param name="maxJitterPercentage">Maximum random percentage of baseDelay to add (0-100)</param>
public class EnergyReportPercentageJitter(TimeSpan baseDelay, int maxJitterPercentage)
{
    private readonly TimeSpan baseDelay = baseDelay;
    private readonly int maxJitterPercentage = maxJitterPercentage;
    private readonly Random random = new();

    /// <summary>
    /// Waits for baseDelay + random percentage jitter before sending.
    /// </summary>
    public async Task SendWithJitterAsync()
    {
        var jitterPercentage = random.Next(0, maxJitterPercentage + 1);
        var jitterMilliseconds = (int)(baseDelay.TotalMilliseconds * jitterPercentage / 100.0);
        var totalDelay = baseDelay.Add(TimeSpan.FromMilliseconds(jitterMilliseconds));

        await Task.Delay(totalDelay);

        // Sends report now
    }
}

Podsumowanie

Jitter to prosta, ale bardzo skuteczna technika pozwalająca uniknąć przeciążenia aplikacji w momentach skumulowanego ruchu. Zamiast pozwalać, by wszystkie zdarzenia były procesowane jednocześnie, celowo wprowadzamy niewielką losowość w harmonogramie lub czasie ponawiania. Dzięki temu serwer nie doświadcza gwałtownych pików obciążenia. Nasz system działa efektywnie, stabilnie i jego zachowanie jest przewidywalne.

Ten artykuł jest częścią serii. Przykładową implementację jitter oraz inne użyteczne algorytmy przy przetwarzaniu zdarzeń znajdziesz w repozytorium: useful-async-algorithms

Użyteczne algorytmy w systemach opartych na zdarzeniach

Part 2 of 3

W świecie .NET tworzymy systemy oparte na zdarzeniach. Reagujemy na pojawiające się informacje, przetwarzamy je asynchronicznie. W naszych rozwiązaniach często wracają te same problemy. Seria pokazuje praktyczne wzorce, które pomagają je rozwiązywać.

Up next

Debounce w .NET – jak zatrzymać lawinę zdarzeń

Wprowadzenie Czasem w systemie zaczyna pojawiać się zbyt dużo zdarzeń. Mogą być one powtarzalne, nadmiernie szczegółowe albo po prostu nieistotne w dużej liczbie. Taka lawina informacji przeciąża nasze rozwiązanie. Potrzebujemy mechanizmu, który ogr...