Pola Request-Reply asinkron

Azure
Azure Logic Apps

Memisahkan pemrosesan backend dari host frontend, di mana pemrosesan backend harus asinkron, tetapi frontend masih membutuhkan respons yang jelas.

Konteks dan masalah

Dalam pengembangan aplikasi modern, hal tersebut normal untuk aplikasi klien - sering kali kode berjalan di web-client (browser) - bergantung pada API jarak jauh untuk menyediakan logika bisnis dan menyusun fungsionalitas. API ini mungkin terkait langsung dengan aplikasi atau mungkin layanan bersama yang disediakan oleh pihak ketiga. Umumnya panggilan API ini terjadi melalui protokol HTTP (S) dan mengikuti semantik REST.

Dalam kebanyakan kasus, API untuk aplikasi klien dirancang untuk merespons dengan cepat, pada urutan 100 ms atau kurang. Banyak faktor yang dapat memengaruhi latensi respons, termasuk:

  • Tumpukan hosting aplikasi.
  • Komponen keamanan.
  • Lokasi geografis relatif pemanggil dan backend.
  • Infrastruktur jaringan.
  • Beban saat ini.
  • Ukuran payload permintaan.
  • Memproses panjang antrean.
  • Waktu bagi backend untuk memproses permintaan.

Salah satu faktor ini dapat menambah latensi pada respons. Beberapa dapat dikurangi dengan meluaskan skala backend. Lainnya, seperti infrastruktur jaringan, sebagian besar di luar kontrol pengembang aplikasi. Sebagian besar API dapat merespons dengan cukup cepat agar respons kembali melalui koneksi yang sama. Kode aplikasi dapat membuat panggilan API sinkron tanpa memblokir, memberikan tampilan pemrosesan asinkron, yang direkomendasikan untuk operasi terkait I/O.

Namun, dalam beberapa skenario, pekerjaan yang dilakukan oleh backend mungkin berjalan lama, pada urutan detik, atau mungkin merupakan proses latar belakang yang dijalankan dalam hitungan menit atau bahkan jam. Dalam hal ini, tidak mungkin menunggu pekerjaan selesai sebelum merespons permintaan. Situasi ini merupakan masalah potensial untuk setiap pola request-reply sinkron.

Beberapa arsitektur memecahkan masalah ini dengan menggunakan perantara pesan untuk memisahkan tahap permintaan dan respons. Pemisahan ini sering dicapai dengan menggunakan pola Tingkat Beban Berbasis Antrean. Pemisahan ini dapat memungkinkan penskalaan proses klien dan API backend secara independen. Namun, pemisahan ini juga membawa kompleksitas tambahan ketika klien membutuhkan pemberitahuan keberhasilan, karena langkah ini perlu menjadi asinkron.

Banyak pertimbangan yang sama yang dibahas untuk aplikasi klien juga berlaku untuk panggilan REST API antarserver dalam sistem terdistribusi — misalnya, dalam arsitektur layanan mikro.

Solusi

Salah satu solusi untuk masalah ini adalah dengan menggunakan polling HTTP. Polling berguna untuk kode sisi klien, karena sulit untuk menyediakan titik akhir panggilan balik atau menggunakan koneksi yang sudah berjalan lama. Bahkan ketika panggilan balik dimungkinkan, perpustakaan dan layanan tambahan yang diperlukan terkadang dapat menambah terlalu banyak kompleksitas ekstra.

  • Aplikasi klien membuat panggilan sinkron ke API, memicu operasi yang sudah berjalan lama di backend.

  • API merespons secara sinkron secepat mungkin. Ini mengembalikan kode status HTTP 202 (Accepted), mengakui bahwa permintaan telah diterima untuk diproses.

    Catatan

    API harus memvalidasi permintaan dan tindakan yang akan dilakukan sebelum memulai proses yang berjalan lama. Jika permintaan tidak valid, segera balas dengan kode kesalahan seperti HTTP 400 (Bad Request).

  • Respons tersebut menyimpan referensi lokasi yang menunjuk ke titik akhir yang dapat disurvei klien untuk memeriksa hasil operasi yang telah berjalan lama.

  • API membongkar pemrosesan ke komponen lain, seperti antrean pesan.

  • Untuk setiap panggilan yang berhasil ke titik akhir status, ia mengembalikan HTTP 200. Saat pekerjaan masih tertunda, titik akhir status mengembalikan sumber daya yang menunjukkan pekerjaan masih berlangsung. Setelah pekerjaan selesai, titik akhir status dapat mengembalikan sumber daya yang menunjukkan penyelesaian, atau mengalihkan ke URL sumber daya lain. Misalnya, jika operasi asinkron membuat sumber daya baru, titik akhir status akan mengarahkan ke URL untuk sumber daya tersebut.

Diagram berikut menunjukkan alur yang khas:

Alur permintaan dan respons untuk permintaan HTTP asinkron

  1. Klien mengirimkan permintaan dan menerima respons HTTP 202 (Accepted).
  2. Klien mengirimkan permintaan GET HTTP ke titik akhir status. Pekerjaan masih tertunda, sehingga panggilan ini mengembalikan HTTP 200.
  3. Pada titik tertentu, pekerjaan selesai dan titik akhir status mengembalikan 302 (Found) yang dialihkan ke sumber daya.
  4. Klien mengambil sumber daya di URL yang ditentukan.

Masalah dan pertimbangan

  • Ada sejumlah cara yang memungkinkan untuk menerapkan pola ini melalui HTTP dan tidak semua layanan upstram memiliki semantik yang sama. Misalnya, sebagian besar layanan tidak akan mengembalikan respons HTTP 202 kembali dari metode GET saat proses jarak jauh belum selesai. Mengikuti semantik REST murni, layanan tersebut harus mengembalikan HTTP 404 (Not Found). Respons ini masuk akal ketika Anda mempertimbangkan hasil panggilan belum ada.

  • Respons HTTP 202 harus menunjukkan lokasi dan frekuensi yang harus disurvei klien untuk respons. Ini harus memiliki header tambahan berikut:

    Header Deskripsi Catatan
    Lokasi URL yang harus disurveri klien untuk status respons. URL ini bisa menjadi token SAS dengan Pola Kunci Valet yang sesuai jika lokasi ini membutuhkan kontrol akses. Pola kunci valet juga berlaku ketika polling respons perlu dibongkar ke backend lain
    Retry-After Perkiraan waktu pemrosesan akan selesai Header ini dirancang untuk mencegah klien polling membanjiri back-end dengan retry.
  • Anda mungkin perlu menggunakan proksi pemrosesan atau fasad untuk memanipulasi header respons atau payload tergantung pada layanan yang mendasar yang digunakan.

  • Jika titik akhir status dialihkan setelah selesai, HTTP 302 atau HTTP 303 adalah kode pengembalian yang sesuai, tergantung pada semantik tepat yang Anda dukung.

  • Setelah pemrosesan berhasil, sumber daya yang ditentukan oleh header Lokasi harus mengembalikan kode respons HTTP yang sesuai seperti 200 (OK), 201 (Created), atau 204 (No Content).

  • Jika terjadi kesalahan selama pemrosesan, pertahankan kesalahan pada URL sumber daya yang dijelaskan di header Lokasi dan idealnya mengembalikan kode respons yang sesuai kepada klien dari sumber daya tersebut (kode 4xx).

  • Tidak semua solusi akan menerapkan pola ini dengan cara yang sama dan beberapa layanan akan mencakup header tambahan atau alternatif. Misalnya, Azure Resource Manager menggunakan varian modifikasi dari pola ini. Untuk informasi selengkapnya, lihat Operasi Asinkron Azure Resource Manager.

  • Klien lama mungkin tidak mendukung pola ini. Dalam hal ini, Anda mungkin perlu menempatkan fasad pada API asinkron untuk menyembunyikan pemrosesan asinkron dari klien asli. Misalnya, Azure Logic Apps mendukung pola ini yang secara asli dapat digunakan sebagai lapisan integrasi antara API asinkron dan klien yang melakukan panggilan sinkron. Lihat Jalankan tugas jangka panjang menggunakan pola tindakan webhook.

  • Dalam beberapa skenario, Anda mungkin ingin menyediakan cara bagi klien untuk membatalkan permintaan yang sudah berjalan lama. Dalam hal ini, layanan backend harus mendukung beberapa bentuk instruksi pembatalan.

Kapan menggunakan pola ini

Gunakan pola ini untuk:

  • Kode sisi klien, seperti aplikasi browser, ketika sulit untuk menyediakan titik akhir panggilan balik, atau penggunaan koneksi yang sudah berjalan lama menambah terlalu banyak kompleksitas tambahan.

  • Panggilan layanan ketika hanya protokol HTTP yang tersedia dan layanan pengembalian tidak dapat mengaktifkan panggilan balik karena pembatasan firewall di sisi klien.

  • Panggilan layanan yang perlu diintegrasikan dengan arsitektur lama yang tidak mendukung teknologi panggilan balik modern seperti WebSocket atau webhook.

Pola ini mungkin tidak cocok ketika:

  • Anda dapat menggunakan layanan yang dibuat untuk pemberitahuan asinkron sebagai gantinya, seperti Azure Event Grid.
  • Respons harus mengalir secara real time ke klien.
  • Klien perlu mengumpulkan banyak hasil, dan wajib menerima latensi dari hasil tersebut. Pertimbangkan pola bus layanan sebagai gantinya.
  • Anda dapat menggunakan koneksi jaringan persisten sisi server seperti WebSocket atau SignalR. Layanan ini dapat digunakan untuk memberiahukan hasilnya kepada pemanggil.
  • Rancangan jaringan memungkinkan Anda membuka port untuk menerima panggilan balik atau webhook asinkron.

Desain beban kerja

Arsitek harus mengevaluasi bagaimana pola Balasan Permintaan Asinkron dapat digunakan dalam desain beban kerja mereka untuk mengatasi tujuan dan prinsip yang tercakup dalam pilar Azure Well-Architected Framework. Contohnya:

Pilar Bagaimana pola ini mendukung tujuan pilar
Efisiensi Performa membantu beban kerja Anda memenuhi tuntutan secara efisien melalui pengoptimalan dalam penskalaan, data, kode. Memisahkan fase permintaan dan balasan interaksi untuk proses yang tidak memerlukan jawaban langsung meningkatkan respons dan skalabilitas sistem. Sebagai appproach asinkron, Anda dapat memaksimalkan konkurensi di sisi server dan menjadwalkan pekerjaan untuk diselesaikan sesuai kapasitas yang memungkinkan.

- PE:05 Penskalaan dan pemartisian
- Pe:07 Kode dan infrastruktur

Seperti halnya keputusan desain apa pun, pertimbangkan tradeoff terhadap tujuan pilar lain yang mungkin diperkenalkan dengan pola ini.

Contoh

Kode berikut menunjukkan kutipan dari aplikasi yang menggunakan Azure Functions untuk mengimplementasikan pola ini. Ada tiga fungsi dalam solusi:

  • Titik akhir API asinkron.
  • Titik akhir status.
  • Fungsi backend yang mengambil item kerja yang diantrekan dan menjalankannya.

Gambar struktur pola Request Reply Asinkron di Fungsi

Logo GitHub Sampel ini tersedia di GitHub.

Fungsi AsyncProcessingWorkAcceptor

Fungsi AsyncProcessingWorkAcceptor mengimplementasikan titik akhir yang menerima pekerjaan dari aplikasi klien dan menempatkannya pada antrean untuk diproses.

  • Fungsi ini menghasilkan ID permintaan dan menambahkannya sebagai metadata ke pesan antrean.
  • Respons HTTP termasuk header lokasi yang menunjuk ke titik akhir status. ID permintaan adalah bagian dari jalur URL.
public static class AsyncProcessingWorkAcceptor
{
    [FunctionName("AsyncProcessingWorkAcceptor")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] CustomerPOCO customer,
        [ServiceBus("outqueue", Connection = "ServiceBusConnectionAppSetting")] IAsyncCollector<ServiceBusMessage> OutMessages,
        ILogger log)
    {
        if (String.IsNullOrEmpty(customer.id) || string.IsNullOrEmpty(customer.customername))
        {
            return new BadRequestResult();
        }

        string reqid = Guid.NewGuid().ToString();

        string rqs = $"http://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{reqid}";

        var messagePayload = JsonConvert.SerializeObject(customer);
        var message = new ServiceBusMessage(messagePayload);
        message.ApplicationProperties.Add("RequestGUID", reqid);
        message.ApplicationProperties.Add("RequestSubmittedAt", DateTime.Now);
        message.ApplicationProperties.Add("RequestStatusURL", rqs);

        await OutMessages.AddAsync(message);

        return new AcceptedResult(rqs, $"Request Accepted for Processing{Environment.NewLine}ProxyStatus: {rqs}");
    }
}

Fungsi AsyncProcessingBackgroundWorker

Fungsi ini AsyncProcessingBackgroundWorker mengambil operasi dari antrean, melakukan beberapa pekerjaan berdasarkan payload pesan, dan menulis hasilnya ke akun penyimpanan.

public static class AsyncProcessingBackgroundWorker
{
    [FunctionName("AsyncProcessingBackgroundWorker")]
    public static async Task RunAsync(
        [ServiceBusTrigger("outqueue", Connection = "ServiceBusConnectionAppSetting")] BinaryData customer,
        IDictionary<string, object> applicationProperties,
        [Blob("data", FileAccess.ReadWrite, Connection = "StorageConnectionAppSetting")] BlobContainerClient inputContainer,
        ILogger log)
    {
        // Perform an actual action against the blob data source for the async readers to be able to check against.
        // This is where your actual service worker processing will be performed

        var id = applicationProperties["RequestGUID"] as string;

        BlobClient blob = inputContainer.GetBlobClient($"{id}.blobdata");

        // Now write the results to blob storage.
        await blob.UploadAsync(customer);
    }
}

Fungsi AsyncOperationStatusChecker

Fungsi AsyncOperationStatusChecker mengimplementasikan titik akhir status. Fungsi ini pertama-tama memeriksa apakah permintaan selesai

  • Jika permintaan selesai, fungsi mengembalikan kunci valet ke respons, atau segera mengalihkan panggilan ke URL kunci valet.
  • Jika permintaan masih tertunda, maka kita harus mengembalikan kode 200, termasuk status saat ini.
public static class AsyncOperationStatusChecker
{
    [FunctionName("AsyncOperationStatusChecker")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "RequestStatus/{thisGUID}")] HttpRequest req,
        [Blob("data/{thisGuid}.blobdata", FileAccess.Read, Connection = "StorageConnectionAppSetting")] BlockBlobClient inputBlob, string thisGUID,
        ILogger log)
    {

        OnCompleteEnum OnComplete = Enum.Parse<OnCompleteEnum>(req.Query["OnComplete"].FirstOrDefault() ?? "Redirect");
        OnPendingEnum OnPending = Enum.Parse<OnPendingEnum>(req.Query["OnPending"].FirstOrDefault() ?? "OK");

        log.LogInformation($"C# HTTP trigger function processed a request for status on {thisGUID} - OnComplete {OnComplete} - OnPending {OnPending}");

        // Check to see if the blob is present
        if (await inputBlob.ExistsAsync())
        {
            // If it's present, depending on the value of the optional "OnComplete" parameter choose what to do.
            return await OnCompleted(OnComplete, inputBlob, thisGUID);
        }
        else
        {
            // If it's NOT present, then we need to back off. Depending on the value of the optional "OnPending" parameter, choose what to do.
            string rqs = $"http://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{thisGUID}";

            switch (OnPending)
            {
                case OnPendingEnum.OK:
                    {
                        // Return an HTTP 200 status code.
                        return new OkObjectResult(new { status = "In progress", Location = rqs });
                    }

                case OnPendingEnum.Synchronous:
                    {
                        // Back off and retry. Time out if the backoff period hits one minute.
                        int backoff = 250;

                        while (!await inputBlob.ExistsAsync() && backoff < 64000)
                        {
                            log.LogInformation($"Synchronous mode {thisGUID}.blob - retrying in {backoff} ms");
                            backoff = backoff * 2;
                            await Task.Delay(backoff);
                        }

                        if (await inputBlob.ExistsAsync())
                        {
                            log.LogInformation($"Synchronous Redirect mode {thisGUID}.blob - completed after {backoff} ms");
                            return await OnCompleted(OnComplete, inputBlob, thisGUID);
                        }
                        else
                        {
                            log.LogInformation($"Synchronous mode {thisGUID}.blob - NOT FOUND after timeout {backoff} ms");
                            return new NotFoundResult();
                        }
                    }

                default:
                    {
                        throw new InvalidOperationException($"Unexpected value: {OnPending}");
                    }
            }
        }
    }

    private static async Task<IActionResult> OnCompleted(OnCompleteEnum OnComplete, BlockBlobClient inputBlob, string thisGUID)
    {
        switch (OnComplete)
        {
            case OnCompleteEnum.Redirect:
                {
                    // Redirect to the SAS URI to blob storage

                    return new RedirectResult(inputBlob.GenerateSASURI());
                }

            case OnCompleteEnum.Stream:
                {
                    // Download the file and return it directly to the caller.
                    // For larger files, use a stream to minimize RAM usage.
                    return new OkObjectResult(await inputBlob.DownloadContentAsync());
                }

            default:
                {
                    throw new InvalidOperationException($"Unexpected value: {OnComplete}");
                }
        }
    }
}

public enum OnCompleteEnum
{

    Redirect,
    Stream
}

public enum OnPendingEnum
{

    OK,
    Synchronous
}

Langkah berikutnya

Informasi berikut mungkin relevan saat menerapkan pola ini: