Производительность: Apache HttpAsyncClient против многопоточного URLConnection
Я пытаюсь выбрать лучший подход для параллельного выполнения большого количества http-запросов. Ниже приведены два подхода, которые у меня есть:
Используя Apache HttpAsyncClient и 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();
Обычный подход потока к запросу:
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"); });
Через несколько прогонов я заметил, что традиционный подход заканчивался почти вдвое быстрее, чем асинхронный / будущий подход.
Хотя я ожидал, что выделенные потоки будут работать быстрее, будет ли эта разница заметной или, возможно, что-то не так с асинхронной реализацией? Если нет, то каков правильный подход?
1 ответ
Вопрос на месте зависит от многих факторов:
- аппаратные средства
- операционная система (и ее конфигурация)
- Реализация JVM
- Сетевые устройства
- Поведение сервера
Первый вопрос - должна ли эта разница быть замечательной?
Зависит от нагрузки, размера пула и сети, но может быть намного больше, чем наблюдаемый коэффициент 2 в каждом из направлений (в пользу асинхронного или многопоточного решения). Согласно вашему последующему комментарию, разница больше из-за проступка, но ради аргумента я объясню возможные случаи.
Выделенные темы могут быть довольно обременительными. (Обработка прерываний и планирование потоков выполняется операционной системой в случае, если вы используете JVM Oracle [HotSpot], поскольку эти задачи делегированы.) Операционная система / система может перестать отвечать, если слишком много потоков и, следовательно, замедляет пакетную обработку (или другие задачи). Существует множество административных задач, связанных с управлением потоками, поэтому пул потоков (и соединений) - это вещь. Хотя хорошая операционная система должна обрабатывать несколько тысяч одновременных потоков, всегда есть вероятность того, что произойдут некоторые ограничения или (ядро) событие.
Вот где пул и асинхронное поведение пригодятся. Например, есть пул из 10 физических потоков, выполняющих всю работу. Если что-то заблокировано (в этом случае ожидает ответа сервера), оно переходит в состояние "Заблокировано" (см. Изображение), и следующая задача заставляет физический поток выполнить некоторую работу. Когда поток уведомляется (данные поступают), он становится "Runnable" (с этого момента механизм пула может его забрать [это может быть решение, реализованное в ОС или JVM]). Для дальнейшего чтения состояний веток я рекомендую W3Rescue. Чтобы лучше понять пул потоков, я рекомендую эту статью о Baeldung.
Второй вопрос - что-то не так с асинхронной реализацией? Если нет, то каков правильный подход?
Реализация в порядке, с этим проблем нет. Поведение просто отличается от многопоточного. Основным вопросом в этих случаях является то, что SLA-ы (соглашения об уровне обслуживания). Если вы являетесь единственным "клиентом" службы, то в основном вам нужно выбирать между задержкой или пропускной способностью, но решение будет влиять только на вас. В основном это не так, поэтому я бы порекомендовал какой-то пул, который поддерживается библиотека, которую вы используете.
Третий вопрос. Однако я только что заметил, что время, которое вы потратили, примерно одинаково, когда вы читаете поток ответов в виде строки. Интересно, почему это?
Скорее всего, сообщение получено полностью в обоих случаях (возможно, ответ - это не поток, а всего лишь несколько http-пакетов), но если вы читаете только заголовок, для которого не требуется анализ самого ответа и его загрузка в регистры ЦП, таким образом уменьшая задержку чтения фактических полученных данных. Я думаю, что это отличная репрезентация в латентности ( источник и источник):
Это получилось довольно длинным ответом, поэтому TL.DR.: масштабирование - действительно хардкорная тема, она зависит от многих вещей:
- аппаратное обеспечение: количество физических ядер, многопоточность, скорость памяти, сетевой интерфейс
- операционная система (и ее конфигурация): управление потоками, обработка прерываний
- Реализация JVM: управление потоками (внутренними или внешними по отношению к ОС), не говоря уже о конфигурациях GC и JIT
- Сетевые устройства: некоторые ограничивают одновременные соединения с данным IP, некоторые пулы не
HTTPS
соединения и действуют как прокси - Поведение сервера: работники из пула или работники по запросу и т. Д.
Скорее всего, в вашем случае узким местом был сервер, так как оба метода дали исправленный случай (HttpResponse::getStatusLine().getStatusCode() and HttpURLConnection::getResponseCode()
). Чтобы дать правильный ответ, вы должны измерить производительность ваших серверов с помощью таких инструментов, как JMeter или LoadRunner и т. Д., А затем соответствующим образом изменить размер вашего решения. Эта статья больше о пуле соединений с БД, но логика применима и здесь.