Асинхронный и синхронный HTTP-запрос на стороне сервера, сравнение производительности - программирование
Подтвердить что ты не робот

Асинхронный и синхронный HTTP-запрос на стороне сервера, сравнение производительности

Я пытаюсь выяснить плюсы и минусы асинхронной и синхронной обработки HTTP-запросов. Я использую Dropwizard с Джерси в качестве основы. Тест сравнивает асинхронную и синхронную обработку HTTP-запросов, это мой код

@Path("/")
public class RootResource {

    ExecutorService executor;

    public RootResource(int threadPoolSize){
        executor = Executors.newFixedThreadPool(threadPoolSize);
    }

    @GET
    @Path("/sync")
    public String sayHello() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1L);
        return "ok";
    }

    @GET
    @Path("/async")
    public void sayHelloAsync(@Suspended final AsyncResponse asyncResponse) throws Exception {
        executor.submit(() -> {
            try {
                doSomeBusiness();
                asyncResponse.resume("ok");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }


    private void doSomeBusiness() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1L);
    }

}

API синхронизации будет выполняться в рабочем потоке, поддерживаемом Jetty, а асинхронный API будет в основном работать в пуле таможенных потоков. И вот мой результат по Jmeter

  • Тест 1, рабочий поток 500 Jetty,/конечная точка синхронизации

    enter image description here

  • Тест 2, 500 пользовательских потоков,/асинхронная конечная точка

    enter image description here Как показывает результат, между этими двумя подходами нет большой разницы.

Мой вопрос будет таким: каковы различия между этими двумя подходами и какой шаблон следует использовать в каком сценарии?

Связанная тема: Разница в производительности между синхронным обработчиком HTTP и асинхронным обработчиком HTTP

Обновить


Я запускаю тест с 10 задержками, как предложено

  • синхронизация-500-сервер-нить

sync-500-server-thread

  • Асинхры-500-WorkerThread

async-500-workerthread

4b9b3361

Ответ 1

Ниже приведены мои мысли.

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

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

Допустим, из-за некоторого ограничения (а не ограничения ресурса сервера) ваш сервер может обрабатывать ограниченное количество соединений (в основном каждое соединение будет обрабатываться в отдельном потоке, в зависимости от сервера, который мы используем). Если ваш сервер может обрабатывать большее количество потоков, чем подключений, а также если вы не хотите возвращать какие-либо данные в результате асинхронной работы, которую вы создали, то вы можете разработать асинхронную логику. Потому что вы создадите новый поток для обработки запрошенной задачи.

Но если вы ожидаете, что результаты операций будут возвращены в ответе, ничто не будет отличаться.

Ответ 2

Вы используете @Suspended в сочетании с async, которые все еще ждут ответа

@Suspended приостановит/приостановит текущий поток, пока он не получит ответ

Если вы хотите повысить производительность в асинхронном режиме, напишите другой асинхронный метод с немедленным ответом, используя ExecutorService и Future

private ExecutorService executor;
private Future<String> futureResult;
@PostConstruct
public void onCreate() {
    this.executor = Executors.newSingleThreadExecutor();
}
@POST
public Response startTask() {
    futureResult = executor.submit(new ExpensiveTask());
    return Response.status(Status.ACCEPTED).build();
}
@GET
public Response getResult() throws ExecutionException, InterruptedException {
    if (futureResult != null && futureResult.isDone()) {
        return Response.status(Status.OK).entity(futureResult.get()).build();
    } else {
        return Response.status(Status.FORBIDDEN).entity("Try later").build();
    }
}

Ответ 3

Давайте рассмотрим следующий сценарий:

Single Backend system
                    ____________
                   |  System A  |
 HTTP Request -->  |            |
                   |  1.        |
                   |  2.        |
 HTTP Response <-- |            |
                   |____________|

У вас есть одна внутренняя система, которая выполняет некоторую обработку на основе запроса, полученного по определенному заказу (операция 1, а затем операция 2). Если вы обрабатываете запрос синхронно или асинхронно, на самом деле это не имеет значения, это тот же объем вычислений, который необходимо выполнить (возможно, некоторые небольшие изменения, с которыми вы столкнулись в своем тесте).

Теперь давайте рассмотрим сценарий с несколькими бэкэндами:

Multi-Backend System
                        ____________
                       |  System A  |       __________
     HTTP Request -->  |            | -->  |          |
                       |  1.        |      | System B |
                       |            | <--  |__________|
                       |            |       __________  
                       |  2.        | -->  |          |
     HTTP Response <-- |            |      | System C |
                       |____________| <--  |__________|

Тем не менее, необходимо выполнить 2 шага обработки, но на этот раз на каждом шаге мы будем вызывать другую систему back'end.

Обработка SYNC:

  1. Система вызова B
  2. Ждите ответа от Системы B
  3. Call System C
  4. Ждите ответа от Системы C

Общее время проведенное: B + C

Обработка ASYNC:

  1. Система вызова B
  2. Идите вперед, так как звонок не блокируется
  3. Call System C
  4. Идите вперед, так как звонок не блокируется
  5. Получите ответ от Системы B
  6. Получите ответ от Системы C
  7. Завершите звонок клиенту

Общее потраченное время: макс. (B, C)

Почему макс? Поскольку все вызовы не блокируются, вам придется ждать только самого медленного back'end, чтобы ответить.

Ответ 4

Я пишу это из своего личного опыта написания движка Promotions, использующего асинхронные обработчики для службы такси, работающей на Шри-Ланке, которая даже конкурирует плечом к плечу с Uber.

Это больше масштабируемость, доступность и использование ресурсов, чем производительность, когда речь идет об использовании асинхронных обработчиков поверх синхронных обработчиков.

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

Если, как если бы вы использовали асинхронные обработчики, счетчик принимаемых потоков не имеет никакого отношения к числу одновременных запросов, которые вы можете обработать. Таким образом, ваш сервис может масштабироваться от 100 до 1 миллиона об/мин и может иметь высокую доступность.

Если вы беспокоитесь о задержке и пропускной способности, вы можете получить хорошие улучшения, если используете неблокирующие API с асинхронными обработчиками. Неблокирующее гнездо (NIO), Неблокирующее хранилище (Mongo DB Reactive, Redis Reactive), Очереди сообщений (Kafka, RabbitMQ) и т.д.

Ответ 5

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

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

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

Синхронизированный поток:

Thread -> sync request method(0ms) -> html request(0ms) -> wait(60ms) -> get content(20ms) -> return content to main thread(0ms) -> Thread resumes action
Thread unresponsive for 80ms

Асинхронный поток:

Thread -> async request method(0ms) -> create handler/callback(0ms) -> perform(0ms) -> return to thread(0ms) -> thread resumes action
                                                                 | 
                                                                 V
                                                               html request(0ms) -> wait(60ms) -> get content(20ms) -> execute callback(1ms)
Thread unresponsive for +-0ms

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

Синхронизированный поток, предполагающий 3 соединения, прибывающих одновременно, система fifo:

Thread -> accept connections -> wait
          {connection1} -> accept connection(0ms) -> handle connection(20ms) -> return
          {connection2 -> wait for connection1 to finish(20ms)} -> accept connection(0ms) -> handle connection(2400ms) -> return
          {connection3 -> wait for connection2 to finish(2420ms)} -> accept connection(0ms) -> handle connection(20ms) -> return

Соединению 3 пришлось подождать 2420 мс, несмотря на то, что у него был "легкий" запрос, потому что принимающий поток был занят, а предыдущие соединения не получили тайм-аут, из-за которого была прервана попытка соединения.

Асинхронный поток, предполагающий 3 соединения, прибывающих одновременно, система fifo:

Thread -> accept connections -> wait
          {connection1} -> accept connection(0ms) -> handle connection(20ms) -> return
          {connection2} -> accept connection(0ms) -> handle connection(2410ms) -> return
          {connection3} -> accept connection(0ms) -> handle connection(40ms) -> return

Не нужно ждать подключения. Недостатком может быть то, что время отклика может отставать, потому что ресурсы могут быть заняты и должны быть освобождены/разблокированы (база данных, что угодно), но соединения принимаются и обрабатываются как можно скорее, а не ставятся в очередь с риском потери.