Шаблон асинхронного запроса-ответа

Azure
Azure Logic Apps

Выполняйте серверную обработку не на внешнем узле, где серверная обработка должна быть асинхронной и когда интерфейс требует четкого ответа.

Контекст и проблема

В современной разработке приложений это нормально для клиентских приложений — часто код, выполняемый в веб-клиенте (браузере), чтобы зависеть от удаленных API для предоставления бизнес-логики и создания функций. Эти API могут быть напрямую связаны с приложением или могут быть общими службами, предоставляемыми сторонним поставщиком. Обычно эти вызовы API происходят по протоколу HTTP(S) и следуют семантике REST.

В большинстве случаев API для клиентского приложения предназначены для быстрого реагирования в порядке 100 мс или меньше. Многие факторы могут повлиять на задержку ответа, в том числе:

  • Стек размещения приложения.
  • Компоненты безопасности.
  • Относительное географическое расположение вызывающего объекта и серверной части.
  • Сетевая инфраструктура.
  • Текущая загрузка.
  • Размер полезных данных запроса.
  • Длина очереди обработки.
  • Время обработки запроса серверной частью.

Любой из этих факторов может добавить задержку в ответ. Некоторые могут быть устранены путем масштабирования серверной части. Другие, такие как сетевая инфраструктура, в значительной степени не контролируются разработчиком приложений. Большинство API-интерфейсов могут реагировать достаточно быстро, чтобы ответы возвращались через то же подключение. Код приложения может сделать синхронный вызов API неблокирующим способом, предоставляя внешний вид асинхронной обработки, которая рекомендуется для операций ввода-вывода.

Однако в некоторых сценариях работа, выполняемая серверной частью, может быть длительной, в порядке секунд или может быть фоновым процессом, выполняемым в минутах или даже часах. В этом случае невозможно ждать завершения работы перед ответом на запрос. Эта ситуация является потенциальной проблемой для любого синхронного шаблона ответа на запрос.

Некоторые архитектуры решают эту проблему с помощью брокера сообщений для разделения этапов запроса и ответа. Это разделение часто достигается с помощью шаблона выравнивания нагрузки на основе очередей. Это разделение позволяет клиентскому процессу и внутреннему API масштабироваться независимо. Но это разделение также приводит к дополнительной сложности, когда клиенту требуется уведомление об успешном выполнении, так как этот шаг должен стать асинхронным.

Многие из этих же соображений, рассмотренных для клиентских приложений, также применяются к вызовам REST API сервера на сервере в распределенных системах, например в архитектуре микрослужб.

Решение

Одним из решений этой проблемы является использование опроса HTTP. Опрос полезен для клиентского кода, так как это может быть трудно предоставить конечные точки обратного вызова или использовать длительные подключения. Даже если обратные вызовы возможны, дополнительные библиотеки и службы, которые требуются, иногда могут добавить слишком много дополнительной сложности.

  • Клиентское приложение выполняет синхронный вызов API, активируя длительную операцию на серверной части.

  • API реагирует синхронно как можно быстрее. Он возвращает код состояния HTTP 202 (принято), признавая, что запрос получен для обработки.

    Примечание.

    API должен проверить как запрос, так и действие, выполняемого перед запуском длительного процесса. Если запрос недопустим, немедленно ответите с кодом ошибки, например HTTP 400 (недопустимый запрос).

  • Ответ содержит ссылку на расположение, указывающую на конечную точку, которую клиент может опрашивать, чтобы проверка в результате длительной операции.

  • API выгружает обработку в другой компонент, например очередь сообщений.

  • Для каждого успешного вызова конечной точки состояния возвращается 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, если для этого расположения требуется контроль доступа. Шаблон ключа valet также действителен, если опрос ответа нуждается в разгрузке в другую серверную часть.
    Retry-After Оценка завершения обработки Этот заголовок предназначен для предотвращения того, чтобы клиенты опросов подавляли внутренний колонтитул с повторными попытками.
  • Возможно, вам потребуется использовать прокси-сервер обработки или фасад для управления заголовками ответов или полезными данными в зависимости от используемых базовых служб.

  • Если конечная точка состояния перенаправляется при завершении, коды возврата http 302 или HTTP 303 соответствуют точной семантике, поддерживаемой вами.

  • При успешной обработке ресурс, указанный заголовком Location, должен возвращать соответствующий код ответа HTTP, например 200 (ОК), 201 (создано) или 204 (нет содержимого).

  • Если во время обработки возникает ошибка, сохраните ошибку по URL-адресу ресурса, описанному в заголовке location, и в идеале возвращает соответствующий код ответа клиенту из этого ресурса (код 4xx).

  • Не все решения реализуют этот шаблон таким же образом, и некоторые службы будут включать дополнительные или альтернативные заголовки. Например, Azure Resource Manager использует измененный вариант этого шаблона. Дополнительные сведения см. в статье "Асинхронные операции Azure Resource Manager".

  • Устаревшие клиенты могут не поддерживать этот шаблон. В этом случае может потребоваться разместить фасад над асинхронным API, чтобы скрыть асинхронную обработку от исходного клиента. Например, Azure Logic Apps поддерживает этот шаблон изначально, можно использовать в качестве уровня интеграции между асинхронным API и клиентом, который выполняет синхронные вызовы. См. статью "Выполнение длительных задач" с помощью шаблона действия веб-перехватчика.

  • В некоторых сценариях может потребоваться предоставить клиентам способ отмены длительного запроса. В этом случае серверная служба должна поддерживать определенную форму инструкции отмены.

Когда следует использовать этот шаблон

Используйте этот шаблон для:

  • Клиентский код, например приложения браузера, где трудно предоставить конечные точки обратного вызова, или использование длительных подключений добавляет слишком много дополнительной сложности.

  • Вызовы служб, в которых доступен только протокол HTTP, и возвращаемая служба не может вызывать обратные вызовы из-за ограничений брандмауэра на стороне клиента.

  • Вызовы служб, которые необходимо интегрировать с устаревшими архитектурами, которые не поддерживают современные технологии обратного вызова, такие как WebSockets или веб-перехватчики.

Этот шаблон может быть не подходит, если:

  • Вместо этого можно использовать службу, созданную для асинхронных уведомлений, например Сетка событий Azure.
  • Ответы должны передаваться в режиме реального времени клиенту.
  • Клиент должен собирать много результатов и получать задержку этих результатов важно. Вместо этого рассмотрим шаблон служебной шины.
  • Вы можете использовать постоянные сетевые подключения на стороне сервера, такие как WebSockets или SignalR. Эти службы можно использовать для уведомления вызывающего объекта результата.
  • Схема сети позволяет открывать порты для получения асинхронных обратных вызовов или веб-перехватчиков.

Проектирование рабочей нагрузки

Архитектор должен оценить, как шаблон асинхронного ответа на запросы может использоваться в проектировании рабочей нагрузки для решения целей и принципов, описанных в основных принципах Azure Well-Architected Framework. Например:

Принцип Как этот шаблон поддерживает цели основных компонентов
Эффективность производительности помогает рабочей нагрузке эффективно соответствовать требованиям путем оптимизации масштабирования, данных, кода. Разделение этапов взаимодействия запросов и ответов для процессов, которые не нуждаются в немедленных ответах, повышает скорость реагирования и масштабируемость систем. В качестве асинхронного приложения можно максимально увеличить параллелизм на стороне сервера и запланировать выполнение работы в качестве емкости.

- Pe:05 Масштабирование и секционирование
- Pe:07 Code и инфраструктура

Как и любое решение по проектированию, рассмотрите любые компромиссы по целям других столпов, которые могут быть представлены с этим шаблоном.

Пример

В следующем коде показаны фрагменты из приложения, использующего Функции Azure для реализации этого шаблона. В решении есть три функции:

  • Конечная точка асинхронного 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 реализует конечную точку состояния. Эта функция сначала проверка указывает, был ли выполнен запрос.

  • Если запрос был завершен, функция возвращает ключ valet-key в ответ или перенаправляет вызов немедленно на URL-адрес valet-key.
  • Если запрос по-прежнему ожидается, мы должны вернуть код 200, включая текущее состояние.
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
}

Следующие шаги

К реализации этого шаблона могут относиться следующие сведения: