Хабрахабр

Java это не только кровавый энтерпрайз, но и быстрые latency-sensitive приложения

Я занимаюсь алгоритмической торговлей в Райффайзенбанке. Это довольно специфичная область банковской сферы. Мы делаем торговую платформу, работающую с низкими и предсказуемыми задержками. Успех приложения зависит, в том числе, и от скорости работы приложения, поэтому нам приходится заниматься всем стеком, задействованным в торговле: приватными сетевыми каналами, специальным аппаратным обеспечением, настройками ОС и специальной JVM, и, конечно же, самим приложением. Мы не можем остановиться на оптимизации исключительно самого приложения — настройки ОС или сети имеют не меньшее значение. Это требует технической экспертизы и эрудиции, чтобы понять, как через весь стек проходят данные, и где может быть задержка.

Не каждая организация/банк может позволить себе разработку подобного класса софта. Но мне повезло, что такой проект был запущен в стенах Райффайзенбанка, а у меня была подходящая специализация — я специализировался на производительности кода в Московской компиляторной лаборатории Intel. Мы делали компиляторы для С, С++ и Fortran. В Райффайзенбанке я перешел на Java. Если раньше я делал какой-то инструмент, которым потом пользовалось много людей, то сейчас я переместился на другую сторону баррикад и занимаюсь прикладным анализом производительности не только кода, но и всего стека приложения. Регулярно путь исследования перформансной проблемы лежит далеко за рамками кода, например, в ядре или настройках сети.

Java не для highload’а?

Распространено мнение, что Java не очень подходит для разработки высоконагруженных систем.
С этим можно согласиться лишь отчасти. Java прекрасна во многих своих аспектах. Если сравнивать его с языком вроде С++, то потенциально накладные расходы у него могут быть выше, но иногда функционально аналогичные решения на С++ могут работать медленнее. Есть оптимизации, которые автоматически работают в Java, но не работают в С++, и наоборот. Глядя на качество кода, который получается после JIT-компилятора Java, мне хочется верить, что производительность будет уступать той, что я мог бы достичь в пике, не более, чем в несколько раз. Но при этом я получаю очень быструю разработку, отличный инструментарий и богатый выбор готовых компонентов.

Если разработчик использует любую из этих сред, то скорость отладки, нахождение багов и написание сложной логики на порядок выше. Давайте будем смотреть правде в лицо: С++ мире среды разработки (IDE) существенно отстают от IntellyJ и Eclipse. Самое занятное, что при написании конкурентного кода, подходы к синхронизации и в Java и C++ очень похожи: это либо примитивы уровня ОС (например, synchronized/std::mutex) или примитивы железа (Atomic*/std::atomic<*>). В итоге получается, что проще подпилить в нужных местах Java, чтобы она работала достаточно быстро, чем делать всё с нуля и очень долго на С++. И очень важно видеть это сходство.

И вообще, мы разрабатываем не highload приложение))

В чем же разница между highload и low latency приложением?

Термин highload не до конца отражает специфику нашей работы — мы занимаемся именно latency-sentitive системами. В чем же разница? Для высоконагруженных систем важно работать в среднем достаточно быстро, полностью используя аппаратные ресурсы. На практике это означает, что каждый сотый/тысячный/../миллионный запрос к системе потенциально может работать очень медленно, ведь мы ориентируемся на средние значения и не всегда учитываем, что наши пользователи существенно страдают от тормозов.

Наша задача — сделать так, чтобы система всегда имела предсказуемый отклик. Мы же занимаемся системами, для которых уровень задержки критически важен. И это не делает их разработку проще. Потенциально latency-sensitive системы могут быть не высоконагруженными, в случае, если события происходят достаточно редко, но требуется гарантированное время отклика. Опасности подстерегают прямо повсюду. Даже наоборот! для throughput. Подавляющее большинство компонентов современного железа и софта ориентированы на хорошую работу «в среднем», т.е.

Мы используем хэш-таблицы, и если происходит ре-хэш всей структуры данных на критическом пути – это может привести к ощутимым тормозам для конкретного пользователя на единичном запросе. Взять хотя бы структуры данных. А ведь быстродействие именно этого редкого случая может быть очень важно для нас! Или JIT компилятор – оптимизирует наиболее часто выполняющийся паттерн кода, пессимизируя редко исполняющийся паттерн кода.

Или какую-то необычную рыночную ситуацию, на которую нужна быстрая реакция? Может этот код обрабатывает редкий вид ордеров? Мы стараемся сделать так, чтобы реакция нашей системы на эти потенциально редкие события занимала какое-то предсказуемое и, желательно, очень маленькое время.

Как добиться предсказуемой длительности реакции?

На этот вопрос не получится ответить в двух фразах. В первом приближении важно понимать, есть ли какая-то синхронизация — synchronized, reentrantlock или что-то из java.util.concurrent. Зачастую приходится использовать синхронизацию на busy-spin'ах. Использование любого примитива синхронизации – это всегда компромисс. И важно понимать, как работают эти примитивы синхронизации и какие компромисы они несут. Также важно оценить сколько мусора генерирует тот или иной участок кода. Лучшее средство борьбы с garbage collector — не тригерить его. Чем меньше мы будем генерировать мусора, тем реже мы будем запускать сборщик мусора, и тем дольше проработает система без его вмешательства.

Нам приходится очень пристально анализировать, насколько медленно работает система каждый сотый, каждый тысячный раз. Также мы пользуемся широким спектром разных инструментов, которые позволяют нам анализировать не только усредненные показатели. Но нам очень важно знать, насколько. Очевидно, что эти показатели будут хуже, чем медиана или среднее. И показать это помогают такие инструменты как, к примеру, Grafana, Prometheus, HDR-гистограммы и JMH.

Можно ли удалять Unsafe?

Зачастую приходится использовать и то, что апологеты называют недокументированным API. Я говорю про знаменитый Unsafe. Я считаю, что unsafe де-факто стал частью публичного API Java-машин. Нет смысла это отрицать. Unsafe используют очень многие проекты, которые мы все активно применяем. И если мы от него откажемся, то что будет с этими проектами? Либо они останутся жить на старой версии Java, либо придется опять потратить очень много сил, чтобы все это переписать. Готово ли комьюнити на это? Готово ли оно потенциально потерять десятки процентов производительности? И главное, в обмен на что?

Думаю, пока хотя бы половина из всех пользующихся Unsafe проектов не переедут на что-то другое, Unsafe будет доступен в том или ином виде. Косвенно мое мнение подтверждает очень аккуратное удаление методов из Unsafe — в Java11 были удалены самые бесполезные методы из Unsafe.

Существует мнение: Банк + Java = кровавый закостенелый энтерпрайз?

В нашей команде таких ужасов нет. На Spring'е у нас написано, наверное, строчек десять, причем мною)) Мы стараемся не использовать большие технологии. Предпочитаем делать маленькое, аккуратное и быстрое, чтобы мы могли это осознать, контролировать и при необходимости модифицировать. Последнее очень важно для систем (вроде нашей), к которым предъявляются нестандартные требования, которые могут отличаться от требования 90% пользователей фреймворка. И в случае использования большого фреймворка, мы не сможем ни донести свои потребности комьюнити ни самостоятельно поправить поведение.

Я пришел в мире Java из C++ и мне очень сильно бросается в глаза разделение сообщества на тех, кто разрабатывает runtime виртуальной машины/компилятор или саму виртуальную машину и на прикладных разработчиков. На мой взгляд, у разработчиков всегда должна быть возможность использовать все доступные инструменты. Зачастую авторы JDK пользуются другим API. Это прекрасно видно по коду стандартных классов JDK. Вообще, я считаю, что использование одинакового API для написания и стандартной библиотеки и прикладного кода – отличный показатель зрелости платформы. Потенциально это означает, что мы не можем добиться пиковой производительности.

Еще кое-что

Думаю, всем разработчикам очень важно знать, как работает, если не весь стек, то хотя бы его половина: Java-код, байт-код, внутренности среды исполнения виртуальной машины и ассемблер, железо, ОС, сеть. Это позволяет шире посмотреть на проблемы.
Также стоит упомянуть производительность. Очень важно не замыкаться на средних показателях и всегда смотреть на показатели медианы и высоких перцентилей (худший из 10/100/1000/… замеров).

Встреча с Сергеем Мельниковым, это я как раз)) Зарегистрироваться можно по ссылке. Обо всем этом буду рассказывать на встрече Java User Group 30 мая в Санкт-Петербурге.

О чем будем говорить?

  1. Про профилирование и использование стандартного профилировщика Linux и perf: как с ними жить, что они измеряют и как трактовать их результаты. Это будет введение в общее профилирование, с советами, лайфхаками, как выжать из профилировщиков все возможное, чтобы они профилировали с максимальной точностью и частотой.
  2. Про особенности оборудования для получения ещё более подробного профиля и просмотра профиля редких событий. Например, когда ваш код каждый сотый раз работает в 10 раз медленнее. Об этом ни один профилировщик не расскажет. Мы с вами напишем свой небольшой профилировщик, используя штатный механизм ядра Linux и попробуем посмотреть профиль какого-нибудь редкого события.

Приходите на встречу, это будет отличный вечер, будет много интересных рассказов про нашу платформу и про наш любимый язык.

До встречи 😉

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть