Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Отделяйте серверную обработку от внешнего узла, когда серверная обработка должна выполняться асинхронно, но интерфейс должен иметь четкий ответ.
Контекст и проблема
В современной разработке приложений клиентские приложения часто зависят от удаленных API для обеспечения бизнес-логики и создания функций. Многие приложения выполняют код в веб-браузере, а другие среды также размещают клиентский код. API могут быть связаны непосредственно с приложением или работать как общие службы из внешней службы. Большинство вызовов API используют ПРОТОКОЛ HTTP или HTTPS и следуют семантике REST.
В большинстве случаев API для клиентского приложения отвечает примерно за 100 миллисекунд (мс) или меньше. Многие факторы могут повлиять на задержку ответа:
- Стек размещения приложения
- Компоненты безопасности
- Относительное географическое расположение звонящего и сервера
- Сетевая инфраструктура
- Текущая загрузка
- Размер полезных данных запроса
- Длина очереди обработки
- Время обработки запроса серверной частью системы
Эти факторы могут добавить задержку в ответ. Вы можете смягчить некоторые факторы, масштабируя серверную часть. Другие факторы, такие как сетевая инфраструктура, находятся вне контроля разработчика приложений. Большинство API-интерфейсов реагируют достаточно быстро, чтобы ответ возвращался через то же подключение. Код приложения может выполнять синхронный вызов API в неблокирующем режиме, чтобы создать видимость асинхронной обработки. Мы рекомендуем этот подход для операций ввода-вывода.
В некоторых сценариях бэкэнд выполняет долгосрочные задачи, которые занимают несколько секунд. В других сценариях серверная часть выполняет длительные фоновые работы в течение нескольких минут или в течение длительных периодов. В таких случаях вы не можете ждать завершения работы перед отправкой ответа. Эта ситуация может создать проблему для синхронных шаблонов ответа на запросы. Рекомендации по проектированию серверной обработки см. в разделе "Фоновые задания".
Некоторые архитектуры решают эту проблему с помощью брокера сообщений для разделения этапов запроса и ответа. Многие системы обеспечивают это разделение с помощью паттерна Queue-Based Load Leveling. Это разделение позволяет клиентскому процессу и серверному API масштабироваться независимо. Он также представляет дополнительную сложность, если клиенту требуется уведомление об успешном выполнении, так как этот шаг также должен стать асинхронным.
Многие из таких же соображений, которые применяются к клиентским приложениям, также применяются к вызовам REST API сервера на сервере в распределенных системах, например в архитектуре микрослужб.
Решение
Одним из решений этой проблемы является использование опроса HTTP. Опрос хорошо подходит для клиентского кода, если конечные точки обратного вызова недоступны или когда длительные соединения становятся слишком сложными. Даже если обратные вызовы возможны, дополнительные библиотеки и службы, необходимые для них, могут увеличить сложность.
Решение описывается следующими шагами.
Клиентское приложение выполняет синхронный вызов к API для активации длительной операции на серверной стороне.
API реагирует синхронно как можно быстрее. Он возвращает код состояния HTTP 202 (Принято), чтобы подтвердить, что он получил запрос на обработку.
Замечание
Интерфейс прикладного программирования должен проверить запрос и выполняемое действие перед запуском длительного процесса. Если запрос недействителен, ответите немедленно с кодом ошибки, например HTTP 400 (недопустимый запрос).
Ответ содержит ссылку на расположение, указывающую на endpoint, который клиент может опросить, чтобы проверить результат длительной операции.
API выгружает обработку в другой компонент, например очередь сообщений.
Для каждого успешного вызова конечной точки состояния конечная точка возвращает HTTP 200 (ОК). Пока работа выполняется, конечная точка статуса возвращает ресурс, который указывает на текущее состояние. Текст ответа состояния должен содержать достаточно информации, чтобы клиент понимал текущее состояние операции.
По завершении работы конечная точка состояния возвращает ресурс, указывающий на завершение или перенаправление на другой URL-адрес ресурса. Например, если асинхронная операция создает новый ресурс, конечная точка состояния перенаправляется по URL-адресу для этого ресурса.
На следующей схеме показан типичный поток.
Клиент отправляет запрос и получает ответ HTTP 202 (принято).
Клиент отправляет HTTP-запрос GET в конечную точку состояния. Этот вызов возвращает HTTP 200, так как работа ожидается.
В какой-то момент работа завершается, и конечная точка статуса возвращает HTTP 303 (См. другое) для перенаправления к ресурсу.
Клиент получает ресурс по указанному URL-адресу.
Проблемы и рекомендации
Учитывайте следующие моменты при принятии решения о том, как реализовать этот шаблон.
Существует несколько способов реализации этого шаблона по протоколу HTTP и вышестоящей службы не всегда используют одну и ту же семантику. Например, некоторые реализации не используют отдельную конечную точку состояния. Вместо этого клиент опрашивает URL-адрес целевого ресурса напрямую и получает HTTP 404 (не найден) до создания ресурса. Этот ответ создается, так как ресурс еще не существует. Однако этот подход может быть неясным, так как идентификаторы недопустимых запросов также возвращают HTTP 404. Выделенная конечная точка состояния, которая возвращает HTTP 200 с текстом состояния, как описано в этом шаблоне, избегает этой путаницы.
Ответ HTTP 202 указывает место, где клиент опрашивает, и частоту. Он должен содержать следующие заголовки.
Header Описание Примечания LocationURL, который клиент опрашивает для получения статуса ответа Этот URL-адрес может быть маркером общей подписи доступа (SAS). Шаблон Valet Key хорошо работает, когда в данном месте требуется управление доступом. Шаблон также применяется, когда опрос отклика должен переключиться на другой бэкенд. Retry-AfterПредполагаемое время завершения обработки Этот заголовок помогает клиентам избежать отправки слишком большого количества запросов на сервер. При разработке этого ответа следует учитывать ожидаемое поведение клиента. Клиент, который вы управляете, может точно следовать этим значениям ответа. Клиенты, созданные другими пользователями, включая клиенты, созданные с помощью средств без кода или с низким уровнем кода, таких как Azure Logic Apps, могут применять собственную обработку для HTTP 202.
Рассмотрите возможность включения следующих полей в ответ конечной точки состояния.
Поле Описание Примечания statusТекущее состояние операции, например ожидание, выполнение, успешное выполнение, сбой или отмена Использует согласованный, документированные наборы значений терминала и нетерминальных значений createdAtВремя принятия операции Помогает клиентам обнаруживать устаревшие или заброшенные операции lastUpdatedAtВремя последнего обновления состояния Помогает клиентам различать застопоренные и выполняющиеся операции. percentCompleteНеобязательный индикатор хода выполнения Полезно, когда серверная часть может оценить прогресс errorСтруктурированный объект ошибки, когда статус сбой Для обеспечения согласованности рекомендуется использовать формат RFC 9457 . Возможно, потребуется использовать обрабатывающий прокси для настройки заголовков ответов или полезных данных в зависимости от используемых базовых служб.
Если конечная точка состояния перенаправляется после завершения, используйте HTTP 303 (см. другие). 303 указывает клиенту выдавать запрос GET на URL-адрес перенаправления независимо от исходного метода запроса. Это правильная семантика для этого шаблона, так как клиент получает отдельный результирующий ресурс, а не повторно отправляет исходную операцию. HTTP 302 (Found) не гарантирует изменение метода. Некоторые клиенты воспроизводят исходный метод в случае перенаправления. Это поведение может вызвать непреднамеренные побочные эффекты, такие как повторяющиеся запросы POST.
После успешного обработки запроса сервером ресурс
Location, указывающий заголовок, возвращает код состояния HTTP, например 200, 201 (создано) или 204 (нет содержимого).Если во время обработки возникает ошибка, сохраните ошибку по URL-адресу ресурса, указанному в заголовке
Location, и верните код состояния 4xx из ресурса, соответствующего сбою. Используйте структурированный формат ошибок, например RFC 9457 (сведения о проблеме для API HTTP), чтобы клиенты могли программно анализировать и обрабатывать сбои.Ресурс состояния и все сохраненные результаты используют хранилище и вычисления. Определите политику хранения данных, чтобы удалить данные после разумного срока. Чтобы сообщить клиентам об окне хранения, можно добавить
Expiresзаголовок в ответ состояния.Решения не все реализуют этот шаблон так же, а некоторые службы включают дополнительные или альтернативные заголовки. Например, Azure Resource Manager использует измененный вариант этого шаблона. Дополнительные сведения см. в разделе асинхронные операции Диспетчера ресурсов.
Устаревшие клиенты могут не поддерживать этот шаблон. В этом случае может потребоваться разместить фасад над асинхронным API, чтобы скрыть асинхронную обработку от исходного клиента. Например, Logic Apps поддерживает этот шаблон в собственном коде, и его можно использовать в качестве уровня интеграции между асинхронным API и клиентом, выполняющим синхронные вызовы. Дополнительные сведения см. в статье "Асинхронное поведение ответа на запросы" в Logic Apps.
Чтобы предоставить клиентам возможность отменить длительный запрос, предоставьте операцию DELETE в ресурсе конечной точки состояния. Этот запрос должен перенаправить инструкцию отмены в серверный компонент обработки. После обработки отмены серверная часть должна обновить ресурс статуса, чтобы отразить состояние отмены. Этот процесс помогает предотвратить потребление ресурсов незавершенной работой на неопределенный срок. Определите, поддерживает ли операция частичный откат или требуется компенсированная транзакция.
Вы можете требовать от клиентов предоставления ключа идемпотентности, например в заголовке
Idempotency-Keyзапроса при отправке первоначального запроса. Если серверная часть получает повторяющийся ключ, она должна вернуть существующий ресурс состояния, а не помещать в очередь второй рабочий элемент. Этот подход защищает от сбоев сети, которые вызывают у клиента повторную попытку POST, которую сервер уже принял. Особенно важно в этом шаблоне, так как клиент не может различать потерянный ответ и запрос, который никогда не был получен.
Замечание
Этот шаблон описывает опрос HTTP, в котором клиент периодически выдает новые запросы для проверки состояния. В длительном опросе клиент отправляет запрос, и сервер держит подключение открытым до тех пор, пока новые данные не будут доступны или истекло время ожидания. Длинный опрос уменьшает задержку ответа по сравнению с периодическим опросом, но это приводит к сложности управления подключениями и времени ожидания.
Когда следует использовать этот шаблон
Используйте этот шаблон, когда:
Вы работаете с клиентским кодом, например приложениями браузера, и эти ограничения затрудняют предоставление конечных точек обратного вызова или длительные подключения добавляют слишком много сложности.
Вы вызываете службу, которая использует только протокол HTTP, и возвращаемая служба не может отправлять обратные вызовы из-за ограничений брандмауэра на стороне клиента.
Вы интегрируете с рабочими нагрузками, которые не поддерживают современные механизмы обратного вызова, такие как WebSockets или вебхуки.
Этот шаблон может быть не подходит, если:
Вместо этого можно использовать службу, созданную для асинхронных уведомлений, например Azure Event Grid.
Ответы должны передаваться в режиме реального времени клиенту. Рассмотрите возможность использования событий, отправляемых сервером (SSEs), которые предоставляют легковесный, собственный HTTP-канал для отправки с сервера на клиент без необходимости опроса клиента.
Клиент должен собирать много результатов, и задержка этих результатов важна. Вместо этого рассмотрите возможность использования брокера сообщений.
Доступны постоянные сетевые подключения на стороне сервера, такие как WebSockets или SignalR. Эти подключения можно использовать для уведомления вызывающего абонента о результате.
Схема сети поддерживает открытые порты для получения асинхронных обратных вызовов или веб-перехватчиков.
Проектирование рабочей нагрузки
Архитектор должен оценить, как можно использовать шаблон асинхронного запроса-ответа в проектировании нагрузки, чтобы решить цели и принципы, рассмотренные в положениях Azure Well-Architected Framework.
| Столп | Как этот шаблон поддерживает цели основных компонентов |
|---|---|
| Эффективность производительности помогает рабочей нагрузке эффективно соответствовать требованиям путем оптимизации масштабирования, данных и кода. | Вы повышаете скорость реагирования и масштабируемость путем отмены этапов запроса и ответа для процессов, которые не требуют немедленного ответа. Асинхронный подход увеличивает параллелизм и позволяет планировать работу сервера по мере доступности емкости. - Pe:05 Масштабирование и секционирование - PE:07 Код и инфраструктура |
Как и в случае с любым решением по проектированию, рассмотрите компромиссы по отношению к целям других столпов, которые этот шаблон может представить.
Пример
В следующем коде показаны фрагменты из приложения, использующего Azure Functions для реализации этого шаблона. Это решение имеет три функции:
- Конечная точка асинхронного API
- Конечная точка состояния
- Серверная функция, которая обрабатывает задания в очереди и выполняет их.
Этот пример доступен в GitHub.
Реализация использует управляемое удостоверение для проверки подлинности с помощью Azure Service Bus и Azure Blob Storage, что позволяет избежать хранения строк подключения или ключей учетной записи. Зависимости регистрируются в Program.cs с использованием DefaultAzureCredential и внедряются через первичные конструкторы.
Функция AsyncProcessingWorkAcceptor
Функция AsyncProcessingWorkAcceptor реализует конечную точку, которая принимает задачи от клиентского приложения и помещает их в очередь для обработки.
Функция создает идентификатор запроса и добавляет его в виде метаданных в сообщение очереди.
Ответ HTTP включает заголовок, указывающий
Locationна конечную точку состояния иRetry-Afterзаголовок, который предлагает интервал опроса. Идентификатор запроса отображается в пути URL-адреса.
public class AsyncProcessingWorkAcceptor(ServiceBusClient _serviceBusClient)
{
[Function("AsyncProcessingWorkAcceptor")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
[FromBody] CustomerPOCO customer)
{
if (string.IsNullOrEmpty(customer.id) || string.IsNullOrEmpty(customer.customername))
{
return new BadRequestResult();
}
string requestId = Guid.NewGuid().ToString();
string statusUrl = $"https://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{requestId}";
var messagePayload = JsonConvert.SerializeObject(customer);
var message = new ServiceBusMessage(messagePayload);
message.ApplicationProperties.Add("RequestGUID", requestId);
message.ApplicationProperties.Add("RequestSubmittedAt", DateTime.UtcNow);
message.ApplicationProperties.Add("RequestStatusURL", statusUrl);
var sender = _serviceBusClient.CreateSender("outqueue");
await sender.SendMessageAsync(message);
req.HttpContext.Response.Headers["Retry-After"] = "5";
return new AcceptedResult(statusUrl, null);
}
}
Функция AsyncProcessingBackgroundWorker
Функция AsyncProcessingBackgroundWorker считывает операцию из очереди, обрабатывает ее на основе полезных данных сообщения и записывает результат в учетную запись хранения.
public class AsyncProcessingBackgroundWorker(BlobContainerClient _blobContainerClient)
{
[Function("AsyncProcessingBackgroundWorker")]
public async Task Run(
[ServiceBusTrigger("outqueue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
{
// Perform an action against the blob data source for the async readers to check against.
// This is where your service worker processing will be performed.
var requestGuid = message.ApplicationProperties["RequestGUID"].ToString();
string blobName = $"{requestGuid}.blobdata";
var blobClient = _blobContainerClient.GetBlobClient(blobName);
using (MemoryStream memoryStream = new MemoryStream())
using (StreamWriter writer = new StreamWriter(memoryStream))
{
writer.Write(message.Body.ToString());
writer.Flush();
memoryStream.Position = 0;
await blobClient.UploadAsync(memoryStream, overwrite: true);
}
}
}
Функция AsyncOperationStatusChecker
Функция AsyncOperationStatusChecker реализует конечную точку состояния. Эта функция проверяет состояние запроса:
Если запрос завершится, функция возвращает HTTP 303 (See Other), перенаправляя клиента на URL-адрес ключа valet для получения результата.
Если запрос ожидается, функция возвращает код HTTP 200, включающий текущее состояние.
public class AsyncOperationStatusChecker(ILogger<AsyncOperationStatusChecker> _logger)
{
[Function("AsyncOperationStatusChecker")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "RequestStatus/{requestId}")] HttpRequest req,
[BlobInput("data/{requestId}.blobdata", Connection = "DataStorage")] BlockBlobClient inputBlob, string requestId)
{
OnCompleteEnum OnComplete = Enum.Parse<OnCompleteEnum>(req.Query["OnComplete"].FirstOrDefault() ?? "Redirect");
OnPendingEnum OnPending = Enum.Parse<OnPendingEnum>(req.Query["OnPending"].FirstOrDefault() ?? "OK");
_logger.LogInformation("Received status request for {RequestId} - OnComplete {OnComplete} - OnPending {OnPending}",
requestId, OnComplete, OnPending);
// Check whether the blob exists.
if (await inputBlob.ExistsAsync())
{
// If the blob exists, the function uses the OnComplete parameter to determine the next action.
return await OnCompleted(OnComplete, inputBlob, requestId, req);
}
else
{
// If the blob doesn't exist, the function uses the OnPending parameter to determine the next action.
switch (OnPending)
{
case OnPendingEnum.OK:
{
// Return an HTTP 200 status code.
return new OkObjectResult(new { status = "In progress", Location = rqs });
}
case OnPendingEnum.Synchronous:
{
// Long polling example: hold the connection open and check for completion
// using exponential backoff. Time out after approximately one minute.
int backoff = 250;
while (!await inputBlob.ExistsAsync() && backoff < 64000)
{
_logger.LogInformation("Synchronous mode {RequestId} - retrying in {Backoff} ms", requestId, backoff);
backoff = backoff * 2;
await Task.Delay(backoff);
}
if (await inputBlob.ExistsAsync())
{
_logger.LogInformation("Synchronous mode {RequestId} - completed after {Backoff} ms", requestId, backoff);
return await OnCompleted(OnComplete, inputBlob, requestId, req);
}
else
{
_logger.LogInformation("Synchronous mode {RequestId} - NOT FOUND after timeout {Backoff} ms", requestId, backoff);
return new NotFoundResult();
}
}
default:
{
throw new InvalidOperationException($"Unexpected value: {OnPending}");
}
}
}
}
private async Task<IActionResult> OnCompleted(OnCompleteEnum OnComplete, BlockBlobClient inputBlob, string requestId, HttpRequest req)
{
switch (OnComplete)
{
case OnCompleteEnum.Redirect:
{
// Generate a user delegation SAS URI by using managed identity credentials.
BlobServiceClient blobServiceClient = inputBlob.GetParentBlobContainerClient().GetParentBlobServiceClient();
var userDelegationKey = await blobServiceClient.GetUserDelegationKeyAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(7));
// Return 303 (See Other) to redirect the client to the result resource.
// GenerateUserDelegationSasUri is a custom helper. See the full implementation on GitHub.
req.HttpContext.Response.Headers.Location = GenerateUserDelegationSasUri(inputBlob, userDelegationKey);
return new StatusCodeResult(StatusCodes.Status303SeeOther);
}
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
}