Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Отделяйте серверную обработку от внешнего узла, когда серверная обработка должна выполняться асинхронно, но интерфейс должен иметь четкий ответ.
Контекст и проблема
В современной разработке приложений клиентские приложения часто используют удаленные API для обеспечения бизнес-логики и создания функций. Многие приложения выполняют код в веб-браузере, а другие среды также размещают клиентский код. API могут быть связаны непосредственно с приложением или работать как общие службы из внешней службы. Большинство вызовов API используют ПРОТОКОЛ HTTP или HTTPS и следуют семантике REST.
В большинстве случаев API для клиентского приложения отвечает примерно за 100 миллисекунд (мс) или меньше. Многие факторы могут повлиять на задержку ответа:
- Стек размещения приложения
- Компоненты безопасности
- Относительное географическое расположение звонящего и сервера
- Сетевая инфраструктура
- Текущая загрузка
- Размер полезных данных запроса
- Длина очереди обработки
- Время обработки запроса серверной частью системы
Эти факторы могут добавить задержку в ответ. Вы можете смягчить некоторые факторы, масштабируя серверную часть. Другие факторы, такие как сетевая инфраструктура, находятся вне контроля разработчика приложений. Большинство API-интерфейсов реагируют достаточно быстро, чтобы ответ возвращался через то же подключение. Код приложения может выполнять синхронный вызов API в неблокирующем режиме, чтобы создать видимость асинхронной обработки. Мы рекомендуем этот подход для операций ввода-вывода.
В некоторых сценариях бэкэнд выполняет долгосрочные задачи, которые занимают несколько секунд. В других сценариях серверная часть выполняет длительные фоновые работы в течение нескольких минут или в течение длительных периодов. В таких случаях вы не можете ждать завершения работы перед отправкой ответа. Эта ситуация может создать проблему для синхронных шаблонов ответа на запросы.
Некоторые архитектуры решают эту проблему с помощью брокера сообщений для разделения этапов запроса и ответа. Многие системы обеспечивают это разделение с помощью паттерна Queue-Based Load Leveling. Это разделение позволяет клиентскому процессу и серверному API масштабироваться независимо. Он также представляет дополнительную сложность, если клиенту требуется уведомление об успешном выполнении, так как этот шаг также должен стать асинхронным.
Многие из таких же соображений, которые применяются к клиентским приложениям, также применяются к вызовам REST API сервера на сервере в распределенных системах, например в архитектуре микрослужб.
Решение
Одним из решений этой проблемы является использование опроса HTTP. Опрос хорошо подходит для клиентского кода, если конечные точки обратного вызова недоступны или когда длительные соединения становятся слишком сложными. Даже если обратные вызовы возможны, дополнительные библиотеки и службы, необходимые для них, могут увеличить сложность.
Решение описывается следующими шагами.
Клиентское приложение выполняет синхронный вызов к API для активации длительной операции на серверной стороне.
API реагирует синхронно как можно быстрее. Он возвращает код состояния HTTP 202 (Принято), чтобы подтвердить, что он получил запрос на обработку.
Замечание
API проверяет запрос и действие, выполняемого перед запуском длительного процесса. Если запрос недействителен, ответите немедленно с кодом ошибки, например HTTP 400 (недопустимый запрос).
Ответ содержит ссылку на расположение, указывающую на endpoint, который клиент может опросить, чтобы проверить результат длительной операции.
API выгружает обработку в другой компонент, например очередь сообщений.
Для успешного вызова конечной точки состояния конечная точка возвращает HTTP 200 (ОК). Пока работа выполняется, конечная точка возвращает ресурс, указывающий на это состояние. По завершении работы конечная точка возвращает ресурс, указывающий на завершение или перенаправление на другой URL-адрес ресурса. Например, если асинхронная операция создает новый ресурс, конечная точка состояния перенаправляется по URL-адресу для этого ресурса.
На следующей схеме показан типичный поток.
Клиент отправляет запрос и получает ответ HTTP 202.
Клиент отправляет HTTP-запрос GET в конечную точку состояния. Работа находится в ожидании, поэтому этот запрос возвращает HTTP 200.
Работа завершается, а конечная точка состояния возвращает HTTP 302 (найдено) для перенаправления к ресурсу.
Клиент получает ресурс по указанному URL-адресу.
Проблемы и рекомендации
Учитывайте следующие моменты при принятии решения о том, как реализовать этот шаблон.
Существует несколько способов реализации этого шаблона по протоколу HTTP и вышестоящей службы не всегда используют одну и ту же семантику. Например, большинство служб возвращают HTTP 404 (Не найдено) из метода GET, когда удаленный процесс не завершен, а не HTTP 202. Согласно стандартной семантике REST, HTTP 404 является правильным ответом, так как результат вызова еще не существует.
Ответ HTTP 202 указывает место, где клиент опрашивает, и частоту. Он содержит следующие заголовки.
Header Описание Примечания LocationURL, который клиент опрашивает для получения статуса ответа Этот URL-адрес может быть токеном доступа с общей подписью. Шаблон Valet Key хорошо работает, когда в данном месте требуется управление доступом. Шаблон также применяется, когда опрос отклика должен переключиться на другой бэкенд. Retry-AfterОценка завершения обработки Этот заголовок предотвращает отправку клиентами слишком большого количества запросов на сервер. При разработке этого ответа следует учитывать ожидаемое поведение клиента. Клиент, который вы управляете, может точно следовать этим значениям ответа. Клиенты, созданные другими пользователями, включая клиенты, созданные с помощью средств без кода или с низким уровнем кода, таких как Azure Logic Apps, могут применять собственную обработку для HTTP 202.
Возможно, потребуется использовать обрабатывающий прокси для настройки заголовков ответов или полезных данных в зависимости от используемых базовых служб.
Если конечная точка состояния перенаправляется после завершения, то в зависимости от поддерживаемой семантики, допустимыми кодами возврата являются HTTP 302 или HTTP 303 (See Other).
После обработки запроса ресурс, задающий
Locationзаголовок, возвращает код состояния HTTP, например 200, 201 (создано) или 204 (нет содержимого).Если во время обработки возникает ошибка, сохраните ошибку по URL-адресу ресурса, который указан в заголовке
Location, и возвратите код состояния 4xx, соответствующий сбою, из этого ресурса.Решения не все реализуют этот шаблон так же, а некоторые службы включают дополнительные или альтернативные заголовки. Например, Azure Resource Manager использует измененный вариант этого шаблона. Дополнительные сведения см. раздел Асинхронные операции диспетчера ресурсов.
Устаревшие клиенты могут не поддерживать этот шаблон. В этом случае может потребоваться поместить прокси-сервер обработки через асинхронный API, чтобы скрыть асинхронную обработку от исходного клиента. Например, Logic Apps поддерживает этот шаблон в собственном коде, и его можно использовать в качестве уровня интеграции между асинхронным API и клиентом, выполняющим синхронные вызовы. Дополнительные сведения см. в разделе "Выполнение длительных задач" с помощью шаблона действия веб-перехватчика.
В некоторых сценариях может потребоваться предоставить клиентам способ отмены длительного запроса. В этом случае служба внутренней службы должна поддерживать определенную форму инструкции отмены.
Когда следует использовать этот шаблон
Используйте этот шаблон, когда:
Вы работаете с клиентским кодом, например приложениями браузера, и эти ограничения затрудняют предоставление конечных точек обратного вызова или длительные подключения добавляют слишком много сложности.
Вы вызываете службу, которая использует только протокол HTTP, и возвращаемая служба не может отправлять обратные вызовы из-за ограничений брандмауэра на стороне клиента.
Происходит интеграция с устаревшими архитектурами, которые не поддерживают современные механизмы обратного вызова, такие как WebSockets или вебхуки.
Этот шаблон может быть не подходит, если:
Вместо этого можно использовать службу, созданную для асинхронных уведомлений, например Azure Event Grid.
Ответы должны передаваться в режиме реального времени клиенту.
Клиенту необходимо собрать много результатов, а задержка этих результатов важна. Вместо этого рассмотрим шаблон служебной шины.
Доступны постоянные сетевые подключения на стороне сервера, такие как WebSockets или SignalR. Эти подключения можно использовать для уведомления вызывающего абонента о результате.
Схема сети поддерживает открытые порты для получения асинхронных обратных вызовов или веб-перехватчиков.
Проектирование рабочей нагрузки
Архитектор должен оценить, как они могут использовать асинхронный шаблон запрос-ответ при проектировании рабочих нагрузок для решения вопросов, связанных с целью и принципами, описанными в столпах Azure Well-Architected Framework.
| Столп | Как этот шаблон поддерживает цели основных компонентов |
|---|---|
| Эффективность производительности помогает рабочей нагрузке эффективно соответствовать требованиям путем оптимизации масштабирования, данных и кода. | Вы повышаете скорость реагирования и масштабируемость путем отмены этапов запроса и ответа для процессов, которые не требуют немедленного ответа. Асинхронный подход увеличивает параллелизм и позволяет планировать работу сервера по мере доступности емкости. - Pe:05 Масштабирование и секционирование - PE:07 Код и инфраструктура |
Как и в случае с любым решением по проектированию, рассмотрите компромиссы по отношению к целям других столпов, которые этот шаблон может представить.
Пример
В следующем коде показаны фрагменты из приложения, использующего Azure Functions для реализации этого шаблона. Это решение имеет три функции:
- Конечная точка асинхронного API
- Конечная точка состояния
- Серверная функция, которая обрабатывает задания в очереди и выполняет их.
Этот пример доступен в GitHub.
Функция AsyncProcessingWorkAcceptor
Функция AsyncProcessingWorkAcceptor реализует конечную точку, которая принимает задачи от клиентского приложения и помещает их в очередь для обработки.
Функция создает идентификатор запроса и добавляет его в виде метаданных в сообщение очереди.
Ответ HTTP включает заголовок, указывающий
Locationна конечную точку состояния. Идентификатор запроса отображается в пути URL-адреса.
public class AsyncProcessingWorkAcceptor(ServiceBusClient _serviceBusClient)
{
[Function("AsyncProcessingWorkAcceptor")]
public async Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, [FromBody] CustomerPOCO customer)
{
if (string.IsNullOrEmpty(customer.id) || string.IsNullOrEmpty(customer.customername))
{
return new BadRequestResult();
}
var reqid = Guid.NewGuid().ToString();
string scheme = Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT") == "Development" ? "http" : "https";
var rqs = $"{scheme}://{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);
var sender = _serviceBusClient.CreateSender("outqueue");
await sender.SendMessageAsync(message);
return new AcceptedResult(rqs, $"Request Accepted for Processing{Environment.NewLine}ProxyStatus: {rqs}");
}
}
Функция AsyncProcessingBackgroundWorker
Функция AsyncProcessingBackgroundWorker считывает операцию из очереди, обрабатывает ее на основе полезных данных сообщения и записывает результат в учетную запись хранения.
public class AsyncProcessingBackgroundWorker(BlobContainerClient _blobContainerClient)
{
[Function(nameof(AsyncProcessingBackgroundWorker))]
public async Task Run([ServiceBusTrigger("outqueue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
{
var requestGuid = message.ApplicationProperties["RequestGUID"].ToString();
string blobName = $"{requestGuid}.blobdata";
await _blobContainerClient.CreateIfNotExistsAsync();
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 реализует конечную точку состояния. Эта функция проверяет состояние запроса:
Если запрос завершится, функция возвращает ключ valet в ответ или перенаправляет вызов немедленно на URL-адрес valet-key.
Если запрос ожидается, функция возвращает код HTTP 200, включающий текущее состояние.
public class AsyncOperationStatusChecker(ILogger<AsyncOperationStatusChecker> _logger)
{
[Function("AsyncOperationStatusChecker")]
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "RequestStatus/{thisGUID}")] HttpRequest req,
[BlobInput("data/{thisGUID}.blobdata", Connection = "DataStorage")] BlockBlobClient inputBlob, string thisGUID)
{
OnCompleteEnum OnComplete = Enum.Parse<OnCompleteEnum>(req.Query["OnComplete"].FirstOrDefault() ?? "Redirect");
OnPendingEnum OnPending = Enum.Parse<OnPendingEnum>(req.Query["OnPending"].FirstOrDefault() ?? "OK");
_logger.LogInformation($"C# HTTP trigger function processed a request for status on {thisGUID} - OnComplete {OnComplete} - OnPending {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, thisGUID);
}
else
{
// If the blob doesn't exist, the function uses the OnPending parameter to determine the next action.
string scheme = Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT") == "Development" ? "http" : "https";
string rqs = $"{scheme}://{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 back-off period reaches one minute.
int backoff = 250;
while (!await inputBlob.ExistsAsync() && backoff < 64000)
{
_logger.LogInformation($"Synchronous mode {thisGUID}.blob - retrying in {backoff} ms");
backoff = backoff * 2;
await Task.Delay(backoff);
}
if (await inputBlob.ExistsAsync())
{
_logger.LogInformation($"Synchronous Redirect mode {thisGUID}.blob - completed after {backoff} ms");
return await OnCompleted(OnComplete, inputBlob, thisGUID);
}
else
{
_logger.LogInformation($"Synchronous mode {thisGUID}.blob - NOT FOUND after timeout {backoff} ms");
return new NotFoundResult();
}
}
default:
{
throw new InvalidOperationException($"Unexpected value: {OnPending}");
}
}
}
}
private async Task<IActionResult> OnCompleted(OnCompleteEnum OnComplete, BlockBlobClient inputBlob, string thisGUID)
{
switch (OnComplete)
{
case OnCompleteEnum.Redirect:
{
// The typical way to generate a shared access signature token in code requires the storage account key.
// If you need to use a managed identity to control access to your storage accounts in code, which is a recommended best practice, you should do so when possible.
// In this scenario, you don't have a storage account key, so you need to find another way to generate the shared access signatures.
// To generate shared access signatures, use a user delegation shared access signature. This approach lets you sign the shared access signature by using Microsoft Entra ID credentials instead of the storage account key.
BlobServiceClient blobServiceClient = inputBlob.GetParentBlobContainerClient().GetParentBlobServiceClient();
var userDelegationKey = await blobServiceClient.GetUserDelegationKeyAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(7));
// Redirect the shared access signature uniform resource identifier (URI) to blob storage.
return new RedirectResult(inputBlob.GenerateSASURI(userDelegationKey));
}
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
}
CloudBlockBlobExtensions Следующий класс предоставляет метод расширения, который проверка статуса использует для создания универсального идентификатора ресурса общего доступа делегирования пользователей (URI) для результирующего BLOB-объекта.
public static class CloudBlockBlobExtensions
{
public static string GenerateSASURI(this BlockBlobClient inputBlob, UserDelegationKey userDelegationKey)
{
BlobServiceClient blobServiceClient = inputBlob.GetParentBlobContainerClient().GetParentBlobServiceClient();
BlobSasBuilder blobSasBuilder = new BlobSasBuilder()
{
BlobContainerName = inputBlob.BlobContainerName,
BlobName = inputBlob.Name,
Resource = "b",
StartsOn = DateTimeOffset.UtcNow,
ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(10)
};
blobSasBuilder.SetPermissions(BlobSasPermissions.Read);
var blobUriBuilder = new BlobUriBuilder(inputBlob.Uri)
{
Sas = blobSasBuilder.ToSasQueryParameters(userDelegationKey, blobServiceClient.AccountName)
};
// Generate the shared access signature on the blob, which sets the constraints directly on the signature.
Uri sasUri = blobUriBuilder.ToUri();
// Return the URI string for the container, including the shared access signature token.
return sasUri.ToString();
}
}