Я занимаюсь алгоритмической торговлей в Райффайзенбанке. Это довольно специфичная область банковской сферы. Мы делаем торговую платформу, работающую с низкими и предсказуемыми задержками. Успех приложения зависит, в том числе, и от скорости работы приложения, поэтому нам приходится заниматься всем стеком, задействованным в торговле: приватными сетевыми каналами, специальным аппаратным обеспечением, настройками ОС и специальной 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 мая в Санкт-Петербурге. Встреча с Сергеем Мельниковым, это я как раз)) Зарегистрироваться можно по ссылке.

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

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

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

До встречи ;)

Комментарии (19)


  1. Antervis
    29.05.2019 01:40
    +1

    В итоге получается, что проще подпилить в нужных местах Java, чтобы она работала достаточно быстро, чем делать всё с нуля и очень долго на С++

    так а что в итоге сложнее: придумать оптимальный код и написать его на с++ или заставить JVM сгенерировать тот же самый код?


    1. RainM Автор
      29.05.2019 12:00

      Лично мне проще написать какое-то узкое место на С++. Но код состоит далеко не только из таких критичных кусков. Итого, если говорить про конкретные участки кода — лично мне проще сделать из на C++. Но если говорить не только про эти супер-критичные куски то проще и быстрее сделать на Java а потом допилить до требуемой производительности, ИМХО


  1. webmascon
    29.05.2019 03:58

    Ну вот как так? Конференция завтра а обьявление о ней читаю сегодня. Как к вам блин за сутки то добраться через пол земного шара?


    1. RainM Автор
      29.05.2019 12:12

      А если бы заранее сделали бы анонс на хабре — приехали бы?))
      Вообще, JUG.ru делали почтовую рассылку


      1. webmascon
        29.05.2019 12:47

        Ну я хоть прислал бы кого кто поближе к питеру. На за сутки начальство не уговоришь


  1. webmascon
    29.05.2019 04:01

    Видео хотя бы будет?


    1. RainM Автор
      29.05.2019 12:00

      На сколько я понимаю, да, видео должно быть. Как узнаю точно — напишу.


    1. RainM Автор
      29.05.2019 13:47

      Мы подумали и мне точно помогут с расшифровкой доклада. Так что в любом случае мы опубликуем расшифровку на хабре.


  1. tmaxx
    29.05.2019 10:21

    Вы пишете интересные статьи, но в них немного не хватает конкретных цифр.


    Low latency — это сколько? Из личного опыта, разные люди отвечали на этот вопрос с разницей в три порядка (если брать исключительно Java)


    1. webmascon
      29.05.2019 11:45

      Это смотря откуда и до куда мерять эту latency.


    1. RainM Автор
      29.05.2019 12:11

      Хороший вопрос)) я бы подошел к этому вопросу немного с другой стороны. Мне кажется, что критерии soft real time (большие материальные потери при не выполнении SLA) ~ latency-sensitive. А low-latency зависит от сложности алгоритмов, выполняющихся при реакции системы на внешний раздражитель. Т.е. для HFT, которые арбитражат стаканы биржи low-latency это одно, а для тех, у кого пересчитываются сложные модели, low-latency — совсем другое.


    1. vyatsek
      29.05.2019 15:22

      10-20 микросов это уже low-latency


    1. dim2r
      29.05.2019 15:49

      в java недоступны некоторые техники, например, блокировка блоков виртуальной памяти в физической памяти, поэтом всегда возможен уход в swap


      1. Rhombus
        29.05.2019 16:16

        Достаточно отключить swap на сервере.
        Ну то есть не совсем отключить, но поставить swappiness = 0


        1. dim2r
          31.05.2019 09:34

          интересный вариант, а что будет, если дойдет до конца памяти?


  1. dim2r
    29.05.2019 14:37

    Прежде, чем делать многопоточность надо хорошо подумать над архитектурой.
    Инструкция процессора, выполняющая CAS работает раз в 20 медленнее, чем простое присваивание. Плюс многочисленные библиотеки грешат тем, что во многих местах стоит синхронизация. Иногда один поток может выполнить намного операцию быстрее, чем во многих потоках. Я предпочитаю иметь несколько однопоточных изолированных процессов и еще один процесс, который кушает результат их труда и раздает задания.


  1. frankmasonus
    29.05.2019 17:29

    Интересны ваши секреты про маркетдата (и борьба с GC) и де/сериализацией сообщений во всяких протоколах типа FIX, etc. Да и какие структуры используются для портфолио. А в теории в принципе все равны — и там и там инструмент надо знать, хоть C++ хоть Java. Но что-то подсказывает что секретов вы раскрывать не можете :)


    1. RainM Автор
      29.05.2019 17:42

      Я даже не знаю, как ответить. NDA ;-)


      1. frankmasonus
        29.05.2019 17:50

        Воот :)