Как повысить производительность при использовании ExecutorService с возможностью тайм-аута потока?

Я не являюсь экспертом по многопоточности, но вижу некоторые проблемы с производительностью в моем текущем коде, который использует ExecutorService.

Я работаю над проектом, в котором мне нужно сделать HTTP-вызов URL-адреса на мой сервер, и если ответ занимает слишком много времени, то прервите вызов. В настоящее время он возвращает простую строку JSON обратно.

Текущее требование у меня есть для 10 ms. В пределах 10 ms он должен иметь возможность получить данные с сервера. Я предполагаю, что это возможно, поскольку это просто HTTP-вызов на сервер в том же центре обработки данных.

Моя клиентская программа и фактические серверы находятся в одном центре обработки данных, и задержка пинга между ними составляет 0.5 ms, поэтому это наверняка выполнимо.

Я использую RestTemplate для этого, чтобы сделать вызов URL.

Ниже приведен мой код, который я написал для себя, который использует ExecutorService и Callables -

public class URLTest {

    private ExecutorService executor = Executors.newFixedThreadPool(10);

    public String getData() {
        Future<String> future = executor.submit(new Task());
        String response = null;

        try {
            System.out.println("Started..");
            response = future.get(100, TimeUnit.MILLISECONDS);
            System.out.println("Finished!");
        } catch (TimeoutException e) {
            System.out.println("Terminated!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        return response;
    }
}

Ниже мой класс Task, который реализует интерфейс Callable -

class Task implements Callable<String> {

    private RestTemplate restTemplate = new RestTemplate();

    public String call() throws Exception {
        //  TimerTest timer = TimerTest.getInstance();  // line 3
            String response = restTemplate.getForObject(url, String.class);
        //  timer.getDuration();    // line 4

        return response;

    }
}

А ниже мой код в другом классе DemoTest, который вызывает метод getData в классе URLTest 500 times и измеряет его 95-й процентиль от начала до конца -

public class DemoTest { 
   public static void main(String[] args) {

        URLTest bc = new URLTest();

        // little bit warmup
        for (int i = 0; i <= 500; i++) {
            bc.getData();
        }

        for (int i = 0; i <= 500; i++) {
            TimerTest timer = TimerTest.getInstance(); // line 1
            bc.getData();
            timer.getDuration(); // line 2
        }

        // this method prints out the 95th percentile
        logPercentileInfo();

    }
}   

С приведенным выше кодом я всегда вижу 95-й процентиль как 14-15 ms (что плохо для моего варианта использования, поскольку это сквозной поток, и это то, что мне нужно измерить).

удивляюсь почему? ExectuorFramework добавляет сюда всю задержку? Может быть каждая задача отправлена, и поток отправки ожидает (через future.get), пока задача не будет завершена.

Моя главная цель - максимально уменьшить задержку здесь. Мой вариант использования прост: сделать вызов URL-адреса на один из моих серверов с включенной функцией TIMEOUT, что означает, что если серверу требуется много времени для ответа, то тайм-аут весь звонок. Клиент будет вызывать наш код из приложения, которое также может быть многопоточным.

Есть ли что-то, что мне не хватает, или какие-то другие варианты ExecutorService, которые мне нужно использовать? Как я могу улучшить свою работу здесь? Любые предложения будут очень полезны ..

Будем очень признательны за любой пример. Я читал о ExecutorCompletionService, не уверен, следует ли мне использовать это или что-то еще..


person arsenal    schedule 20.01.2014    source источник
comment
Действительно не понимаю, чего вы ожидаете добиться в отношении производительности веб-сервера, возясь с реализацией пула потоков? Посмотрите на веб-сервер и веб-клиент...! Вам нужно установить тайм-аут на веб-клиенте, чтобы он действительно сдался. С тайм-аутом на будущее вы просто отказываетесь от задачи, но рабочий поток все еще сидит там, пытаясь выполнить ее, пока она не будет выполнена.   -  person Affe    schedule 20.01.2014
comment
Это выглядит довольно однопоточным для меня...   -  person pamphlet    schedule 20.01.2014
comment
Здесь виновата не структура исполнителя. Что-то занимает больше времени, чем вы ожидали. Я бы порекомендовал изучить запуск профилировщика Java. Вы также можете регистрировать миллисекунды настенных часов различных частей вашей системы, чтобы увидеть, что занимает так много времени.   -  person Gray    schedule 20.01.2014
comment
Кроме того, даже несмотря на то, что вы истечете время своей работы, она будет продолжать выполняться и потреблять системные ресурсы в фоновом режиме. Убить RPC важно.   -  person Gray    schedule 20.01.2014
comment
@pamphlet: мне нужен синхронный и асинхронный метод для моего варианта использования, один для блокировки вызова.. И другой для отправки в будущее.. В зависимости от этих методов клиент может вызывать любой метод по своему усмотрению.. Вот почему У меня есть это..   -  person arsenal    schedule 20.01.2014
comment
@Gray: здесь я использую system.nanotime для измерения производительности. Одна вещь, которую я нашел здесь очень странной, это то, что если я закомментирую строку 1 и строку 2 в классе DemoTest и раскомментирую строку 3 и строку 4 в классе Task, а затем запущу программа снова начнет давать мне 95-й процентиль как 3 мс. Так что не уверен, почему сквозной поток дает мне 95-й процентиль как 14-15 ms?   -  person arsenal    schedule 20.01.2014
comment
Почему бы не поиздеваться над этими звонками и не посмотреть, что еще может быть причиной этого?   -  person Kitesurfer    schedule 20.01.2014
comment
@Affe: у меня есть служба REST, развернутая на одном из моих серверов, поэтому я запускаю эту службу отдыха с помощью шаблона REST.   -  person arsenal    schedule 20.01.2014


Ответы (2)


Что касается вашего наблюдения, что вы измеряете 15 мс снаружи, но только 3 мс внутри, я уверен, что конструкция RestTemplate принимает разницу. Это можно исправить рефакторингом.

Обратите внимание, что RestTemplate — это тяжеловесный потокобезопасный объект, который предназначен для развертывания как одноэлементный объект для всего приложения. Ваш текущий код находится в критическом нарушении этого намерения.


Если вам нужны асинхронные HTTP-запросы, вам действительно следует использовать асинхронную HTTP-библиотеку, такую ​​​​как AsyncHttpClient, основанную на Netty внизу, которая снова основана на Java NIO. Это означает, что вам не нужно занимать поток для каждого незавершенного HTTP-запроса. AsyncHttpClient также работает с фьючерсами, поэтому у вас будет привычный API. Он также может работать с обратными вызовами, что предпочтительнее для асинхронного подхода.

Однако, даже если вы сохраните свою текущую синхронную библиотеку, вам следует как минимум настроить тайм-аут на клиенте REST, вместо того, чтобы позволить ему работать своим чередом.

person Marko Topolnik    schedule 20.01.2014
comment
Спасибо за ваше предложение. Любые мысли, какие изменения я должен внести? И было бы здорово, если бы вы могли привести пример с AsyncHttpClient. Я просто пытаюсь понять, как это будет соответствовать моему текущему варианту использования. - person arsenal; 20.01.2014
comment
Чтобы проверить эту гипотезу, просто сделайте RestTemplate статическим. - person Marko Topolnik; 20.01.2014
comment
AsyncHttpClient сам по себе не имеет удобства RESTful, но, вероятно, есть библиотеки, которые с этим справляются. С другой стороны, я бы сначала посмотрел на возможности самого RestTemplate в этом отношении. - person Marko Topolnik; 20.01.2014
comment
Имеет ли значение в любом случае, статический или экземпляр.? Методы RestTemplate для выполнения HTTP-запросов являются потокобезопасными, поэтому, если у вас есть экземпляр RestTemplate для каждого экземпляра Task или общий экземпляр для всех экземпляров Task, это не имеет значения (за исключением сборки мусора), я думаю.. поправьте меня, если я ошибаюсь.. - person arsenal; 20.01.2014
comment
RestTemplate — это тяжеловесный объект, поддерживающий полную инфраструктуру ввода-вывода, пулы соединений и т. д. Это определенно должно иметь огромное значение. То есть, если я полностью не ошибаюсь в конструкции RestTemplate, что тоже возможно. - person Marko Topolnik; 20.01.2014
comment
Я попробовал вышеупомянутый подход. Сократите время цикла с 15 мс до примерно 10 мс для моего приложения. - person Vibha; 22.05.2018

затем снова запустите программу, она начнет давать мне 95-й процентиль как 3 мс. Так что не уверен, почему сквозной поток дает мне 95-й процентиль как 14-15 мс

Вы генерируете задачи быстрее, чем успеваете их обработать. Это означает, что чем дольше вы запускаете тесты, тем дальше они отстают, поскольку ставят их в очередь. Я ожидаю, что если вы сделаете эти 2000 запросов, вы увидите задержки в 4 раза больше, чем сейчас. Узкое место может быть на стороне клиента (в этом случае поможет большее количество потоков), но весьма вероятно, что узкое место находится на стороне сервера, и в этом случае большее количество потоков может ухудшить ситуацию.


По умолчанию для HTTP устанавливается новое TCP-соединение для каждого запроса. Время соединения для нового соединения TCP может легко достигать 20 мс, даже если у вас есть две машины рядом. Я предлагаю рассмотреть использование HTTp/1.1 и поддерживать постоянное соединение.

Кстати, вы можете пинговать с одной стороны Лондона на другую за 0,5 мс. Однако надежно получить меньше 1 мс с HTTP сложно, поскольку протокол не предназначен для низкой задержки. Он предназначен для использования в сетях с высокой задержкой.

Примечание: вы не можете увидеть задержки ниже 25 мс, а 100 мс достаточно для большинства веб-запросов. Именно с такими предположениями был разработан HTTP.

person Peter Lawrey    schedule 20.01.2014
comment
Спасибо, Питер за предложение. Есть одна очень странная вещь, которую я хотел бы вам рассказать. Может быть, это может дать вам некоторую подсказку. Одна вещь, которую я нашел здесь очень странной, это если я закомментирую строку 1 и строку 2 в DemoTest. class и раскомментируйте строку 3 и строку 4 в классе Task, а затем снова запустите программу, она начнет давать мне 95-й процентиль как 3 мс. Так что не уверен, почему сквозной поток дает мне 95-й процентиль как 14-15 мс? - person arsenal; 20.01.2014
comment
Для интерпретации у OP есть 3 мс для самого вызова REST, но 15 мс для будущего вызова. - person Marko Topolnik; 20.01.2014
comment
У вас также увеличена задержка запуска потоков. Ваш код не будет скомпилирован, пока он не будет выполнен 10 000 раз, и я обычно игнорирую первые 10 КБ или первые 2 секунды теста. - person Peter Lawrey; 20.01.2014
comment
@PeterLawrey Чтобы ответить на этот вопрос. Я уже запускал его пару раз, прежде чем измерять. В настоящее время я запускаю 500 раз для разогрева, затем начинаю измерять вызов, как показано на моем DemoTest class. Я могу увеличить число прогрева, чтобы увидеть, влияет ли это на производительность или нет. - person arsenal; 20.01.2014
comment
@Webby Мне кажется, вы генерируете запросы быстрее, чем обрабатываете их, поэтому они стоят в очереди. Более длительный запуск должен казаться медленнее, так как очередь становится длиннее. Смотрите мой ответ. - person Peter Lawrey; 20.01.2014
comment
@PeterLawrey Понятно, короче говоря, вы говорите, что я должен поспать пару миллисекунд, прежде чем делать какие-либо вызовы моего метода getData? - person arsenal; 20.01.2014
comment
@Webby, если вы замедляете производителя, например. добавьте Thread.sleep(1) после каждого вызова, вы должны увидеть лучшие цифры. Вы бы не сделали этого в реальности, что является одной из опасностей при попытке сравнить эту систему. Я предлагаю изменить проблему и сказать, быстро ли вы можете отправлять запросы и убедиться, что 99% составляют менее 10 мс. - person Peter Lawrey; 20.01.2014
comment
@Webby и я запускали тест не менее 10 секунд, в идеале час или ночь. - person Peter Lawrey; 20.01.2014
comment
давайте продолжим это обсуждение в чате - person arsenal; 20.01.2014