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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

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

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

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

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

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

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

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

Возьмем 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/GPU/StartUp и т.д.
CPU/GPU/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/GPU/StartUp и т.д.
CPU/GPU/StartUp и т.д.

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

SpeedUp
SpeedUp

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

SpeedUp
SpeedUp

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

GPU and StartUp
GPU 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 и логировать данные в своем формате без валидаций и конвертаций или/и логировать только ошибочные ответы.

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

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

Источник: https://habr.com/ru/post/681484/


Интересные статьи

Интересные статьи

Удивительно, сколько полезностей можно узнать за один хабрамитап Хабр ПРО. Например, какая судьба ждёт монолит при переходе на микросервисы и кто отвечает за общий код между двумя микросервисами. Эти...
Слово «микросервисы» на слуху последние несколько лет. Технология активно развивается, на онлайн-конференциях о ней говорят, да и сами мы пишем их каждый день. Когда-то н...
Я работаю специалистом по обработке и анализу данных (data scientist), поэтому большая часть моей работы включает в себя подбор оптимизируемых метрик и размышления о том, как выполнять ...
Два с половиной года назад мы запустили проект Otus.ru и я написал вот эту статью. Сказать, что я ошибся – это совсем ничего не сказать. Сегодня я хотел бы подвести промежуточный итог и немного р...
Самый первый эпик фейл по масштабу — это когда мы ночью перед конференцией внезапно обнаружили на баннере 3 на 10 метров опечатку. Такую, которую не заклеишь. А висел этот баннер над стойкой реги...