В предыдущей статье « Пакетные (сворачивающиеся) запросы в 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 отдельных запросов и сворачивает их, снижая нагрузку на нисходящий поток. Так что разрушение очень мощное,просто помните, какой тип оптимизации вы действительно хотите достичь.





