Pytanie Akka Http Performance Tuning


Przeprowadzam testowanie obciążenia na frameworku Akka-http (wersja: 10.0), którego używam wrk narzędzie. polecenie wrk:

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

pierwszy uruchom bez blokowania,

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"
        }
      }
    }
}

output of 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

Teraz jeśli dodaję przyszłe połączenie na trasie i ponownie uruchomę test.

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

Wyjście, z 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

Jak widać z przyszłego połączenia tylko Przesłano 13744 wniosków.

Po wykonaniu czynności Dokumentacja Akka, Dodałem oddzielną pulę wątków dyspozytora dla trasy, która tworzy max, z 200 wątków.

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
}

Po powyższej zmianie wydajność nieco się poprawiła

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

w my-blocking-dispatcher config jeśli zwiększę wielkość puli powyżej 200, wydajność jest taka sama.

Teraz, jakie inne parametry lub config powinienem użyć, aby zwiększyć wydajność podczas korzystania z przyszłego połączenia. Ta aplikacja zapewnia maksymalną przepustowość.


18
2017-12-23 07:50


pochodzenie




Odpowiedzi:


Oto niektóre z nich: nie pracowałem wrk narzędzie wcześniej, więc może coś złego. Oto założenia, które poczyniłem dla tej odpowiedzi:

  1. Liczba połączeń jest niezależna od liczby wątków, tj. Jeśli określam -t4 -c10000 utrzymuje 10000 połączeń, a nie 4 * 10000.
  2. Dla każdego połączenia zachowanie jest następujące: wysyła żądanie, odbiera odpowiedź całkowicie i natychmiast wysyła następną itd., Aż skończy się czas.

Uruchomiłem też serwer na tej samej maszynie co wrk, a moja maszyna wydaje się być słabsza od twojej (mam tylko dwurdzeniowy procesor), więc zmniejszyłem liczbę wątków wrk do 2, a liczba połączeń do 1000, aby uzyskać przyzwoite wyniki.

Używana wersja Http Akka to 10.0.1i wersja wrk jest 4.0.2.

Teraz odpowiedź. Spójrzmy na kod blokujący, który masz:

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

Oznacza to, że każde żądanie potrwa co najmniej 100 milisekund. Jeśli masz 200 wątków i 1000 połączeń, oś czasu będzie wyglądała następująco:

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

Gdzie Msg to ilość przetworzonych wiadomości, Ms upłynął czas w milisekundach.

Daje to 2000 wiadomości przetworzonych na sekundę lub ~ 60000 wiadomości na 30 sekund, co w większości zgadza się z wynikami testu:

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

Oczywiste jest również, że liczba ta (2000 komunikatów na sekundę) jest ściśle powiązana z liczbą wątków. Na przykład. gdybyśmy mieli 300 wątków, przetwarzalibyśmy 300 wiadomości co 100 ms, więc mielibyśmy 3000 wiadomości na sekundę, jeśli nasz system może obsłużyć tak wiele wątków. Zobaczmy, jak zapłacimy, jeśli zapewniamy 1 wątek na połączenie, czyli 1000 wątków w puli:

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

Jak widzisz, teraz jedno żądanie zajmuje średnio dokładnie 100 ms, czyli tyle samo, co my Thread.sleep. Wygląda na to, że nie możemy uzyskać dużo szybciej niż to! Teraz jesteśmy prawie w standardowej sytuacji one thread per request, który działał całkiem dobrze przez wiele lat, dopóki asynchroniczne serwery IO let nie skalują się znacznie wyżej.

Dla porównania, oto pełne nieblokujące wyniki testu na moim komputerze z domyślną pulą wątków łączenia widełek:

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

Podsumowując, jeśli używasz operacji blokowania, potrzebujesz jednego wątku na żądanie, aby uzyskać najlepszą przepustowość, więc odpowiednio skonfiguruj swoją pulę wątków. Istnieją naturalne ograniczenia dotyczące liczby wątków obsługiwanych przez Twój system i może być konieczne dostrojenie systemu operacyjnego w celu uzyskania maksymalnej liczby wątków. Aby uzyskać najlepszą przepustowość, unikaj operacji blokowania.

Nie należy również mylić operacji asynchronicznych z operacjami nieblokującymi. Twój kod za pomocą Future i Thread.sleep jest doskonałym przykładem asynchronicznej, ale blokującej operacji. W tym trybie działa wiele popularnych programów (niektórzy starsze klienty HTTP, sterowniki Cassandra, pakiety SDK Java AWS itp.). Aby w pełni czerpać korzyści z nieblokującego się serwera HTTP, musisz nie blokować całego rozwiązania, a nie tylko asynchronicznego. Może nie zawsze jest to możliwe, ale jest to coś, do czego trzeba dążyć.


24
2017-12-23 16:13



Bardzo dobra analiza. <łuki> - Mon Calamari
Podsumowując, problem polega na tym Thread.sleep zjada twoje wątki. Do celów testowych możesz także spróbować akka.pattern.after stworzyć przyszłość, która zostanie ukończona dopiero później, bez blokowania wątków. - jrudolph
@Haspemulator Jestem nowicjuszem w świecie akka i musiałem naprawdę podrapać się w głowę, co znalazło się w pliku conf, aby uzyskać wspomnianą wydajność. "Dla porównania, oto w pełni nieblokujące wyniki testu na mojej maszynie z domyślnym widelcem -jako pulę wątków: "czy możesz podzielić się tym samym. Twoja odpowiedź jest bardzo pouczająca. Twoje zdrowie - Akash
@Akash nie ma nic w pliku .conf dla domyślnej puli wątków join-join. Tak naprawdę jest domyślny wartość. :) Nie mam kodu, którego użyłem do udzielenia odpowiedzi na to pytanie, więc nie mogę tu niczego opublikować. Wystarczy spojrzeć na dokumentację HTTP Akka, wszystko powinno tam być. - Haspemulator
Dziękujemy za tak szczegółowe wyjaśnienia i testy! Rzeczywiście bardzo przydatne. - Alexey