quickstart lightbend explained example scala akka akka-http

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:

  1. El recuento de conexiones es independiente del recuento de subprocesos, es decir, si especifico -t4 -c10000 mantiene 10000 conexiones, no 4 * 10000.
  2. 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.