Привет, Хабр. Для будущих студентов курса "Highload Architect" подготовили перевод материала.
Также приглашаем на открытый вебинар по теме «Репликация как паттерн горизонтального масштабирования хранилищ». На занятии участники вместе с экспертом разберут репликацию — одну из техник масштабирования баз данных, обсудят смысл и ее назначение, рассмотрят преимущества и недостатки различных видов репликации.
Микросервисы на Java замечательны тем, что с помощью них можно создавать большие и сложные системы из множества независимых компонент. Вместо одного приложения получается несколько мини-приложений или сервисов. Компоненты могут тестироваться, развертываться и обслуживаться независимо друг от друга. Так что, если убрать один кирпич, то здание не разрушится полностью.
Тем не менее очевидное преимущество микросервисов может стать и причиной возникновения проблем. Если раньше вы уже работали с микросервисами на Java, то знаете, что для получения высокой производительности без потери функциональности могут потребоваться усилия. Но если вы справитесь с этим, то получите потрясающие результаты.
Spring Boot — это быстрый способ создания микросервисов на Java. В этой статье мы рассмотрим, как улучшить производительность Spring Boot-микросервиса.
Что будем использовать
Мы будем использовать два микросервиса:
External-service (внешний сервис): "реальный" микросервис, доступный по HTTP.
Facade-service (фасад): микросервис, который будет читать данные из external-service и отправлять результат клиентам. Будем оптимизировать этот сервис.
Что нам нужно
Java 8
Jmeter 5.3
Java IDE
Gradle 6.6.1
Исходный код
Прежде всего, скачайте исходный код, который мы будем улучшать, отсюда.
External service
Сервис был создан с помощью Spring Initializer. В нем один контроллер, имитирующий нагрузку:
@RestController
public class ExternalController {
@GetMapping(“/external-data/{time}”)
public ExternalData getData(@PathVariable Long time){
try {
Thread.sleep(time);
} catch (InterruptedException e) {
// do nothing
}
return new ExternalData(time);
}
}
Запустите ExternalServiceApplication
. Сервис должен быть доступен по адресу https://localhost:8543/external-data/300 .
Facade service
Этот сервис также был создан с помощью Spring Initializer. В нем два основных класса: ExternalService
и ExternalServiceClient
.
Класс ExternalService
читает данные из сервиса External Service с помощью externalServiceClient
и вычисляет сумму.
@Service
public class ExternalService {
@Autowired
private ExternalServiceClient externalServiceClient;
public ResultData load(List<Long> times) {
Long start = System.currentTimeMillis();
LongSummaryStatistics statistics = times
.parallelStream()
.map(time -> externalServiceClient.load(time).getTime())
.collect(Collectors.summarizingLong(Long::longValue));
Long end = System.currentTimeMillis();
return new ResultData(statistics, (end — start));
}
}
Для чтения данных из external service класс ExternalServiceClient
использует библиотеку openfeign. Реализация HTTP-клиента на основе OKHttp выглядит следующим образом:
@FeignClient(
name = “external-service”,
url = “${external-service.url}”,
configuration = ServiceConfiguration.class)
public interface ExternalServiceClient {
@RequestMapping(
method = RequestMethod.GET,
value = “/external- data/{time}”,
consumes = “application/json”)
Data load(@PathVariable(“time”) Long time);
}
Запустите класс FacadeServiceApplication
и перейдите на http://localhost:8080/data/1,500,920,20000.
Ответ будет следующим:
{
“statistics”: {
“count”: 4,
“sum”: 1621,
“min”: 1,
“max”: 920,
“average”: 405.25
},
“spentTime”: 1183
}
Подготовка к тестированию производительности
Запустите Jmeter 5.3.1 и откройте файл perfomance-testing.jmx в корне проекта.
Конфигурация теста:
Нагрузочный тест будем проводить по следующему URL-адресу: http://localhost:8080/data/1,500,920,200
Перейдите в Jmeter и запустите тест.
Первый запуск Jmeter
Сервер стал недоступен. Это связано с тем, что в ExternalService
мы использовали parallelStream()
. Stream API для параллельной обработки данных использует ForkJoinPool
. А по умолчанию параллелизм ForkJoinPool
рассчитывается на основе количества доступных процессоров. В моем случае их три. Для операций ввода-вывода это узкое место. Итак, давайте увеличим параллелизм ForkJoinPool
до 1000.
-Djava.util.concurrent.ForkJoinPool.common.parallelism=1000
И запустим Jmeter еще раз.
Второй запуск Jmeter
Как вы видите, пропускная способность (throughput) увеличилась с 6 до 26 запросов в секунду. Это хороший результат. Кроме того, сервис работает стабильно без ошибок. Но тем не менее среднее время (average time) составляет 9 секунд. У меня есть предположение, что это связано с затратами на создание HTTP-соединение. Давайте добавим пул соединений:
@Configuration
public class ServiceConfiguration {
…
@Bean
public OkHttpClient client()
throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, NoSuchProviderException {
…
okhttp3.OkHttpClient client = new okhttp3.OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(), trustManager)
.hostnameVerifier((s, sslSession) -> true)
.connectionPool(new ConnectionPool(2000, 10, TimeUnit.SECONDS))
.build();
OkHttpClient okHttpClient = new OkHttpClient(client);
return okHttpClient;
}
Таким образом, приложение может поддерживать до 2000 HTTP-соединений в пуле в течение 10 секунд.
Третий запуск Jmeter
Пропускная способность улучшилась почти в три раза: с 26 до 71 запросов в секунду.
В целом пропускная способность улучшилась в 10 раз: с 6 до 71 запросов / сек, но мы видим, что максимальное время запроса (maximum time) составляет 7 секунд. Это много и влияет как на общую производительность, так и на задержку в UI.
Поэтому давайте ограничим количество обрабатываемых запросов. Сделать это можно, используя указанные ниже свойства Tomcat в application.properties
:
server.tomcat.accept-count=80
server.tomcat.max-connections=80
server.tomcat.max-threads=160
Приложение будет отклонять запросы на подключение и отвечать ошибкой "Connection refused" (отказ соединения) всем клиентам, как только количество подключений достигнет 160.
Четвертый запуск Jmeter
Теперь максимальное время составляет меньше пяти секунд и число запросов увеличилось с 71 до 94 запросов в секунду. Процент ошибок ожидаемо увеличился до 29%. Это все ошибки "Connection refused".
Заключение
В этой статье мы продемонстрировали реальный сценарий повышения производительности в 15 раз с 6 до 94 запросов / сек без каких-либо сложных изменений кода. Кроме того, упомянутые выше шаги позволяют снизить стоимость инфраструктуры, такой как AWS. Возможно, для вашего следующего проекта вам стоит подумать об использовании микросервисов. Хотя одна из тенденций последних лет — переход к бессерверной архитектуре, но вы должны всё взвесить при переходе к такой архитектуре.
Мы рассмотрели общий подход к улучшению производительности Java-приложений, который вы можете использовать на практике. Однако в статье не рассматриваются некоторые специфичные случаи, такие как работа с базами данных. В мире Java-микросервисов есть еще много места для открытий и экспериментов.
Узнать подробнее о курсе "Highload Architect".
Смотреть открытый вебинар по теме «Репликация как паттерн горизонтального масштабирования хранилищ».