Akka Http 性能调优
Akka Http Performance tuning
我正在对 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
现在,如果我在路由中添加一个未来的呼叫,然后 运行 再次测试。
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
正如您在未来的调用中看到的那样,只有 13744 个请求得到处理。
在遵循 Akka documentation 之后,我为创建最大 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 配置中 如果我将池大小增加到 200 以上,性能是一样的。
现在,我应该使用哪些其他参数或配置来提高性能,同时使用未来 call.So 该应用程序提供最大吞吐量。
首先声明一些免责声明:我之前没有使用过 wrk
工具,所以我可能会出错。以下是我为这个答案所做的假设:
- 连接数独立于线程数,即如果我指定
-t4 -c10000
它保持 10000 个连接,而不是 4 * 10000。
- 对于每个连接,行为如下:它发送请求,完全接收响应,然后立即发送下一个,等等,直到 运行 结束。
另外我运行服务器和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
的时间相同。看来我们不能比这更快了!现在我们几乎处于 one thread per request
的标准情况下,在异步 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
总而言之,如果您使用阻塞操作,则每个请求需要一个线程来实现最佳吞吐量,因此请相应地配置您的线程池。您的系统可以处理的线程数存在自然限制,您可能需要调整 OS 以获得最大线程数。为获得最佳吞吐量,请避免阻塞操作。
也不要将异步操作与非阻塞操作混淆。您使用 Future
和 Thread.sleep
的代码是异步但阻塞操作的完美示例。许多流行的软件都在这种模式下运行(一些遗留的 HTTP 客户端、Cassandra 驱动程序、AWS Java SDK 等)。要充分享受非阻塞 HTTP 服务器的好处,您需要始终保持非阻塞,而不仅仅是异步。这可能并不总是可能的,但这是值得努力的事情。
使用此配置,我在本地主机上获得了 x3 性能:
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.
我正在对 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
现在,如果我在路由中添加一个未来的呼叫,然后 运行 再次测试。
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
正如您在未来的调用中看到的那样,只有 13744 个请求得到处理。
在遵循 Akka documentation 之后,我为创建最大 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 配置中 如果我将池大小增加到 200 以上,性能是一样的。
现在,我应该使用哪些其他参数或配置来提高性能,同时使用未来 call.So 该应用程序提供最大吞吐量。
首先声明一些免责声明:我之前没有使用过 wrk
工具,所以我可能会出错。以下是我为这个答案所做的假设:
- 连接数独立于线程数,即如果我指定
-t4 -c10000
它保持 10000 个连接,而不是 4 * 10000。 - 对于每个连接,行为如下:它发送请求,完全接收响应,然后立即发送下一个,等等,直到 运行 结束。
另外我运行服务器和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
的时间相同。看来我们不能比这更快了!现在我们几乎处于 one thread per request
的标准情况下,在异步 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
总而言之,如果您使用阻塞操作,则每个请求需要一个线程来实现最佳吞吐量,因此请相应地配置您的线程池。您的系统可以处理的线程数存在自然限制,您可能需要调整 OS 以获得最大线程数。为获得最佳吞吐量,请避免阻塞操作。
也不要将异步操作与非阻塞操作混淆。您使用 Future
和 Thread.sleep
的代码是异步但阻塞操作的完美示例。许多流行的软件都在这种模式下运行(一些遗留的 HTTP 客户端、Cassandra 驱动程序、AWS Java SDK 等)。要充分享受非阻塞 HTTP 服务器的好处,您需要始终保持非阻塞,而不仅仅是异步。这可能并不总是可能的,但这是值得努力的事情。
使用此配置,我在本地主机上获得了 x3 性能:
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.