Производительность: Apache HttpAsyncClient против многопоточного URLConnection

Я пытаюсь выбрать лучший подход для параллельного выполнения большого количества http-запросов. Ниже приведены два подхода, которые у меня есть:

  1. Используя 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();
    
  2. Обычный подход потока к запросу:

    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 и т. Д., А затем соответствующим образом изменить размер вашего решения. Эта статья больше о пуле соединений с БД, но логика применима и здесь.

Другие вопросы по тегам