Penyetelan performa - Streaming peristiwa

Azure Functions
Azure IoT Hub
Azure Cosmos DB

Artikel ini menjelaskan bagaimana tim pengembangan menggunakan metrik untuk menemukan penyempitan dan meningkatkan performa sistem terdistribusi. Artikel ini didasarkan pada pengujian beban aktual yang kami lakukan untuk aplikasi contoh.

Artikel ini adalah bagian dari seri. Baca bagian pertama di sini.

Skenario: Memproses aliran peristiwa menggunakan Azure Functions.

Diagram arsitektur streaming peristiwa

Dalam skenario ini, armada drone mengirimkan data posisi secara real time ke Azure IoT Hub. Aplikasi Functions menerima peristiwa, mengubah data menjadi format GeoJSON , dan menulis data yang diubah ke Azure Cosmos DB. Azure Cosmos DB memiliki dukungan asli untuk data geospasial, dan koleksi Azure Cosmos DB dapat diindeks untuk kueri spasial yang efisien. Misalnya, aplikasi klien dapat meminta semua drone dalam jarak 1 km dari lokasi tertentu, atau menemukan semua drone di area tertentu.

Persyaratan pemrosesan ini cukup sederhana sehingga tidak memerlukan mesin pemrosesan aliran penuh. Secara khusus, pemrosesan tidak bergabung dengan aliran, data agregat, atau proses di seluruh jendela waktu. Berdasarkan persyaratan ini, Azure Functions cocok untuk memproses pesan. Azure Cosmos DB juga dapat menskalakan untuk mendukung throughput tulis yang sangat tinggi.

Throughput pemantauan

Skenario ini menghadirkan tantangan performa yang menarik. Tingkat data per perangkat diketahui, tetapi jumlah perangkat mungkin berfluktuasi. Untuk skenario bisnis ini, persyaratan latensi tidak terlalu ketat. Posisi drone yang dilaporkan hanya perlu akurat dalam satu menit. Dikatakan bahwa aplikasi fungsi harus mengikuti tingkat konsumsi rata-rata dari waktu ke waktu.

IoT Hub menyimpan pesan dalam aliran log. Pesan masuk ditambahkan ke ekor aliran. Pembaca aliran - dalam hal ini, aplikasi fungsi - mengontrol lajunya sendiri untuk melintasi aliran. Pemisahan jalur baca dan tulis ini membuat IoT Hub sangat efisien, tetapi juga berarti bahwa pembaca yang lambat dapat tertinggal. Untuk mendeteksi kondisi ini, tim pengembangan menambahkan metrik kustom untuk mengukur keterlambatan pesan. Metrik ini mencatat delta antara saat pesan tiba di IoT Hub, dan saat fungsi menerima pesan untuk diproses.

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);
}

Metode TrackMetric menulis metrik kustom ke Application Insights. Untuk informasi tentang penggunaan TrackMetric di dalam Azure Function, lihat Telemetri kustom di fungsi C#.

Jika fungsi mengikuti volume pesan, metrik ini harus tetap pada keadaan stabil yang rendah. Beberapa latensi tidak dapat dihindari, sehingga nilainya tidak akan pernah nol. Tetapi jika fungsi tertinggal, delta antara waktu yang diantre dan waktu pemrosesan akan mulai naik.

Uji 1: Garis Besar

Pengujian beban pertama menunjukkan masalah langsung: Aplikasi Fungsi secara konsisten menerima kesalahan HTTP 429 dari Azure Cosmos DB, yang menunjukkan bahwa Azure Cosmos DB membatasi permintaan tulis.

Grafik permintaan yang dibatasi Azure Cosmos DB

Sebagai tanggapan, tim menskalakan Azure Cosmos DB dengan meningkatkan jumlah RU yang dialokasikan untuk koleksi, tetapi kesalahan berlanjut. Ini tampak aneh, karena perhitungan back-of-amplop mereka menunjukkan bahwa Azure Cosmos DB seharusnya tidak memiliki masalah mengikuti volume permintaan tulis.

Kemudian pada hari itu, salah satu pengembang mengirim email berikut ke tim:

Saya melihat Azure Cosmos DB untuk jalur hangat. Ada satu hal yang saya tidak mengerti. Kunci partisi adalah deliveryId, namun kami tidak mengirim deliveryId ke Azure Cosmos DB. Apakah saya melewatkan sesuatu?

Itu petunjuknya. Melihat peta panas partisi, ternyata semua dokumen mendarat di partisi yang sama.

Grafik peta panas partisi Azure Cosmos DB

Apa yang ingin Anda lihat di peta panas adalah distribusi yang merata di semua partisi. Dalam hal ini, karena setiap dokumen ditulis ke partisi yang sama, menambahkan RU tidak membantu. Masalahnya ternyata bug dalam kode. Meskipun koleksi Azure Cosmos DB memiliki kunci partisi, Azure Function sebenarnya tidak menyertakan kunci partisi dalam dokumen. Untuk informasi selengkapnya tentang peta panas partisi, lihat Menentukan distribusi throughput di seluruh partisi.

Uji 2: Memperbaiki masalah partisi

Ketika tim menyebarkan perbaikan kode dan menjalankan kembali pengujian, Azure Cosmos DB berhenti melakukan pembatasan. Untuk sementara, semuanya tampak bagus. Tetapi pada beban tertentu, telemetri menunjukkan bahwa fungsi tersebut menulis lebih sedikit dokumen yang seharusnya. Grafik berikut menunjukkan pesan yang diterima dari IoT Hub versus dokumen yang ditulis ke Azure Cosmos DB. Garis kuning adalah jumlah pesan yang diterima per batch, dan hijau adalah jumlah dokumen yang ditulis per batch. Ini harus proporsional. Sebaliknya, jumlah operasi penulisan database per batch turun secara signifikan sekitar pukul 07:30.

Grafik pesan yang dihilangkan

Grafik berikutnya menunjukkan latensi antara saat pesan tiba di IoT Hub dari perangkat, dan saat aplikasi fungsi memproses pesan tersebut. Anda dapat melihat bahwa pada titik waktu yang sama, keterlambatan melonjak secara dramatis, rata, lalu menurun.

Grafik keterlambatan pesan

Alasan nilai memuncak pada 5 menit dan kemudian turun ke nol adalah karena aplikasi fungsi membuang pesan yang terlambat lebih dari 5 menit:

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;
    }
}

Anda dapat melihat ini di grafik ketika metrik keterlambatan turun kembali ke nol. Sementara itu, data telah hilang, karena fungsinya membuang pesan.

Apa yang terjadi? Untuk pengujian beban khusus ini, koleksi Azure Cosmos DB memiliki RU untuk cadangan, sehingga hambatan tidak ada di database. Sebaliknya, masalahnya ada di perulangan pemrosesan pesan. Sederhananya, fungsi itu tidak menulis dokumen cukup cepat untuk mengikuti volume pesan yang masuk. Seiring waktu, itu jatuh semakin jauh di belakang.

Uji 3: Penulisan paralel

Jika waktu untuk memproses pesan terhambat, salah satu solusinya adalah memproses lebih banyak pesan secara paralel. Dalam skenario ini:

  • Tingkatkan jumlah partisi IoT Hub. Setiap partisi IoT Hub diberi satu instans fungsi pada satu waktu, jadi kami berharap throughput akan diskalakan secara linear dengan jumlah partisi.
  • Sejajarkan penulisan dokumen dalam fungsi.

Untuk mengeksplorasi opsi kedua, tim memodifikasi fungsi untuk mendukung penulisan paralel. Versi asli fungsi menggunakan pengikatan output Azure Cosmos DB. Versi yang dioptimalkan memanggil klien Azure Cosmos DB secara langsung dan melakukan penulisan secara paralel menggunakan 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);
}

Perhatikan bahwa kondisi race dimungkinkan dengan pendekatan. Misalkan dua pesan dari drone yang sama kebetulan tiba dalam kumpulan pesan yang sama. Dengan menulisnya secara paralel, pesan sebelumnya dapat menimpa pesan selanjutnya. Untuk skenario khusus ini, aplikasi dapat mentolerir kehilangan pesan sesekali. Drone mengirim data posisi baru setiap 5 detik, sehingga data di Azure Cosmos DB diperbarui terus-menerus. Namun, dalam skenario lain, mungkin penting untuk memproses pesan secara ketat.

Setelah menyebarkan perubahan kode ini, aplikasi mampu menyerap lebih dari 2500 permintaan / detik, menggunakan IoT Hub dengan 32 partisi.

Pertimbangan sisi klien

Pengalaman klien secara keseluruhan mungkin berkurang oleh paralelisasi agresif di sisi server. Pertimbangkan untuk menggunakan pustaka pelaksana massal Azure Cosmos DB (tidak ditunjukkan dalam implementasi ini) yang secara signifikan mengurangi sumber daya komputasi sisi klien yang diperlukan untuk memenuhi throughput yang dialokasikan ke kontainer Azure Cosmos DB. Aplikasi berulir tunggal yang menulis data menggunakan API impor massal mencapai throughput tulis hampir sepuluh kali lebih besar jika dibandingkan dengan aplikasi berulir banyak yang menulis data secara paralel sambil menjenuhkan CPU mesin klien.

Ringkasan

Untuk skenario ini, penyempitan berikut diidentifikasi:

  • Partisi tulis panas, karena nilai kunci partisi yang hilang dalam dokumen yang ditulis.
  • Menulis dokumen secara serial per partisi IoT Hub.

Untuk mendiagnosis masalah ini, tim pengembangan mengandalkan metrik berikut:

  • Permintaan yang dibatasi di Azure Cosmos DB.
  • Peta panas partisi — RU maksimum yang digunakan per partisi.
  • Pesan yang diterima versus dokumen yang dibuat.
  • Keterlambatan pesan.

Langkah berikutnya

Ulas antipola performa