نمط Request-Reply غير متزامن

Azure
Azure Logic Apps

افصل معالجة الواجهة الخلفية عن مضيف الواجهة الأمامية، حيث يجب أن تكون معالجة الواجهة الخلفية غير متزامنة، لكن الواجهة الأمامية لا تزال بحاجة إلى استجابة واضحة.

السياق والمشكلة

في تطوير التطبيقات الحديثة، من الطبيعي أن تعتمد تطبيقات العميل - غالبًا ما تعمل التعليمات البرمجية في عميل الويب (المتصفح) - على واجهات برمجة التطبيقات البعيدة لتوفير منطق العمل وإنشاء الوظائف. قد تكون واجهات برمجة التطبيقات هذه مرتبطة مباشرة بالتطبيق أو قد تكون خدمات مشتركة تقدمها جهة خارجية. عادة ما تتم استدعاءات واجهة برمجة التطبيقات هذه عبر بروتوكول HTTP (S) واتبع دلالات REST.

في معظم الحالات، تم تصميم واجهات برمجة التطبيقات لتطبيق العميل للاستجابة بسرعة، بترتيب 100 مللي ثانية أو أقل. يمكن أن تؤثر العديد من العوامل على زمن انتقال الاستجابة، بما في ذلك:

  • مكدس استضافة التطبيق.
  • مكونات الأمان.
  • الموقع الجغرافي النسبي للمتصل والواجهة الخلفية.
  • البنية الأساسية للشبكة.
  • التحميل الحالي.
  • حجم حمولة الطلب.
  • معالجة طول قائمة الانتظار.
  • وقت الواجهة الخلفية لمعالجة الطلب.

يمكن لأي من هذه العوامل إضافة زمن انتقال إلى الاستجابة. يمكن تخفيف بعضها عن طريق توسيع الواجهة الخلفية. البعض الآخر، مثل البنية الأساسية للشبكة، خارج سيطرة مطور التطبيق إلى حد كبير. يمكن لمعظم واجهات برمجة التطبيقات الاستجابة بسرعة كافية حتى تصل الاستجابات مرة أخرى عبر نفس الاتصال. يمكن للتعليمات البرمجية للتطبيق إجراء استدعاء واجهة برمجة تطبيقات متزامنة بطريقة غير محظورة، ما يعطي مظهر معالجة غير متزامنة، وهو ما يوصى به للعمليات المرتبطة بـ I/O.

ومع ذلك، في بعض السيناريوهات، قد يكون العمل المنجز بواسطة الخلفية طويل الأمد، أو بترتيب الثواني، أو قد يكون عملية خلفية يتم تنفيذها في دقائق أو حتى ساعات. في هذه الحالة، ليس من الممكن انتظار اكتمال العمل قبل الاستجابة للطلب. هذا الموقف هو مشكلة محتملة لأي نمط طلب رد متزامن.

تحل بعض البنيات هذه المشكلة باستخدام وسيط رسالة لفصل مراحل الطلب والاستجابة. غالبًا ما يتم تحقيق هذا الفصل باستخدام نمط تسوية التحميل المستند إلى قائمة الانتظار. يمكن أن يسمح هذا الفصل لعملية العميل وواجهة برمجة التطبيقات الخلفية بالتحجيم بشكل مستقل. ولكن هذا الفصل يجلب أيضًا تعقيدًا إضافيًا عندما يتطلب العميل إعلاما بالنجاح، حيث يجب أن تصبح هذه الخطوة غير متزامنة.

تنطبق العديد من الاعتبارات نفسها التي تمت مناقشتها لتطبيقات العميل أيضًا على استدعاءات REST API من خادم إلى خادم في الأنظمة الموزعة - على سبيل المثال، في بنية الخدمات المصغرة.

حل

أحد حلول هذه المشكلة هو استخدام استقصاء HTTP. يعد الاستقصاء مفيدًا للتعليمات البرمجية من جانب العميل، حيث قد يكون من الصعب توفير نقاط نهاية رد الاتصال أو استخدام اتصالات طويلة الأمد. حتى عندما تكون عمليات رد الاتصال ممكنة، يمكن أن تضيف المكتبات والخدمات الإضافية المطلوبة أحيانًا الكثير من التعقيد الإضافي.

  • يقوم تطبيق العميل بإجراء استدعاء متزامن لواجهة برمجة التطبيقات، ما يؤدي إلى تشغيل عملية طويلة الأمد على الخلفية.

  • تستجيب واجهة برمجة التطبيقات بشكل متزامن في أسرع وقت ممكن. يقوم بإرجاع رمز حالة HTTP 202 (مقبول)، مع الإقرار بأنه تم تلقي الطلب للمعالجة.

    إشعار

    يجب أن تتحقق واجهة برمجة التطبيقات من صحة كل من الطلب والإجراء الذي سيتم تنفيذه قبل بدء عملية التشغيل الطويل. إذا كان الطلب غير صالح، قم بالرد على الفور برمز خطأ مثل HTTP 400 (طلب غير صالح).

  • تحتوي الاستجابة على مرجع موقع يشير إلى نقطة نهاية يمكن للعميل الاستقصاء عنها للتحقق من نتيجة العملية طويلة الأمد.

  • تقوم واجهة برمجة التطبيقات بإلغاء تحميل المعالجة إلى مكون آخر، مثل قائمة انتظار الرسائل.

  • لكل استدعاء ناجح لنقطة نهاية الحالة، فإنه يرجع HTTP 200. بينما لا يزال العمل معلقًا، تقوم نقطة نهاية الحالة بإرجاع مورد يشير إلى أن العمل لا يزال قيد التقدم. بمجرد اكتمال العمل، يمكن لنقطة نهاية الحالة إما إرجاع مورد يشير إلى الإكمال، أو إعادة التوجيه إلى عنوان URL آخر لمورد. على سبيل المثال، إذا كانت العملية غير المتزامنة تنشئ موردًا جديدًا، فستعاد توجيه نقطة نهاية الحالة إلى عنوان URL لهذا المورد.

يوضح الرسم التخطيطي التالي تدفقًا نموذجيًا:

تدفق الطلب والاستجابة لطلبات HTTP غير المتزامنة

  1. يرسل العميل طلبا ويتلقى استجابة HTTP 202 (مقبولة).
  2. يرسل العميل طلب HTTP GET إلى نقطة نهاية الحالة. لا يزال العمل معلقًا، لذلك يرجع هذا الاستدعاء HTTP 200.
  3. في مرحلة ما، يكتمل العمل وترجع نقطة نهاية الحالة 302 (تم العثور عليه) مع إعادة التوجيه إلى المورد.
  4. يجلب العميل المورد في عنوان URL المحدد.

المسائل والاعتبارات

  • هناك عدد من الطرق الممكنة لتنفيذ هذا النمط عبر HTTP وليس جميع الخدمات المصدر لها نفس الدلالات. على سبيل المثال، لن ترجع معظم الخدمات استجابة HTTP 202 مرة أخرى من أسلوب GET عند عدم انتهاء عملية بعيدة. باتباع دلالات REST النقية، يجب أن ترجع HTTP 404 (غير موجود). هذه الاستجابة منطقية عندما تفكر في أن نتيجة المكالمة غير موجودة بعد.

  • يجب أن تشير استجابة HTTP 202 إلى الموقع والتكرار الذي يجب أن يستطلعه العميل للاستجابة. يجب أن يحتوي على العناوين الإضافية التالية:

    الرأس ‏‏الوصف الملاحظات
    الموقع عنوان URL يجب أن يستطلعه العميل للحصول على حالة استجابة. يمكن أن يكون عنوان URL هذا رمز SAS مميزًا مع كون نمط مفتاح Valet مناسبًا إذا كان هذا الموقع يحتاج إلى التحكم في الوصول. يكون نمط مفتاح الخادم صالحا أيضًا عندما يحتاج استقصاء الاستجابة إلى إلغاء التحميل إلى خلفية أخرى
    Retry-After تقدير وقت اكتمال المعالجة تم تصميم هذا العنوان لمنع عملاء الاستقصاء من التأثير على الواجهة الخلفية مع إعادة المحاولة.
  • قد تحتاج إلى استخدام وكيل معالجة أو واجهة لمعالجة رؤوس الاستجابة أو الحمولة اعتمادًا على الخدمات الأساسية المستخدمة.

  • إذا تم إعادة توجيه نقطة نهاية الحالة عند الاكتمال، فإن HTTP 302 أو HTTP 303 هي رموز إرجاع مناسبة، اعتمادًا على الدلالات الدقيقة التي تدعمها.

  • عند المعالجة الناجحة، يجب أن يقوم المورد المحدد بواسطة عنوان الموقع بإرجاع رمز استجابة HTTP مناسب مثل 200 (OK) أو 201 (تم الإنشاء) أو 204 (بلا محتوى).

  • إذا حدث خطأ في أثناء المعالجة، فاستمر في ظهور الخطأ في عنوان URL للمورد الموضح في عنوان الموقع وقم بشكل مثالي بإعادة رمز استجابة مناسب إلى العميل من هذا المورد (رمز 4xx).

  • لن تنفذ جميع الحلول هذا النمط بنفس الطريقة وستتضمن بعض الخدمات عناوين إضافية أو بديلة. على سبيل المثال، يستخدم Azure Resource Manager متغيرًا معدلًا من هذا النمط. لمزيد من المعلومات، راجع Azure Resource Manager Async Operations.

  • قد لا يدعم العملاء القديمون هذا النمط. في هذه الحالة، قد تحتاج إلى وضع واجهة على واجهة برمجة التطبيقات غير المتزامنة لإخفاء المعالجة غير المتزامنة من العميل الأصلي. على سبيل المثال، تدعم Azure Logic Apps هذا النمط في الأصل يمكن استخدامه كطبقة تكامل بين واجهة برمجة تطبيقات غير متزامنة وعميل يقوم بإجراء مكالمات متزامنة. راجع تنفيذ المهام طويلة الأمد باستخدام نمط إجراء خطاف الويب.

  • في بعض السيناريوهات، قد تحتاج إلى توفير طريقة للعملاء لإلغاء طلب طويل الأمد. في هذه الحالة، يجب أن تدعم خدمة الواجهة الخلفية شكلًا من أشكال تعليمات الإلغاء.

موعد استخدام النمط

استخدم هذا النمط من أجل:

  • التعليمات البرمجية من جانب العميل، مثل تطبيقات المتصفح، حيث يصعب توفير نقاط نهاية رد الاتصال، أو استخدام الاتصالات طويلة الأمد يضيف الكثير من التعقيد الإضافي.

  • استدعاءات الخدمة حيث يتوفر بروتوكول HTTP فقط ولا يمكن لخدمة الإرجاع إطلاق عمليات رد الاتصال بسبب قيود جدار الحماية على جانب العميل.

  • استدعاءات الخدمة التي تحتاج إلى التكامل مع البنيات القديمة التي لا تدعم تقنيات رد الاتصال الحديثة مثل WebSockets أو webhooks.

قد لا يكون هذا النمط مناسبا عندما:

  • يمكنك استخدام خدمة تم إنشاؤها للإعلامات غير المتزامنة بدلًا من ذلك، مثل Azure Event Grid.
  • يجب أن تتدفق الاستجابات في الوقت الحقيقي إلى العميل.
  • يحتاج العميل إلى جمع العديد من النتائج، وزمن انتقال تلك النتائج مهم. ضع في اعتبارك نمط ناقل خدمة بدلًا من ذلك.
  • يمكنك استخدام اتصالات الشبكة الثابتة من جانب الخادم مثل WebSockets أو SignalR. يمكن استخدام هذه الخدمات لإعلام المتصل بالنتيجة.
  • يسمح لك تصميم الشبكة بفتح المنافذ لتلقي عمليات رد الاتصال غير المتزامنة أو خطافات الويب.

تصميم حمل العمل

يجب على المهندس المعماري تقييم كيفية استخدام نمط الطلب والرد غير المتزامن في تصميم حمل العمل الخاص بهم لمعالجة الأهداف والمبادئ التي تغطيها ركائز Azure Well-Architected Framework. على سبيل المثال:

الركيزة كيف يدعم هذا النمط أهداف الركيزة
تساعد كفاءة الأداء حمل العمل الخاص بك على تلبية الطلبات بكفاءة من خلال التحسينات في التحجيم والبيانات والرمز. يؤدي فصل مراحل الطلب والرد على التفاعلات للعمليات التي لا تحتاج إلى إجابات فورية إلى تحسين استجابة الأنظمة وقابليتها للتوسع. ك appproach غير متزامن، يمكنك زيادة التزامن على جانب الخادم وجدولة العمل لإكماله كما تسمح السعة.

- PE:05 التحجيم والتقسيم
- PE:07 Code والبنية الأساسية

كما هو الحال مع أي قرار تصميم، ضع في اعتبارك أي مفاضلات ضد أهداف الركائز الأخرى التي يمكن إدخالها مع هذا النمط.

مثال

تعرض التعليمات البرمجية التالية مقتطفات من تطبيق يستخدم Azure Functions لتنفيذ هذا النمط. هناك ثلاث وظائف في الحل:

  • نقطة نهاية API غير المتزامنة.
  • نقطة نهاية الحالة.
  • دالة خلفية تأخذ عناصر العمل في قائمة الانتظار وتنفذها.

صورة لبنية نمط الرد على الطلب غير المتزامن في الدالات

شعار GitHub تتوفر هذه العينة على GitHub.

دالة AsyncProcessingWorkAcceptor

تنفذ الدالة AsyncProcessingWorkAcceptor نقطة نهاية تقبل العمل من تطبيق عميل وتضعه في قائمة انتظار للمعالجة.

  • تنشئ الدالة معرف طلب وتضيفه كبيانات تعريف إلى رسالة قائمة الانتظار.
  • تتضمن استجابة HTTP عنوان موقع يشير إلى نقطة نهاية الحالة. معرف الطلب هو جزء من مسار 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}");
    }
}

دالة AsyncProcessingBackgroundWorker

تلتقط AsyncProcessingBackgroundWorker الدالة العملية من قائمة الانتظار، وتقوم ببعض الأعمال استنادا إلى حمولة الرسالة، وتكتب النتيجة إلى حساب تخزين.

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

دالة AsyncOperationStatusChecker

تنفذ الدالة AsyncOperationStatusChecker نقطة نهاية الحالة. تتحقق هذه الدالة أولًا مما إذا كان الطلب قد اكتمل أم لا

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
}

الخطوات التالية

قد تكون المعلومات التالية ذات صلة عند تنفيذ هذا النمط: