Akka Http 性能调优

22

我正在对Akka-http框架(版本:10.0)进行负载测试,使用的是wrk工具。 wrk命令:

wrk -t6 -c10000 -d 60s --timeout 10s --latency http://localhost:8080/hello

首次运行没有任何阻塞调用。

object WebServer {

  implicit val system = ActorSystem("my-system")
  implicit val materializer = ActorMaterializer()
  implicit val executionContext = system.dispatcher
  def main(args: Array[String]) {


    val bindingFuture = Http().bindAndHandle(router.route, "localhost", 8080)

    println(
      s"Server online at http://localhost:8080/\nPress RETURN to stop...")
    StdIn.readLine() // let it run until user presses return
    bindingFuture
      .flatMap(_.unbind()) // trigger unbinding from the port
      .onComplete(_ => system.terminate()) // and shutdown when done
  }
}

object router {
  implicit val executionContext = WebServer.executionContext


  val route =
    path("hello") {
      get {
        complete {
        "Ok"
        }
      }
    }
}

wrk的输出结果:

    Running 1m test @ http://localhost:8080/hello
  6 threads and 10000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     4.22ms   16.41ms   2.08s    98.30%
    Req/Sec     9.86k     6.31k   25.79k    62.56%
  Latency Distribution
     50%    3.14ms
     75%    3.50ms
     90%    4.19ms
     99%   31.08ms
  3477084 requests in 1.00m, 477.50MB read
  Socket errors: connect 9751, read 344, write 0, timeout 0
Requests/sec:  57860.04
Transfer/sec:      7.95MB

现在,如果我在路由中添加一个future call并再次运行测试。

val route =
    path("hello") {
      get {
        complete {
          Future { // Blocking code
            Thread.sleep(100)
            "OK"
          }
        }
      }
    }

输出,关于wrk:

Running 1m test @ http://localhost:8080/hello
  6 threads and 10000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   527.07ms  491.20ms  10.00s    88.19%
    Req/Sec    49.75     39.55   257.00     69.77%
  Latency Distribution
     50%  379.28ms
     75%  632.98ms
     90%    1.08s 
     99%    2.07s 
  13744 requests in 1.00m, 1.89MB read
  Socket errors: connect 9751, read 385, write 38, timeout 98
Requests/sec:    228.88
Transfer/sec:     32.19KB

正如您所看到的,仅使用future调用,提供了13744个请求

按照Akka文档的建议,我为路由添加了一个专用调度程序线程池,其中最多创建200个线程

implicit val executionContext = WebServer.system.dispatchers.lookup("my-blocking-dispatcher")
// config of dispatcher
my-blocking-dispatcher {
  type = Dispatcher
  executor = "thread-pool-executor"
  thread-pool-executor {
    // or in Akka 2.4.2+
    fixed-pool-size = 200
  }
  throughput = 1
}

在上述更改之后,性能略有提升。

Running 1m test @ http://localhost:8080/hello
  6 threads and 10000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   127.03ms   21.10ms 504.28ms   84.30%
    Req/Sec   320.89    175.58   646.00     60.01%
  Latency Distribution
     50%  122.85ms
     75%  135.16ms
     90%  147.21ms
     99%  190.03ms
  114378 requests in 1.00m, 15.71MB read
  Socket errors: connect 9751, read 284, write 0, timeout 0
Requests/sec:   1903.01
Transfer/sec:    267.61KB

如果我在my-blocking-dispatcher config中将线程池大小增加到200以上,性能不会有所改善。

现在,我应该使用哪些其他参数或配置来增加使用future call时的性能,以便应用程序提供最大吞吐量。

2个回答

29

首先有一些免责声明:我以前没有使用过wrk工具,所以可能会有些不正确。这是我为此答案做出的假设:

  1. 连接数独立于线程数,即如果我指定 -t4 -c10000,则它保持10000个连接,而不是4 * 10000。
  2. 对于每个连接,行为如下:发送请求,完全接收响应,并立即发送下一个请求,等到时间耗尽.

此外,我在与wrk相同的机器上运行了服务器,我的机器似乎比你的弱(我只有双核CPU),因此我将wrk的线程计数降低到2,将连接计数降低到1000,以获得良好的结果。

我使用的Akka Http版本是10.0.1,wrk版本是4.0.2

现在来看看你的阻塞代码:

Future { // Blocking code
  Thread.sleep(100)
  "OK"
}

这意味着每个请求至少需要100毫秒。如果你有200个线程和1000个连接,时间轴将如下所示:

Msg: 0       200      400      600      800     1000     1200      2000
     |--------|--------|--------|--------|--------|--------|---..---|---...
Ms:  0       100      200      300      400      500      600      1000

其中Msg是已处理的消息量,Ms是以毫秒为单位的经过时间。

这意味着每秒处理2000条消息,或者每30秒处理约60000条消息,这基本上与测试数据相符:

wrk -t2 -c1000 -d 30s --timeout 10s --latency http://localhost:8080/hello
Running 30s test @ http://localhost:8080/hello
  2 threads and 1000 connections
  Thread Stats   Avg     Stdev     Max   +/- Stdev
    Latency   412.30ms   126.87ms 631.78ms   82.89%
    Req/Sec     0.95k    204.41     1.40k    75.73%
  Latency Distribution
     50%  455.18ms
     75%  512.93ms
     90%  517.72ms
     99%  528.19ms
here: --> 56104 requests in 30.09s <--, 7.70MB read
  Socket errors: connect 0, read 1349, write 14, timeout 0
Requests/sec:   1864.76
Transfer/sec:    262.23KB

很明显,每秒发送2000条消息的数量受线程数的严格限制。例如,如果我们有300个线程,我们每100毫秒就会处理300个消息,因此我们每秒可以处理3000个消息(如果我们的系统可以处理这么多线程)。让我们看看如果我们为每个连接提供1个线程,即在池中使用1000个线程,我们将取得什么成果:

wrk -t2 -c1000 -d 30s --timeout 10s --latency http://localhost:8080/hello
Running 30s test @ http://localhost:8080/hello
  2 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   107.08ms   16.86ms 582.44ms   97.24%
    Req/Sec     3.80k     1.22k    5.05k    79.28%
  Latency Distribution
     50%  104.77ms
     75%  106.74ms
     90%  110.01ms
     99%  155.24ms
  223751 requests in 30.08s, 30.73MB read
  Socket errors: connect 0, read 1149, write 1, timeout 0
Requests/sec:   7439.64
Transfer/sec:      1.02MB

如您所见,现在每个请求平均需要花费大约100毫秒的时间,即我们放入 Thread.sleep 的相同数量。看起来我们不能比这更快了!现在我们几乎处于标准的 一请求一个线程 的情况下,这在许多年内运行得非常好,直到异步IO使服务器的扩展能力大幅提升。

为了比较,以下是在我的计算机上使用默认的fork-join线程池进行完全非阻塞测试的结果:

complete {
  Future {
    "OK"
  }
}

====>

wrk -t2 -c1000 -d 30s --timeout 10s --latency http://localhost:8080/hello
Running 30s test @ http://localhost:8080/hello
  2 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    15.50ms   14.35ms 468.11ms   93.43%
    Req/Sec    22.00k     5.99k   34.67k    72.95%
  Latency Distribution
     50%   13.16ms
     75%   18.77ms
     90%   25.72ms
     99%   66.65ms
  1289402 requests in 30.02s, 177.07MB read
  Socket errors: connect 0, read 1103, write 42, timeout 0
Requests/sec:  42946.15
Transfer/sec:      5.90MB

总之,如果您使用阻塞操作,需要每个请求一个线程才能实现最佳吞吐量,因此请相应地配置您的线程池。系统可以处理多少个线程有自然限制,您可能需要调整操作系统以获得最大线程数。为了获得最佳吞吐量,请避免阻塞操作。

此外,不要将异步操作与非阻塞操作混淆。您的代码中的 Future 和 Thread.sleep 是异步但阻塞操作的完美示例。许多流行软件都在这种模式下运行(一些传统的 HTTP 客户端、Cassandra 驱动程序、AWS Java SDK 等)。为了充分利用非阻塞 HTTP 服务器的优势,您需要始终保持非阻塞,而不仅仅是异步。虽然这可能并不总是可能,但这是值得努力追求的目标。


5
总之,问题在于Thread.sleep会消耗你的线程。为了测试目的,你也可以尝试使用akka.pattern.after来创建一个仅在稍后完成而不阻塞线程的Future。 - jrudolph
@Haspemulator 我是Akka世界的新手,为了获得所提到的性能,我必须仔细研究conf文件中包含了什么内容。"为了比较起见,这是在我的机器上使用默认fork-join线程池的完全非阻塞测试结果:" 你能分享一下这个吗?你的回答非常有用。干杯! - Akash
@Akash 在默认的 fork-join 线程池的 .conf 文件中没有任何内容。这实际上是 默认值。 :) 我不再拥有我用来回答这个问题的代码,所以我无法在这里发布任何东西。只需查看 Akka HTTP 文档,所有内容都应该在那里。 - Haspemulator
非常感谢您提供如此详细的解释和测试!确实非常有用。 - Alexey
除了配置阻塞调度程序之外,我们是否需要使用任何Akka路由? - Diego Ramos

0

使用这个配置,我的本地主机性能提高了3倍:

akka {
  actor {
    default-dispatcher {
      fork-join-executor {
        parallelism-min = 1
        parallelism-max = 64
        parallelism-factor = 1
      }
      throughput = 64
    }
  }

  http {
    host-connection-pool {
      max-connections = 10000
      max-open-requests = 4096
    }

    server {
      pipelining-limit = 1024
      max-connections = 4096
      backlog = 1024
    }
  }
}

也许这些参数的其他值会更好(如果是,请写信给我)。

Akka Http 版本为 10.1.12。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接