1
votes

We moved from using Camel HTTP4 to Akka HTTP and although we are now able to control errors much better, it is getting really hard to get better performance considering all the adjustable parameters in Akka HTTP (Client).

We have an actor that receives messages, makes an HTTP GET request to an external service (easily capable of managing over 1500 RPS) and then responds with the http response body as a String.

We are capped at 650 RPS right now, and even tho we are not getting errors (as previously with Camel's HTTP4), we cannot go over those 650 (as opposed to previous 800 RPS with HTTP4 using default parameters).

Our HTTP requests are made with singleRequest:

val httpResponseFuture: Future[HttpResponse] = http.singleRequest(HttpRequest(uri = uri))

val tokenizationResponse = for {
  response <- httpResponseFuture
  body <- Unmarshal(response.entity).to[String]
} yield transformResponse(response.status, body, path, response.headers)

And then these are the settings that produced the best results (going over those numbers doesn't show any real improvement:

akka {

    actor.deployment {
      /HttpClient {
        router = balancing-pool
        nr-of-instances = 7
      }
    }

    http {
      host-connection-pool {
        max-connections = 30
        max-retries = 5
        max-open-requests = 8192
        pipelining-limit = 200
        idle-timeout = 30 s
      }
    }

}

We tried resizing the pool, the actor instances, all the other parameters under host-connection-pool but we can't get any better.

Any suggestions are welcome!

1

1 Answers

2
votes

Don't Mix & Match Concurrency

Presumably your querying functionality is just sending a message to the Actor and waiting for a response:

//what your code may look like now

object Message

val queryActorRef : ActorRef = ???

val responseBody : Future[String] = (queryActorRef ? Message).mapTo[String]

But this is unnecessary. The only reason to utilize an Actor in this use case would be to protect a limited resource. But the underlying http connection pool deals with resource utilization for you. Removing the Actor intermediary will allow you to work with Futures alone:

val entityTimeout : FiniteDuration = 10.seconds

val responseBodyWithoutAnActor : Future[String] = 
    http
      .singleRequest(HttpRequest(uri = uri))
      .flatMap(response => response.entity.toStrict(timeout))
      .map(_.data.utf8String)

Streams

If the "messages" being sent to the Actor have an underlying source, e.g. an Iterable, then you can use streaming instead:

type Message = ???

val messagesSource : Iterable[Message] = ???

val uri : String = ???

val poolClientFlow = Http().cachedHostConnectionPool[Promise[HttpResponse]](uri)

val entityParallelism = 10

Source
  .apply(messagesSource)
  .via(poolClientFlow)
  .mapAsync(entityParallelism)(resp.entity.toStrict(entityTimeout).data.utf8String)
  .runForeach { responseBody : String =>
    //whatever you do with the bodies
  }