【问题标题】:Asynchronous and synchronous HTTP request on server side, performance comparison服务器端异步和同步HTTP请求,性能对比
【发布时间】:2019-09-15 16:52:36
【问题描述】:

我试图弄清楚异步和同步 HTTP 请求处理的优缺点。我正在使用带有 Jersey 的 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);
    }

}

sync API 将运行在 Jetty 维护的工作线程中,async API 将主要运行在自定义线程池中。这是我的 Jmeter 结果

  • 测试 1,500 个 Jetty 工作线程,/sync 端点

  • 测试 2,500 个自定义线程,/async 端点

    结果表明,这两种方法没有太大区别。

我的问题是:这两种方法有什么区别,在哪种情况下我应该使用哪种模式?

相关话题:Performance difference between Synchronous HTTP Handler and Asynchronous HTTP Handler

更新


我按照建议延迟了 10 次运行测试

  • sync-500-server-thread

  • async-500-workerthread

【问题讨论】:

  • [1] 在“/async”摘要报告中,min 值 7(毫秒?)看起来非常低。如果您重复运行,报告是否仍然显示 min 的类似低值? [2] 您可能需要考虑将其中一个标签替换为 jmeter 以吸引更合适的受众。
  • 您是否尝试过延迟 10 秒进行复制?
  • @skomisa 这是因为我在运行异步测试时出现了一些套接字错误,如您所见,错误率为 1.08%
  • @user7294900 我以 10 秒的延迟重新运行测试用例,实际上,异步方式在最大延迟方面稍差一些,我认为这是因为这里有更多的上下文切换

标签: java multithreading http asynchronous jmeter


【解决方案1】:

以下是我的想法。

无论是同步还是异步请求,都与 HTTP 的性能无关,但它与您的应用程序的性能有关

同步请求将阻塞应用程序,直到它收到响应,而在异步请求中,基本上,您将把这项工作分配给一个单独的工作线程,该工作线程将处理其余的事情。所以在异步设计中,你的主线程仍然可以继续自己的工作。

假设由于某些限制(不是服务器的资源限制),您的服务器可以处理有限数量的连接(基本上每个连接都将在单独的线程中处理,这在我们使用的服务器之间有所不同)。如果您的服务器可以处理比连接更多的线程,并且如果您不希望由于创建的异步工作而返回任何数据,那么您可以设计异步逻辑。因为您将创建一个新线程来处理请求的任务。

但是,如果您希望在响应中返回操作结果,则不会有任何不同。

【讨论】:

  • 是要使用您的客户端连接线程还是从您的服务器创建一个新的 HttpRequest ?
  • 是的,我同意。但在大多数情况下,主线程无事可做,只是等待另一个工作线程的响应。
  • 在这种情况下同步 HTTP 请求就可以了
【解决方案2】:

您正在使用 @Suspended 与仍在等待响应的 async 结合

@Suspended 将暂停/暂停当前线程,直到它得到响应

如果您想在异步中获得更好的性能,请使用 immediate responseExecutorServiceFuture 编写不同的异步方法

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

【讨论】:

  • 感谢您指出挂起注释。但如代码中所述,端点应该返回“Hello”作为业务需求。如果我删除挂起,客户端将获得没有响应正文的 HTTP 204。
  • @JadeTang 你可以返回 "Hello" 但仍然使用 Future&lt;String&gt;
  • 如果我删除挂起,客户端将得到一个空响应的 HTTP 204
  • @JadeTang 不要删除挂起,但仍然使用Future&lt;String&gt;
【解决方案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 个处理步骤,但这一次,在每个步骤中,我们将调用另一个后端系统。

SYNC处理:

  1. 呼叫系统 B
  2. 等待系统 B 的响应
  3. 呼叫系统 C
  4. 等待系统 C 的响应

总花费时间:B + C

异步处理:

  1. 呼叫系统 B
  2. 通话没有阻塞,继续前进
  3. 呼叫系统 C
  4. 通话没有阻塞,继续前进
  5. 接收来自系统 B 的响应
  6. 接收来自系统 C 的响应
  7. 完成对客户端的调用

总花费时间:max(B, C)

为什么是最大值?由于所有呼叫都是非阻塞的,因此您将不得不等待最慢的后端回复。

【讨论】:

  • 我认为您将服务器端异步/同步 HTTP 请求处理与异步/同步 HTTP 客户端混淆了。如果我以同步方式处理 HTTP 请求,我仍然可以使用异步 HTTP 客户端请求系统 B 和 C,这给了我最大响应时间(B,C)
【解决方案4】:

我写这篇文章是根据我的个人经验写的,它使用异步处理程序为斯里兰卡的领先出租车叫车服务编写促销引擎,该服务甚至与优步并肩竞争。

在使用异步处理程序而不是同步处理程序时,它比性能更具有可扩展性、可用性和资源利用率。

如果您使用同步处理程序,则您的最大并发请求数由可用于接受新连接的线程数定义,之后您的服务将不再接受请求。

如果您使用异步处理程序,接受线程数与您可以满足的并发请求数无关。因此,您的服务可以从 100 rps 扩展到 100 万 rps,并且可以具有高可用性。

如果您担心延迟和吞吐量,如果您将非阻塞 API 与异步处理程序一起使用,则可以获得很好的改进。非阻塞套接字(NIO)、非阻塞存储(Mongo DB Reactive、Redis Reactive)、消息队列(Kafka、RabbitMQ)等。

【讨论】:

  • 如果您使用异步处理程序,接受线程数与您可以满足的并发请求无关。但是在这种情况下,你需要另一个线程池来处理你的 HTTP 请求,它也不能是无穷大的。
【解决方案5】:

异步编程的主要性能优势是您可以减少系统上的线程数量。 documentation of Jetty 看起来是个不错的参考。

servlet API(2.5 之前)仅支持同步调用样式,因此 servlet 需要进行的任何等待都必须是阻塞的。不幸的是,这意味着在等待期间必须保留分配给请求的线程及其所有资源:内核线程、堆栈内存和经常池化的缓冲区、字符转换器、EE 身份验证上下文等。保留这些资源是浪费系统资源等待中的资源。如果等待异步完成,则可以显着提高可扩展性和服务质量。

问题的代码没有利用这个优势,因为它仍然为每个请求阻塞一个线程,它只是与自定义线程池不同的线程:

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

请注意,这种实现特别有问题,因为除了阻塞线程之外,线程的数量还受到线程池的严格限制。

如果你想利用异步请求处理的优势,你必须重新实现TimeUnit.SECONDS.sleep(1L),这样它就不会阻塞任何线程。

【讨论】:

  • 谢谢,但在我们的例子中,TimeUnit.SECONDS.sleep(1L) 实际上是一个 MySQL 数据库调用,我没有看到任何不阻塞的 MySQL 数据库驱动程序。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多