Как стать автором
Обновить

Юнга, стоп, у нас кончились ресурсы. Или как мы оптимизировали наши микросервисы

Время на прочтение6 мин
Количество просмотров8.3K

Всем привет. Как вы все знаете, после определенных событий у нас случился ресурсный кризис. И появился запрос на оптимизацию потребляемых ресурсов.

Темой и станет оптимизация потребления ресурсов микросервисов и уменьшение времени выполнения наших запросов.

Перед тем как начать работу, нужно подвести цели:

  • Уменьшение потребления ЦПУ и ОЗУ

  • Уменьшение времени на обработку запросов в секунду

  • Уменьшение время инициализации (запуска)

  • Унификация кодовой базы для наших микросервисов

Цели выявили и перед тем, как понять, что мы их достигли, нужно сделать замеры. Прикладываем линейку к нашим сервисам.

Возьмем 5 микросервисов, которые мы оптимизировали, и посмотрим на результаты до и после:

Слева сверху видим среднее потребление ресурсов в бою (До/После). Остальные графики реализованы с помощью синтетических тестов для большей наглядности (Нагрузку давали по одному из самых популярных методов на микросервис). От 1 до 5 - это условное обозначение наших микросервисов.

Как видим, выделено ресурсов было намного больше среднего потребления ЦПУ. Было это из-за DDoS-атак, которым мы переодически подвергаемся.

  • По графикам до и после можно увидеть, что время обработки запросов уменьшилось во много раз.

  • Снизилось потребление Heap - спасибо JDK17.

  • Уменьшилось количество потоков, так как запросы стали выполнятся быстрее.

  • Немного понизилось потребление ОЗУ.

  • Значительно уменьшилось время инициализации микросервисов.

  • Ну и самая главная преследуемая цель - снизилось потребление ЦПУ.

  • Также уменьшился вес собираемого пакета примерно в 2 раза.

В итоге на на данный момент мы сэкономили около 80% потребляемого ЦПУ и и около 7% ресурсов ОЗУ. Раз мы тут разговариваем об оптимизации перед тем как перейти к сладкому, о том как мы решали данную проблему. Как мы выявляли утечки в наших микросервисах?

Нам понадобится Intellij Idea Ultimate (а именно, его Profiler), нагрузочное тестирование (в моем случае - wrc/wrc2), и для более чистого эксперимента лучше помещать наше приложение в контейнер докера c необходимым ограничением ресурсов.

Запускаем наш микросервис и подключаемся через профайлер Intellij Idea:

После подключения к микросервису начнется запись JFR, по которой в будущем можно будет построить различные графики для анализа.

Также можно посмотреть текущее потребление ЦПУ, ОЗУ и прочее через "CPU and Memory Live Charts".

Делаем нагрузочное тестирование с включенным JFR и анализируем, что у нас дольше всего выполняется.

Пример нагрузочного тестирования через wrk2:

wrk2 -c 20 -d 10m -R 400 -H "Authorization: Basic ***" "http://localhost:8082/service/test?test1=test"

Подробнее как настраивать и анализировать JFR через profiler Intellij Idea.

Можно много рассказывать о том, какие данные подозрительны или нет, но, если вкратце, то стоит смотреть, что потребляет больше всего и что с этим можно сделать. Справа FlameGraph построенный по JFR, слева google по которому ищешь, как срезать косты потребления по логике, которая долго по твоему мнению отрабатывает.

Давайте уже перейдем к решениям, которые помогли нам сэкономить данные ресурсы. Касается они, конечно, Java и Spring.

CPU
CPU

Используем правильный модуль авторизации. В нашем случае это был BCryptPasswordEncoder, который задавался для SpringSecurity. То есть каждый раз использовалась очень надёжная, но довольно прожорливая авторизация, так как декрипт происходит с помощью процессора, который делает множество итераций для того, чтобы надежно декодировать присланный пароль. Данный тип реализации авторизации было бы нормально использовать для gui авторизации, но на каждую обработку запроса использовать данный способ плохо. На данный момент решил использовать кеширование, которое решает данную проблему. Это сняло часть нагрузки с наших микросервисов. В идеале стоит использовать отдельный микросервис, который отвечал бы за авторизацию, например, с использованием OAuth 2.0. Но это только в планах.

Пример ошибки которая была допущена:

    //Бин в SpringSecurity
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); //Наш инкодер
    }

Все авторизации мы пропускали через данный Encoder, включая все REST запросы.

CPU/ОЗУ/StartUp и т.д.
CPU/ОЗУ/StartUp и т.д.

Для маленьких микросервисов – маленькие фреймворки. Не забываем, что есть большое количество фреймворков, которые могут делать излишние абстракции, маппинг и т.д. Например Hibernate, lucene. В нашем случае переход на простой Spring JDBC помог ускорить выполнение методов.

CPU
CPU

Стараемся не использовать RegExp(split, replaceAll), тем более, в больших текстовых наборах, если время выполнения нам важно. Java не очень любит быстро работать с регулярками. Переделываем на нативные методы или высокопроизводительные встроенные методы jdk, такие как indexOf.

SpeedUp
SpeedUp

Не используем Java StreamAPI на маленькие коллекции. Это увеличивает время обработки запросов, так как тратит время на преобразования.

Пример TestEnum:

public enum TestEnum {
        A("1"),
        B("2"),
        C("3"),
        D("4"),
        E("5"),
        F("6"),
        G("7");
        private String number;

        private TestEnum(String number) {
            this.number = number;
        }

        public String getNumber() {
            return number;
        }
}

Пример вызова с помощью StreamApi:

staticVarTestEnum = Arrays.stream(TestEnum.values()) // Время на преоброзование в StreamAPI - опционально
  .filter(value -> value.number.equals("4"))
  .findFirst()
  .get();
Пример использования StreamApi - FlameGraph
Пример использования StreamApi - FlameGraph

Если даже не учитывать время на преобразования из Array в Stream, то внутренние операции в StreamApi на небольших массивах все равно используют слишком много времени выполнение по сравнению без StreamApi.

Пример без StreamApi:

for (TestEnum value : TestEnum.values()) {    
  if (value.getNumber().equals("4")) {       
    staticVarTestEnum = value;    
  }
}
Тот-же код но с использованием без StreamApi
Тот-же код но с использованием без StreamApi
CPU/ОЗУ/StartUp и т.д.
CPU/ОЗУ/StartUp и т.д.

Переезд на JDK 17. Принёс хорошую оптимизацию и новый GC. Не буду пересказывать уже написанные статьи, но, что важно отметить, так это выросшую в 1.5 раза скорость запуска при простом переходе с JDK 11 до JDK 17.

SpeedUp
SpeedUp

Долгое выполнение запросов на стороне БД? Не забываем проиндексировать нужные поля. Но правильно. Убрав неиспользуемые индексы с базы данных и добавив действительно необходимые, увеличили скорость выборки из БД.

SpeedUp
SpeedUp

Если мы используем Spring, индексируем его компоненты с помощью spring-indexes, что даст прирост скорости запуска. Прирост зависит от количества компонентов в проекте.

ОЗУ and StartUp
ОЗУ and StartUp

Для увеличения скорости запуска проекта и уменьшения потребление heap-memory используем Lazy инициализацию для неиспользуемых или редко используемых модулей.

StartUp
StartUp

Используем строго прописанный property файл для приложения. Так как Spring ищет сотни вариантов написания нашего property. Применимо, если нам важен каждый процент CPU.

@EnableConfigurationProperties(ApplicationProperties::class) //kotlin example
SpeedUp and StartUp
SpeedUp and StartUp

Создаем профайл для конфигураций, тем самым убирая из автоконфигураций ненужные классы. Это ускорит выполнение тестов и ускорит запуск нашего приложения.

@SpringBootApplication(exclude = [DataSourceAutoConfiguration::class]) 
//Выпиливаем все не нужные.
//Проверить какие пытаются сконфигурироваться можно в debug режиме.
StartUp и SpeedUp
StartUp и SpeedUp

Не используем тяжелые layout для logger (например, LogStash). Они создают много абстракций, и в результате тратится много времени на запуск и лишнее время на логирование ваших сообщений.

Пример для logback:

 		<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 				//Указываем свой Encoder
        <encoder class="ru.tinkoff.logger.perfomance.TinkoffLogbackEncoder"/>
    </appender>

		//В данном примере используем ассинхронный лог
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="STDOUT"/>
    </appender>
    
    //Указываем уровень логирования
    <root level="INFO">
        <appender-ref ref="ASYNC"/>
    </root>

Пример реализованного простого encoder

public class LogbackEncoder extends EncoderBase<ILoggingEvent> {
    private static final Logger LOGGER = LoggerFactory.getLogger(LogbackEncoder.class);
    private final ObjectMapper mapper;

    public byte[] encode(ILoggingEvent event) {
        this.start();

        try {
            ObjectNode eventNode = this.mapper.createObjectNode();
                 this.getContext().getCopyOfPropertyMap().forEach((key, value) -> {
                eventNode.put(StringUtils.uncapitalize(key), value);
            }); //создаем наш конектест
          
            event.getMDCPropertyMap().forEach((key, value) -> {
                eventNode.put(StringUtils.uncapitalize(key), value);
            }); //копируем данные в наш контекст из МДС
            return (this.mapper.writeValueAsString(eventNode) + System.lineSeparator()).getBytes(StandardCharsets.UTF_8); 
     				//возврашаем наш контектс в виде байтов
        } catch (Exception ex) {
            LOGGER.error(ex.getMessage(), ex);
        } finally {
            this.stop();
        }

        return new byte[0];
    }
}
CPU и SpeedUp
CPU и SpeedUp

Не логируем тела запросов (JSON/XML и т.д.). Encoder конвертирует из текста в объект и обратно для валидации и вывода вашего сообщения. Это увеличивает потребление ресурсов и время обработки запросов.

В данном случае выход один – отказаться от прямого логирования request и response и логировать данные в своем формате без валидаций и конвертаций или/и логировать только ошибочные ответы.

С помощью данных подходов получилось решить поставленные задачи.

Спасибо за внимание!

Теги:
Хабы:
Всего голосов 17: ↑16 и ↓1+18
Комментарии26

Публикации

Истории

Работа

Java разработчик
461 вакансия

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
24 – 25 октября
One Day Offer для AQA Engineer и Developers
Онлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
26 октября
ProIT Network Fest
Санкт-Петербург
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань