I am trying to choose the best approach for making a large number of http requests in parallel. Below are the two approaches I have so far:
Using Apache HttpAsyncClient and CompletableFutures:
try (CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom()
.setMaxConnPerRoute(2000).setMaxConnTotal(2000)
.setUserAgent("Mozilla/4.0")
.build()) {
httpclient.start();
HttpGet request = new HttpGet("http://bing.com/");
long start = System.currentTimeMillis();
CompletableFuture.allOf(
Stream.generate(()->request).limit(1000).map(req -> {
CompletableFuture<Void> future = new CompletableFuture<>();
httpclient.execute(req, new FutureCallback<HttpResponse>() {
@Override
public void completed(final HttpResponse response) {
System.out.println("Completed with: " + response.getStatusLine().getStatusCode())
future.complete(null);
}
...
});
System.out.println("Started request");
return future;
}).toArray(CompletableFuture[]::new)).get();
Conventional thread-per-request approach:
long start1 = System.currentTimeMillis();
URL url = new URL("http://bing.com/");
ExecutorService executor = Executors.newCachedThreadPool();
Stream.generate(()->url).limit(1000).forEach(requestUrl ->{
executor.submit(()->{
try {
URLConnection conn = requestUrl.openConnection();
System.out.println("Completed with: " + conn.getResponseCode());
} catch (IOException e) {
e.printStackTrace();
}
});
System.out.println("Started request");
});
Across multiple runs, I noticed that the conventional approach was finishing almost twice as fast as the async/future approach.
Although I expected dedicated threads to run faster, is the difference supposed to be this remarkable or perhaps there is something wrong with the async implementation? If not, what is the right approach to go about here?
The Apache HTTP client was the first widely adopted open source client to be released, arriving in its original form in 2002. Now on its 5th major version, it's probably still the most commonly used client outside of Java's core libraries.
Concurrent execution of HTTP methodsHttpClient is fully thread-safe when used with a thread-safe connection manager such as MultiThreadedHttpConnectionManager.
Once created, an HttpClient instance is immutable, thus automatically thread-safe, and you can send multiple requests with it.
[Closeable]HttpClient implementations are expected to be thread safe. It is recommended that the same instance of this class is reused for multiple request executions.
The question in place is dependent on a lot of factors:
First question - is the difference supposed to be this remarkable?
Depends on the load, pool size and network but it could be way more than the observed factor of 2 in each of the directions (in favour of Async or threaded solution). According to your later comment the difference is more because of a misconduct, but for the sake of argument I'll explain the possible cases.
Dedicated threads could be quite a burden. (Interrupt handling and thread scheduling is done by the operating system in case you are are using Oracle [HotSpot] JVM as these tasks are delegated.) The OS/system could become unresponsive if there are too many threads and thus slowing your batch processing (or other tasks). There are a lot of administrative tasks regarding thread management this is why thread (and connection) pooling is a thing. Although a good operating system should be able to handle a few thousand concurrent threads, there is always the chance that some limits or (kernel) event occur.
This is where pooling and async behaviour comes in handy. There is for example a pool of 10 phisical threads doing all the work. If something is blocked (waits for the server response in this case) it gets in the "Blocked" state (see image) and the following task gets the phisical thread to do some work. When a thread is notified (data arrived) it becomes "Runnable" (from which point the pooling mechanism is able to pick it up [this could be the OS or JVM implemented solution]). For further reading on the thread states I recommend W3Rescue. To understand the thread pooling better I recommend this baeldung article.
Second question - is something wrong with the async implementation? If not, what is the right approach to go about here?
The implementation is OK, there is no problem with it. The behaviour is just different from the threaded way. The main question in these cases are mostly what the SLA-s (service level agreements) are. If you are the only "customer of the service, then basically you have to decide between latency or throughput, but the decision will affect only you. Mostly this is not the case, so I would recommend some kind of pooling which is supported by the library you are using.
Third question - However I just noted that the time taken is roughly the same the moment you read the response stream as a string. I wonder why this is?
The message is most likely arrived completely in both cases (probably the response is not a stream just a few http package), but if you are reading the header only that does not need the response itself to be parsed and loaded on the CPU registers, thus reducing the latency of reading the actual data received. I think this is a cool represantation in latencies (source and source):
This came out as a quite long answer so TL.DR.: scaling is a really hardcore topic, it depends on a lot of things:
HTTPS
connections and act as proxiesMost likely in your case the server was the bottleneck as both methods gave the same result in the corrected case (HttpResponse::getStatusLine().getStatusCode() and HttpURLConnection::getResponseCode()
). To give a proper answer you should measure your servers performance with some tools like JMeter or LoadRunner etc and then size your solution accordingly. This article is more on DB connection pooling, but the logic is applicable in here as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With