scala - lightbend - akka http quickstart
Akka Http Performance tuning (1)
Algunos descargos de responsabilidad primero: no he trabajado con la herramienta wrk
antes, por lo que podría fallar algo. Aquí están las suposiciones que he hecho para esta respuesta:
- El recuento de conexiones es independiente del recuento de subprocesos, es decir, si especifico
-t4 -c10000
mantiene 10000 conexiones, no 4 * 10000. - Para cada conexión, el comportamiento es el siguiente: envía la solicitud, recibe la respuesta completamente e inmediatamente envía la siguiente, etc., hasta que se agote el tiempo.
También he ejecutado el servidor en la misma máquina que wrk, y mi máquina parece ser más débil que la tuya (solo tengo CPU de doble núcleo), así que reduje el número de hilos de wrk a 2 y la conexión a 1000. para obtener resultados decentes.
La versión Akka Http que he usado es la 10.0.1
, y la versión 10.0.1
es la 10.0.1
.
Ahora a la respuesta. Veamos el código de bloqueo que tiene:
Future { // Blocking code
Thread.sleep(100)
"OK"
}
Esto significa que cada solicitud tomará al menos 100 milisegundos. Si tiene 200 hilos y 1000 conexiones, la línea de tiempo será la siguiente:
Msg: 0 200 400 600 800 1000 1200 2000
|--------|--------|--------|--------|--------|--------|---..---|---...
Ms: 0 100 200 300 400 500 600 1000
Donde Msg
es la cantidad de mensajes procesados, Ms
es el tiempo transcurrido en milisegundos.
Esto nos da 2000 mensajes procesados por segundo, o ~ 60000 mensajes por 30 segundos, lo que en su mayoría está de acuerdo con las cifras de la prueba:
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
También es obvio que este número (2000 mensajes por segundo) está estrictamente vinculado por el número de hilos. Por ejemplo, si tuviéramos 300 hilos, procesaríamos 300 mensajes cada 100 ms, por lo que tendríamos 3000 mensajes por segundo, si nuestro sistema puede manejar tantos hilos. Veamos cómo nos irá si proporcionamos 1 hilo por conexión, es decir, 1000 hilos en el grupo:
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
Como puede ver, ahora una solicitud toma casi exactamente 100 ms en promedio, es decir, la misma cantidad que ponemos en Thread.sleep
. ¡Parece que no podemos llegar mucho más rápido que esto! Ahora estamos prácticamente en una situación estándar de one thread per request
, que funcionó bastante bien durante muchos años, hasta que la IO asíncrona permitió que los servidores se escalasen mucho más.
En aras de la comparación, aquí están los resultados de la prueba totalmente no bloqueantes en mi máquina con el conjunto de subprocesos de unión de bifurcación predeterminado:
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
Para resumir, si utiliza operaciones de bloqueo, necesita un subproceso por solicitud para lograr el mejor rendimiento, así que configure su grupo de subprocesos en consecuencia. Hay límites naturales para la cantidad de subprocesos que puede manejar su sistema, y es posible que deba ajustar su sistema operativo para que el número máximo de subprocesos. Para obtener el mejor rendimiento, evite las operaciones de bloqueo.
Tampoco confunda las operaciones asíncronas con las no bloqueantes. Su código con Future
y Thread.sleep
es un ejemplo perfecto de operación asíncrona, pero de bloqueo. Un montón de software popular opera en este modo (algunos clientes HTTP heredados, controladores Cassandra, AWS Java SDK, etc.). Para aprovechar al máximo los beneficios del servidor HTTP sin bloqueo, debe estar sin bloqueo hasta el final, no solo asíncrono. Puede que no siempre sea posible, pero es algo por lo que luchar.
Estoy realizando pruebas de carga en el marco Akka-http (versión: 10.0), estoy usando la herramienta wrk . comando wrk:
wrk -t6 -c10000 -d 60s --timeout 10s --latency http://localhost:8080/hello
primera ejecución sin ninguna llamada de bloqueo,
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"
}
}
}
}
salida de 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
Ahora si agrego una llamada futura en la ruta y vuelvo a ejecutar la prueba.
val route =
path("hello") {
get {
complete {
Future { // Blocking code
Thread.sleep(100)
"OK"
}
}
}
}
Salida, de 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
Como se puede ver en futuras llamadas, solo se están atendiendo 13744 solicitudes .
Después de seguir la documentación de Akka , agregué un grupo de subprocesos de despachador por separado para la ruta que crea un máximo de 200 subprocesos .
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
}
Después del cambio anterior, el rendimiento mejoró un poco.
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
En la configuración de my -bloqueo-despachador, si aumente el tamaño de la agrupación por encima de 200, el rendimiento es el mismo.
Ahora, ¿qué otros parámetros o configuraciones debo usar para aumentar el rendimiento mientras uso una llamada futura? Por lo tanto, esa aplicación proporciona el rendimiento máximo.