Ladění výkonu – Streamování událostí

Azure Functions
Azure IoT Hub
Azure Cosmos DB

Tento článek popisuje, jak vývojový tým použil metriky k nalezení kritických bodů a zlepšení výkonu distribuovaného systému. Článek je založený na skutečném zátěžovém testování, které jsme provedli pro ukázkovou aplikaci.

Tento článek je součástí série článků. První část si můžete přečíst tady.

Scénář: Zpracování streamu událostí pomocí Azure Functions

Diagram architektury streamování událostí

V tomto scénáři flotila dronů odesílá data o poloze v reálném čase, aby Azure IoT Hub. Aplikace Functions přijímá události, transformuje data do formátu GeoJSON a zapisuje transformovaná data do služby Azure Cosmos DB. Azure Cosmos DB má nativní podporu geoprostorových dat a kolekce Azure Cosmos DB je možné indexovat pro efektivní prostorové dotazy. Klientská aplikace může například dotazovat všechny drony do 1 km od daného umístění nebo najít všechny drony v určité oblasti.

Tyto požadavky na zpracování jsou natolik jednoduché, že nevyžadují plnohodnotný modul pro zpracování datových proudů. Zpracování zejména nespojuje datové proudy, neagreguje data ani nezpracuje v časových oknech. Na základě těchto požadavků je Azure Functions vhodný pro zpracování zpráv. Azure Cosmos DB se také může škálovat, aby podporovala velmi vysokou propustnost zápisu.

Monitorování propustnosti

Tento scénář představuje zajímavou výzvu z hlediska výkonu. Rychlost dat na zařízení je známá, ale počet zařízení může kolísat. Pro tento obchodní scénář nejsou požadavky na latenci nijak zvlášť přísné. Ohlášená poloha dronu musí být přesná pouze do minuty. Aplikace funkcí ale musí držet krok s průměrnou mírou příjmu dat v průběhu času.

IoT Hub ukládá zprávy do streamu protokolu. Příchozí zprávy se připojují na konec datového proudu. Čtenář streamu – v tomto případě aplikace funkcí – řídí vlastní rychlost procházení streamu. Díky tomuto oddělení cest čtení a zápisu je IoT Hub velmi efektivní, ale také to znamená, že pomalá čtečka může zaostává. Aby vývojový tým tuto podmínku zjistil, přidal vlastní metriku pro měření zpoždění zpráv. Tato metrika zaznamenává rozdíl mezi příchodem zprávy na IoT Hub a okamžikem, kdy funkce přijme zprávu ke zpracování.

var ticksUTCNow = DateTimeOffset.UtcNow;

// Track whether messages are arriving at the function late.
DateTime? firstMsgEnqueuedTicksUtc = messages[0]?.EnqueuedTimeUtc;
if (firstMsgEnqueuedTicksUtc.HasValue)
{
    CustomTelemetry.TrackMetric(
                        context,
                        "IoTHubMessagesReceivedFreshnessMsec",
                        (ticksUTCNow - firstMsgEnqueuedTicksUtc.Value).TotalMilliseconds);
}

Metoda TrackMetric zapíše vlastní metriku do Application Insights. Informace o použití TrackMetric ve funkci Azure Functions najdete v tématu Vlastní telemetrie ve funkci jazyka C#.

Pokud funkce drží krok s objemem zpráv, měla by tato metrika zůstat v nízkém stabilním stavu. Určitá latence je nevyhnutelná, takže hodnota nebude nikdy nulová. Pokud ale funkce zaostává, rozdíl mezi časem zařazení do fronty a časem zpracování se začne zhoršovat.

Test 1: Směrný plán

První zátěžový test ukázal okamžitý problém: Aplikace funkcí konzistentně přijímala chyby HTTP 429 ze služby Azure Cosmos DB, což značí, že Služba Azure Cosmos DB omezovala požadavky na zápis.

Graf omezených požadavků služby Azure Cosmos DB

V reakci na to tým škáloval službu Azure Cosmos DB zvýšením počtu RU přidělených kolekci, ale chyby pokračovaly. To vypadalo divně, protože jejich výpočet zpětné obálky ukázal, že azure Cosmos DB by neměl mít problém udržet krok s objemem žádostí o zápis.

Později ten den poslal jeden z vývojářů týmu následující e-mail:

Podíval jsem se na Azure Cosmos DB na teplou cestu. Je tu jedna věc, které nerozumím. Klíč oddílu je deliveryId, ale id doručení do služby Azure Cosmos DB neodesíláme. Chybí mi něco?

To bylo vodítko. Při pohledu na heat mapu oddílů se ukázalo, že všechny dokumenty přistály ve stejném oddílu.

Graf heat mapy oddílů služby Azure Cosmos DB

To, co chcete vidět v heat mapě, je rovnoměrné rozdělení napříč všemi oddíly. Protože se v tomto případě každý dokument zapisoval do stejného oddílu, přidání RU nepomohlo. Ukázalo se, že problém je chyba v kódu. I když kolekce Azure Cosmos DB měla klíč oddílu, funkce Azure Ve skutečnosti tento klíč oddílu do dokumentu nezahrnovala. Další informace o heat mapě oddílů najdete v tématu Určení distribuce propustnosti mezi oddíly.

Test 2: Oprava problému s dělením

Když tým nasadil opravu kódu a znovu spustil test, Azure Cosmos DB zastavilo omezování. Chvíli všechno vypadalo dobře. Při určitém zatížení ale telemetrie ukázala, že funkce píše méně dokumentů, než by měla. Následující graf znázorňuje zprávy přijaté z IoT Hub a dokumenty zapsané do služby Azure Cosmos DB. Žlutá čára je počet přijatých zpráv v dávce a zelená je počet dokumentů napsaných v dávce. Ty by měly být proporcionální. Místo toho počet operací zápisu databáze na dávku výrazně klesne přibližně v 07:30.

Graf vynechaných zpráv

Další graf ukazuje latenci mezi okamžikem, kdy zpráva přijde na IoT Hub ze zařízení, a když aplikace funkcí tuto zprávu zpracuje. Můžete vidět, že ve stejném okamžiku se zpoždění výrazně zhrouceně, slábnou se a poklesy.

Graf zpoždění zpráv

Důvodem, proč hodnota dosáhne vrcholu po 5 minutách a pak klesne na nulu, je to, že aplikace funkcí zahodí zprávy, které jsou zpožděné o více než 5 minut:

foreach (var message in messages)
{
    // Drop stale messages,
    if (message.EnqueuedTimeUtc < cutoffTime)
    {
        log.Info($"Dropping late message batch. Enqueued time = {message.EnqueuedTimeUtc}, Cutoff = {cutoffTime}");
        droppedMessages++;
        continue;
    }
}

Můžete to vidět v grafu, když metrika lateness klesne zpět na nulu. Mezitím došlo ke ztrátě dat, protože funkce zahazovala zprávy.

Co se stalo? Pro tento konkrétní zátěžový test měla kolekce Azure Cosmos DB jednotky RU, které bylo potřeba ušetřit, takže kritický bod nebyl v databázi. Problém byl spíše ve smyčce zpracování zpráv. Jednoduše řečeno, funkce nezapisovala dokumenty dostatečně rychle, aby udržela krok s příchozím objemem zpráv. Postupem času se to chytlo dál a dál za sebou.

Test 3: Paralelní zápisy

Pokud je kritickým bodem doba zpracování zprávy, jedním z řešení je paralelní zpracování více zpráv. V tomto scénáři:

  • Zvyšte počet oddílů IoT Hub. Každému IoT Hub oddílu se vždy přiřadí jedna instance funkce, takže bychom očekávali, že propustnost bude škálovat lineárně s počtem oddílů.
  • Paralelizovat zápisy dokumentů v rámci funkce.

Při zkoumání druhé možnosti tým upravil funkci tak, aby podporovala paralelní zápisy. Původní verze funkce používala výstupní vazbu Služby Azure Cosmos DB. Optimalizovaná verze volá klienta služby Azure Cosmos DB přímo a provádí zápisy paralelně pomocí task.WhenAll:

private async Task<(long documentsUpserted,
                    long droppedMessages,
                    long cosmosDbTotalMilliseconds)>
                ProcessMessagesFromEventHub(
                    int taskCount,
                    int numberOfDocumentsToUpsertPerTask,
                    EventData[] messages,
                    TraceWriter log)
{
    DateTimeOffset cutoffTime = DateTimeOffset.UtcNow.AddMinutes(-5);

    var tasks = new List<Task>();

    for (var i = 0; i < taskCount; i++)
    {
        var docsToUpsert = messages
                            .Skip(i * numberOfDocumentsToUpsertPerTask)
                            .Take(numberOfDocumentsToUpsertPerTask);
        // client will attempt to create connections to the data
        // nodes on Azure Cosmos DB clusters on a range of port numbers
        tasks.Add(UpsertDocuments(i, docsToUpsert, cutoffTime, log));
    }

    await Task.WhenAll(tasks);

    return (this.UpsertedDocuments,
            this.DroppedMessages,
            this.CosmosDbTotalMilliseconds);
}

Všimněte si, že podmínky časování jsou možné s přístupem. Předpokládejme, že ve stejné dávce zpráv přijdou dvě zprávy ze stejného dronu. Jejich paralelním zápisem by dřívější zpráva mohla přepsat pozdější zprávu. Pro tento konkrétní scénář může aplikace tolerovat občasnou ztrátu zprávy. Drony odesílají nová data o poloze každých 5 sekund, takže se data ve službě Azure Cosmos DB průběžně aktualizují. V jiných scénářích ale může být důležité zpracovávat zprávy přísně v pořadí.

Po nasazení této změny kódu byla aplikace schopna ingestovat více než 2500 požadavků za sekundu pomocí IoT Hub s 32 oddíly.

Důležité informace na straně klienta

Celkové prostředí klienta může snížit agresivní paralelizace na straně serveru. Zvažte použití knihovny hromadného exekutoru služby Azure Cosmos DB (která se v této implementaci nezobrazuje), která výrazně snižuje výpočetní prostředky na straně klienta potřebné k nasycení propustnosti přidělené kontejneru služby Azure Cosmos DB. Jednovláknová aplikace, která zapisuje data pomocí rozhraní API pro hromadný import, dosahuje téměř desetkrát větší propustnosti zápisu v porovnání s aplikací s více vlákny, která zapisuje data paralelně a současně saturuje procesor klientského počítače.

Souhrn

Pro tento scénář byly identifikovány následující kritické body:

  • Horký oddíl zápisu kvůli chybějící hodnotě klíče oddílu v zapisovaných dokumentech.
  • Psaní dokumentů sériově podle IoT Hub oddílu

Při diagnostice těchto problémů vývojový tým spoléhal na následující metriky:

  • Omezené požadavky ve službě Azure Cosmos DB
  • Heat map oddílu – maximální spotřeba RU na oddíl.
  • Přijaté zprávy a vytvořené dokumenty
  • Zpoždění zpráv.

Další kroky

Kontrola antipatternů výkonu