Java это не только кровавый энтерпрайз, но и быстрые latency-sensitive приложения
Я занимаюсь алгоритмической торговлей в Райффайзенбанке. Это довольно специфичная область банковской сферы. Мы делаем торговую платформу, работающую с низкими и предсказуемыми задержками. Успех приложения зависит, в том числе, и от скорости работы приложения, поэтому нам приходится заниматься всем стеком, задействованным в торговле: приватными сетевыми каналами, специальным аппаратным обеспечением, настройками ОС и специальной JVM, и, конечно же, самим приложением. Мы не можем остановиться на оптимизации исключительно самого приложения — настройки ОС или сети имеют не меньшее значение. Это требует технической экспертизы и эрудиции, чтобы понять, как через весь стек проходят данные, и где может быть задержка.
Не каждая организация/банк может позволить себе разработку подобного класса софта. Но мне повезло, что такой проект был запущен в стенах Райффайзенбанка, а у меня была подходящая специализация — я специализировался на производительности кода в Московской компиляторной лаборатории Intel. Мы делали компиляторы для С, С++ и Fortran. В Райффайзенбанке я перешел на Java. Если раньше я делал какой-то инструмент, которым потом пользовалось много людей, то сейчас я переместился на другую сторону баррикад и занимаюсь прикладным анализом производительности не только кода, но и всего стека приложения. Регулярно путь исследования перформансной проблемы лежит далеко за рамками кода, например, в ядре или настройках сети.
Java не для highload’а?
Распространено мнение, что Java не очень подходит для разработки высоконагруженных систем.
С этим можно согласиться лишь отчасти. Java прекрасна во многих своих аспектах. Если сравнивать его с языком вроде С++, то потенциально накладные расходы у него могут быть выше, но иногда функционально аналогичные решения на С++ могут работать медленнее. Есть оптимизации, которые автоматически работают в Java, но не работают в С++, и наоборот. Глядя на качество кода, который получается после JIT-компилятора Java, мне хочется верить, что производительность будет уступать той, что я мог бы достичь в пике, не более, чем в несколько раз. Но при этом я получаю очень быструю разработку, отличный инструментарий и богатый выбор готовых компонентов.
Давайте будем смотреть правде в лицо: в мире С++ среды разработки (IDE) существенно отстают от IntelliJ и Eclipse. Если разработчик использует любую из этих сред, то скорость отладки, нахождение багов и написание сложной логики на порядок выше. В итоге получается, что проще подпилить в нужных местах Java, чтобы она работала достаточно быстро, чем делать всё с нуля и очень долго на С++. Самое занятное, что при написании конкурентного кода, подходы к синхронизации и в Java и C++ очень похожи: это либо примитивы уровня ОС (например, synchronized/std::mutex) или примитивы железа (Atomic*/std::atomic<*>). И очень важно видеть это сходство.
И вообще, мы разрабатываем не 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 — в Java11 были удалены самые бесполезные методы из Unsafe. Думаю, пока хотя бы половина из всех пользующихся Unsafe проектов не переедут на что-то другое, Unsafe будет доступен в том или ином виде.
Существует мнение: Банк + Java = кровавый закостенелый энтерпрайз?
В нашей команде таких ужасов нет. На Spring'е у нас написано, наверное, строчек десять, причем мною)) Мы стараемся не использовать большие технологии. Предпочитаем делать маленькое, аккуратное и быстрое, чтобы мы могли это осознать, контролировать и при необходимости модифицировать. Последнее очень важно для систем (вроде нашей), к которым предъявляются нестандартные требования, которые могут отличаться от требования 90% пользователей фреймворка. И в случае использования большого фреймворка, мы не сможем ни донести свои потребности комьюнити ни самостоятельно поправить поведение.
На мой взгляд, у разработчиков всегда должна быть возможность использовать все доступные инструменты. Я пришел в мире Java из C++ и мне очень сильно бросается в глаза разделение сообщества на тех, кто разрабатывает runtime виртуальной машины/компилятор или саму виртуальную машину и на прикладных разработчиков. Это прекрасно видно по коду стандартных классов JDK. Зачастую авторы JDK пользуются другим API. Потенциально это означает, что мы не можем добиться пиковой производительности. Вообще, я считаю, что использование одинакового API для написания и стандартной библиотеки и прикладного кода – отличный показатель зрелости платформы.
Еще кое-что
Думаю, всем разработчикам очень важно знать, как работает, если не весь стек, то хотя бы его половина: Java-код, байт-код, внутренности среды исполнения виртуальной машины и ассемблер, железо, ОС, сеть. Это позволяет шире посмотреть на проблемы.
Также стоит упомянуть производительность. Очень важно не замыкаться на средних показателях и всегда смотреть на показатели медианы и высоких перцентилей (худший из 10/100/1000/… замеров).
Обо всем этом буду рассказывать на встрече Java User Group 30 мая в Санкт-Петербурге. Встреча с Сергеем Мельниковым, это я как раз)) Зарегистрироваться можно по ссылке.
О чем будем говорить?
- Про профилирование и использование стандартного профилировщика Linux и perf: как с ними жить, что они измеряют и как трактовать их результаты. Это будет введение в общее профилирование, с советами, лайфхаками, как выжать из профилировщиков все возможное, чтобы они профилировали с максимальной точностью и частотой.
- Про особенности оборудования для получения ещё более подробного профиля и просмотра профиля редких событий. Например, когда ваш код каждый сотый раз работает в 10 раз медленнее. Об этом ни один профилировщик не расскажет. Мы с вами напишем свой небольшой профилировщик, используя штатный механизм ядра Linux и попробуем посмотреть профиль какого-нибудь редкого события.
Приходите на встречу, это будет отличный вечер, будет много интересных рассказов про нашу платформу и про наш любимый язык.
До встречи ;)