Scénář optimalizace výkonu: Streamování událostí s Azure Functions

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

Tento článek je součástí série. 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 vozový park dronů odesílá data o poloze v reálném čase do Azure IoT Hub. Aplikace Functions přijímá události, transformuje data do formátu GeoJSON a transformovaná data zapisuje do Cosmos DB. Cosmos Databáze má nativní podporu geoprostorovýchdat a kolekce Cosmos DB je možné indexovat pro efektivní prostorové dotazy. Klientská aplikace se může například dotazovat na všechny drony v okruhu 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ů. Konkrétně se při zpracování nesoučtou datové proudy, agregují data ani nezpracují v různých časových oknech. Na základě těchto požadavků Azure Functions vhodné pro zpracování zpráv. Cosmos Databáze se také může škálovat, aby podporovala velmi vysokou propustnost zápisu.

Monitorování propustnosti

Tento scénář představuje zajímavý problém s výkonem. Rychlost dat na zařízení je známá, ale počet zařízení může kolísá. V tomto obchodním scénáři nejsou požadavky na latenci zvláště přísné. Nahlášená pozice dronu musí být přesná jenom během minuty. Aplikace funkcí ale musí držet s průměrnou rychlostí příjmu dat v průběhu času.

IoT Hub ukládá zprávy do streamu protokolu. Příchozí zprávy se připojí na chvost datového proudu. Čtenář streamu v tomto případě řídí vlastní rychlost procházení datového proudu aplikací — — funkcí. Díky tomuto oddělení cest pro čtení a zápis IoT Hub velmi efektivní, ale také to znamená, že pomalý čtenář se může pohánět. Aby vývojový tým tuto podmínku detekovat, přidal vlastní metriku pro měření zpoždění zpráv. Tato metrika zaznamenává rozdíl mezi doručením zprávy do IoT Hub a přijetím zprávy ke zpracování funkcí.

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 služby Application Přehledy. Informace o použití TrackMetric uvnitř funkce Azure najdete v tématu Vlastní telemetrie ve funkci jazyka C#.

Pokud funkce udržuje přehled o objemu zpráv, měla by tato metrika zůstat v nízkém stabilním stavu. Některá latence je nevyhnutelná, takže hodnota nebude nikdy nulová. Pokud ale funkce poklesne, začne se ztácet rozdíl mezi časem zařazení do fronty a časem zpracování.

Test 1: Standardní hodnoty

První zátěžový test ukázal okamžitý problém: Aplikace funkcí ze služby Cosmos DB konzistentně přijímal chyby HTTP 429, což značí, že Cosmos DB omezováním požadavků na zápis.

Graph požadavků Cosmos DB

V reakci na to tým škáloval Cosmos DB zvýšením počtu PŘIDĚLENÝCH PRO KOLEKCI, ale chyby pokračovaly. To se zdál zvláštní, protože jejich výpočet na zadní straně obálky ukázal, Cosmos DB by neměla mít žádný problém držet krok s objemem žádostí o zápis.

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

Podíval jsem se Cosmos DB na teplou cestu. Je tu jedna věc, které nerozumím. Klíč oddílu je deliveryId, ale do databáze deliveryId se neposíleje Cosmos DB. Něco mi chybí?

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

Graph heat mapy Cosmos DB

Co chcete vidět na heat mapě, je rovnoměrně rozdělené mezi všechny oddíly. V tomto případě, protože se každý dokument zapisuje do stejného oddílu, nepomáhou přidání OU. Zjistilo se, že problém je chybou v kódu. Přestože Cosmos DB měla klíč oddílu, funkce Azure ve skutečnosti klíč oddílu v dokumentu nezahrnula. 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 spustili test, Cosmos databáze zastavila omezování. Chvíli to vypadalo dobře. Při určitém zatížení ale telemetrie ukázala, že funkce zapisuje méně dokumentů, než by měla. Následující graf znázorňuje zprávy, které přijímá IoT Hub a dokumenty zapsané do Cosmos DB. Žlutý řádek je počet přijatých zpráv na dávku 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 do databáze na dávku výrazně klesne přibližně v 7:30.

Graph zahozených zpráv

Další graf ukazuje latenci mezi doručením zprávy ze zařízení IoT Hub když aplikace funkcí tuto zprávu zpracuje. Vidíte, že ve stejný bod v čase se zpoždění dramaticky špičky, poklesy a poklesy odchýlnou.

Graph zpoždění zpráv

Důvodem, proč je hodnota ve špičce 5 minut a pak klesne na nulu, je to, že aplikace funkcí zahodí zprávy, které jsou opož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;
    }

To můžete vidět v grafu, když metrika zpoždění klesne zpět na nulu. Mezitím došlo ke ztrátě dat, protože funkce zahazuje zprávy.

Co se stalo? U tohoto konkrétního zátěžového testu měla kolekce Cosmos DB k dispozici, 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 nezapisuje dokumenty dostatečně rychle, aby zdržela příchozí objem zpráv. V průběhu času se propadl dál a pozadu.

Test 3: Paralelní zápisy

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

  • Zvyšte počet IoT Hub oddílů. Každému IoT Hub oddílu se přiřadí jedna instance funkce po druhé, takže očekáváme lineární škálování propustnosti s počtem oddílů.
  • Paralelizujte zápisy dokumentu v rámci funkce.

Aby tým prozkoumala druhou možnost, upravil funkci tak, aby podporovala paralelní zápisy. Původní verze funkce používala výstupní vazbu Cosmos DB. Optimalizovaná verze volá klienta Cosmos DB přímo a provádí zápisy paralelně pomocí metody 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 Cosmos db's 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);
}

Mějte na vědomí, že s přístupem jsou možné podmínky častých časů. Předpokládejme, že dvě zprávy ze stejného dronu dorazí do stejné dávky zpráv. Když je zapíšete paralelně, předchozí zpráva by mohla přepsat pozdější zprávu. V tomto konkrétním scénáři může aplikace tolerovat ztrátu občasné zprávy. Drony odesílaly nová data o poloze každých 5 sekund, takže se data ve Cosmos DB průběžně aktualizují. V jiných scénářích ale může být důležité zpracovávat zprávy striktně v pořadí.

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

Aspekty na straně klienta

Celková zkušenost klienta může být snížena agresivní paralelizací na straně serveru. Zvažte využití knihovny Bulk Executor služby Azure Cosmos DB (neuvedené v této implementaci), která významně snižuje výpočetní prostředky na straně klienta potřebné k nasytání propustnosti přidělené kontejneru Cosmos DB. Jedna aplikace s vláknem, která zapisuje data pomocí rozhraní API pro hromadný import, dosahuje téměř desetkrát větší propustnost zápisu v porovnání s vícevlovou aplikací, která zapisuje data paralelně a zároveň nasytá procesor klientského počítače.

Souhrn

V tomto scénáři byly zjištěny následující kritické body:

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

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

  • Omezování požadavků ve Cosmos DB.
  • Rozdělte heat — mapu na maximální spotřebované množství RU na oddíl.
  • Přijaté zprávy versus vytvořené dokumenty.
  • Zpoždění zpráv.

Další kroky

Kontrola antipatternů výkonu