В предыдущей статье « Пакетные (сворачивающиеся) запросы в Hystrix » мы рассматривали сворачивающийся API в Hystrix. Проверьте это, прежде чем приступить к этой статье. Представленный пример был довольно искусственным, просто представляющим API. Сегодня давайте рассмотрим пример из полу-реальной жизни и проведем сравнительный анализ. Мы уже использовали API random.org некоторое время назад в качестве примера (см. Ваше первое сообщение — обнаружение Akka ), давайте использовать его снова. Представьте, что наше приложение вызывает следующий API-фасад, чтобы получить ровно одно случайное число за запрос ( generateIntegers(1)
)
public interface RandomOrgClient { RandomIntegers generateIntegers(int howMany); }
Как вы можете видеть, этот метод может легко получить более одного числа. Вы можете спросить, почему он возвращает какой-то причудливый RandomIntegers
класс, а не, скажем List<Integer>
? Ну, список целых чисел — это просто структура данных, он не представляет никакой бизнес-концепции и *random integers*
не оставляет места для спекуляций. Тем не менее неудивительно, что реализация — это просто оболочка / декоратор над списком:
public class RandomIntegers extends AbstractList<Integer> { private final ImmutableList<Integer> values; public RandomIntegers(Collection<Integer> values) { this.values = ImmutableList.copyOf(values); } @Override public Integer get(int index) { return values.get(index); } @Override public int size() { return values.size(); } public RandomIntegers take(int n) { return new RandomIntegers(values.subList(0, n)); } public RandomIntegers drop(int n) { return new RandomIntegers(values.subList(n, values.size())); } }
Этот неизменный объект значения имеет два дополнительных метода take(n)
и drop(n)
разделить его. Мы будем использовать их в будущем. Чтобы избежать неожиданной задержки и управлять ошибками, мы заключаем клиента в команду Hystrix :
public class GenerateIntegersCmd extends HystrixCommand<RandomIntegers> { private final RandomOrgClient randomOrgClient; private final int howMany; public GenerateIntegersCmd(RandomOrgClient randomOrgClient, int howMany) { super(Setter .withGroupKey(asKey("random.org")) .andCommandKey(HystrixCommandKey.Factory.asKey("generateIntegers")) .andThreadPoolPropertiesDefaults( HystrixThreadPoolProperties.Setter() .withCoreSize(100) .withMaxQueueSize(100) .withQueueSizeRejectionThreshold(100)) .andCommandPropertiesDefaults( HystrixCommandProperties.Setter() .withExecutionIsolationThreadTimeoutInMilliseconds(2000)) ); this.randomOrgClient = randomOrgClient; this.howMany = howMany; } @Override protected RandomIntegers run() throws Exception { return randomOrgClient.generateIntegers(howMany); } }
Мы используем чрезвычайно большой пул потоков (100), Netflix утверждает, что в их случаях достаточно 10-20 , позже мы увидим, как это исправить. Теперь представьте, что у нас есть простая конечная точка, которая должна вызывать этот API для каждого запроса:
@RestController public class RandomController { private final RandomOrgClient randomOrgClient; @Inject public RandomController(RandomOrgClient randomOrgClient) { this.randomOrgClient = randomOrgClient; } @RequestMapping("/{howMany}") public String random(@PathVariable("howMany") int howMany) { final HystrixExecutable<RandomIntegers> generateIntsCmd = new GenerateIntegersCmd(randomOrgClient, howMany); final RandomIntegers response = generateIntsCmd.execute(); return response.toString(); } }
Теперь представьте, random.org
что в среднем задержка составляет 500 мс (полностью составленное число, см. Здесь для реальных данных). При нагрузочном тестировании нашего простого приложения с 100 клиентами мы можем ожидать около 200 транзакций в секунду со средним временем отклика, равным половине секунды:
Свертывание (пакетирование) — это сбор одинаковых маленьких запросов в один больший. Это снижает нагрузку на нижестоящие зависимости и сетевой трафик. Однако эта функция имеет цену, заключающуюся в увеличении времени транзакции, потому что перед random.org
непосредственным вызовом мы должны немного подождать, на случай, если другой клиент захочет позвонить random.org
одновременно. Первым шагом для пакетных запросов является реализация HystrixCollapser
:
public class GenerateIntegersCollapser extends HystrixCollapser<RandomIntegers, RandomIntegers, Integer> { private final int howMany; private final RandomOrgClient randomOrgClient; public GenerateIntegersCollapser(RandomOrgClient randomOrgClient, int howMany) { super(withCollapserKey(HystrixCollapserKey.Factory.asKey("generateIntegers")) .andCollapserPropertiesDefaults(Setter().withTimerDelayInMilliseconds(100)) .andScope(Scope.GLOBAL)); this.howMany = howMany; this.randomOrgClient = randomOrgClient; } @Override public Integer getRequestArgument() { return howMany; } @Override protected HystrixCommand<RandomIntegers> createCommand(Collection<CollapsedRequest<RandomIntegers, Integer>> collapsedRequests) { final int totalHowMany = collapsedRequests .stream() .mapToInt(CollapsedRequest::getArgument) .sum(); return new GenerateIntegersCmd(randomOrgClient, totalHowMany); } @Override protected void mapResponseToRequests(RandomIntegers batchResponse, Collection<CollapsedRequest<RandomIntegers, Integer>> collapsedRequests) { RandomIntegers ints = batchResponse; for (CollapsedRequest<RandomIntegers, Integer> curRequest : collapsedRequests) { final int count = curRequest.getArgument(); curRequest.setResponse(ints.take(count)); ints = ints.drop(count); } } }
GenerateIntegersCollapser
используется дважды в Hystrix — сначала, когда несколько запросов поступают примерно в одно и то же время. После настроенного окна (в нашем примере 100 мс) все запросы собраны вместе, и Hystrix просит нас создать одну пакетную команду (см . createCommand()
:). Все, что мы делаем, это вычисляем, сколько всего нам нужно случайных целых чисел, и запрашиваем их все за один раз. Второй раз GenerateIntegersCollapser
используется, когда приходит пакетный ответ, и нам нужно разделить его на отдельные небольшие запросы. Это ответственность mapResponseToRequests()
. Видишь, как мы нарезаем batchResponse
на мелкие кусочки? Сначала я использовал умопомрачительную реализацию, reduce
чтобы избежать изменчивости:
collapsedRequests.stream().reduce(batchResponse, (leftInBatch, curRequest) -> { final int count = curRequest.getArgument(); curRequest.setResponse(leftInBatch.take(count)); return leftInBatch.drop(count); }, (x, y) -> {throw new UnsupportedOperationException("combiner not needed");});
Так что не так много изменений с точки зрения клиентского кода. Теперь давайте немного отметим:
Ой, это довольно разочаровывает. Среднее время отклика выросло с 500 до 600 миллисекунд. В то же время пропускная способность измеряется в транзакциях в секунду ( TPS, генерируемый 100 одновременными потоками) снизился до примерно 160-170 / с со значительной дисперсией. Эти цифры не должны вызывать удивления — для включения пакетной обработки требуется почти каждый запрос, чтобы немного подождать, на случай, если в ближайшем будущем появится другой запрос. Менее стабильные измерения также понятны — задержка каждого отдельного запроса зависит от того, открылось ли свертывающееся окно или собирается закрыться. Хорошо, так в чем же дело, почему мы беспокоимся о разрушении? Вспомните предыдущий пример — с 200 TPS мы сделали 200 сетевых вызовов и запросов к внешнему сервису. 100 одновременных клиентов означали 100 одновременных запросов к службе поддержки. Самый большой выигрыш в пакетировании / свертывании — это снижение нагрузки на выходе, генерируемой нашим кодом. Это означает, что мы меньше загружаем наши зависимости и сглаживаем пики.Просто сравните количество запросов к random.org
API с и без пакетирования:
200 запросов (каждый запрашивающий одно случайное число) делятся на 5-7 запросов в секунду, однако гораздо большего размера — в среднем запрашивается около 30 случайных чисел, а не … одно. Это означает, что в течение 100 миллисекунд мы собрали около 30 запросов и свели их вместе. И это, мои дорогие друзья, большое улучшение. Мы немного пожертвовали своей производительностью, чтобы уменьшить генерируемый трафик на порядок. Конечно, пакетное окно в 100 миллисекунд является довольно экстремальным, но даже 10 миллисекунд (едва заметные в нормальных условиях) значительно снижают генерируемую нагрузку примерно до 20 запросов в секунду (а не 200). Этот эксперимент показывает, что даже 10 миллисекундное окно захватывает (в среднем) 9 отдельных запросов и сворачивает их, снижая нагрузку на нисходящий поток. Так что разрушение очень мощное,просто помните, какой тип оптимизации вы действительно хотите достичь.