Уже два десятилетия активно плодятся мифы о том, что приложениям на Java свойственны проблемы с производительностью. Одновременно с этим на Java создаются по-настоящему высоконагруженные системы. Кто же в конечном итоге прав? Чтобы составить мнение о том, как сейчас обстоят дела с производительностью Java, мы обратились к двум заинтересованным сторонам: создателям самой Java и клиентам, использующим Java в своих системах. На наши вопросы любезно согласились ответить Алексей Шипилёв (Oracle) и Олег Анастасьев (Одноклассники).
Java Performance глазами создателей JDK
Jug.Ru: Расскажите, пожалуйста, о себе и о своей работе?
Алексей Шипилёв: Меня зовут Алексей Шипилёв. Я работаю над производительностью Java уже более 10 лет. За это время я успел поработать над разными JVM — сначала над Apache Harmony в Intel, потом перешел в Sun Microsystems, где занимался OpenJDK. На текущий момент моя работа по большей части заключается в том, чтобы находить проблемы производительности в продукте и обозначать пути их решения или же исправлять собственными руками, если проблемы простые. Сюда входит и оптимизация под стандартные бенчмарки, тестирующие производительность виртуальной машины, и решение клиентских проблем (оптимизация их приложений), и улучшение глобальных вещей, которые необходимы многомиллионной экосистеме Java.
Jug.Ru: Как вы считаете, корректно ли вообще на сегодняшний день говорить о том, что производительность — проблема в целом Java, как технологии, а не отдельных приложений?
Алексей Шипилёв: Об этом сложно говорить, поскольку производительность чаще определяется всё же кодом конечного приложения, а не задействованным для его создания языком. У языка программирования вообще нет производительности, она может быть только у реализации этого языка. Причем, различных реализаций может быть много. Однако в мире Java случилось так, что чаще всего речь идет о реализации JDK от Sun/Oracle, которая занимает более 95% рынка. Её и будем иметь в виду.
В самом начале, в 1995 — 2000 годах, Java, как и всякий молодой продукт, действительно была не очень эффективно реализована. Но за последнее десятилетие в реализациях Java сделано настолько много, что проблемы, которые раньше считались типичными, перестали так сильно и так больно бить разработчиков по голове.
Конечно, многие сложности, связанные с разработкой высокопроизводительных приложений, сохранились, так что говорить, что эта проблема окончательно решена, по-моему, неосмотрительно. Есть ещё много вещей, которые необходимо докручивать.
Но важно помнить, что далеко не всем нужны высокопроизводительные решения: у многих разработчиков производительность не входит в список критериев успеха — "работает за приемлемое время" и хорошо. Если же разработчикам действительно нужна каждая последняя капля производительности, им придётся обходить подводные камни, которые присутствуют в любом достаточно сложном продукте (а виртуальная машина HotSpot — и OpenJDK в целом — очень сложный продукт).
Jug.Ru: Иными словами, производительность уже не является глобальной проблемой Java?
Алексей Шипилёв: Думаю, да.
Честно говоря, мне кажется, что проблемы производительности любой платформы — преувеличены. Как я говорил, большинство приложений не требуют большой производительности. Но в программистском сообществе существуют стойкие легенды и простые рецепты, которые очень удобно повторять (когда ты их повторяешь, кажется, что присоединяешься к группе посвящённых!). И одна из таких легенд заключается в том, что "Java тормозит". Лично я уверен, что она давно не релевантна. Для неё были объективные подтверждения лет 10-15 назад, но сейчас обстановка изменилась. Безусловно, и сейчас можно написать приложения, которые наступят на проблемы с производительностью в рантайме. Но эти проблемы в основном известны, для них есть обходные пути, а те, кому такие пути не подходят, создают собственные "костыли".
Jug.Ru: Насколько активно идёт развитие JDK и, соответственно, устранение известных проблем производительности (если говорить об Oracle JDK)?
Алексей Шипилёв: Достаточно активно, и этому есть причина — экосистема Java очень большая, даже в пересчёте на одну компанию. У того же Oracle энтерпрайз-стеки написаны на Java. Соответственно, любое улучшение, которое делается в платформе, распространяется по всему стеку и облегчает жизнь в том числе и разработчикам Oracle. Но это история, почему Oracle развивает OpenJDK. Эта история в разных качествах повторяется и для других вендоров, и для других проектов с открытым кодом.
Jug.Ru: Какие последние нововведения в Java вам кажутся наиболее значимыми с точки зрения повышения производительности?
Алексей Шипилёв: Во-первых, мне нравится, что история со сборщиком мусора Garbage-First (G1) потихоньку идет к логическому концу. Garbage-First был анонсирован давно, но только в Java 8 и 9 он стал себя довольно прилично вести, так что его можно использовать в промышленных масштабах — настолько, что в Java 9 он включён по умолчанию. Этот многолетний проект наконец-то выстреливает и делает вещи, которые задумывались с самого начала.
Во-вторых, мне близка история с высокопроизводительными приложениями, которые требуют Unsafe, на замену части которого разрабатывается VarHandles. Есть легитимные случаи, когда хочется выжать последние капли производительности при помощи низкоуровневых хаков. Но Unsafe, как известно, — это приватный API, толком не стандартизированный, т.е. его использование — это бег с острыми ножницами по тлеющим углям горящего здания. А VarHandles — это один из путей, при помощи которого мы можем предоставить публичный API для таких редких, но важных случаев, когда нужна максимальная производительность или какая-то функциональность, которая иначе не доступна.
Еще одно интересное нововведение — Compact Strings. Я лично участвовал в этом проекте и прочих "строковых" оптимизациях. Подобного рода изменения платформы, серьёзно улучшающие общеупотребительные классы, существенно повышают производительность вообще всех приложений, которые написаны на Java, и тем самым ещё больше снижают необходимость костылей.
И в данном случае мы получили очень хороший результат не только на синтетике, но и на реальных приложениях. Достигнутое улучшение занимаемой памяти и производительности на 10% на больших приложениях — это очень хорошие приросты для такой здоровой взрослой платформы, как Java.
Jug.Ru: Раз уж мы говорим о производительности, существуют ли какие-то "канонические" методы её измерения? Сводится ли для бизнеса всё к деньгам?
Алексей Шипилёв: Производительность не всегда переводится в деньги. На практике довольно сложно оценить, как прирост производительности влияет на экономическую сторону вопроса. Часто это косвенные эффекты — время, которое тратит программист на написание кода, укладывающегося в цели по производительности; время, которое тратят пользователи на ожидание результата, и т.п. Но с размещением серверов в облаках и плотных датацентрах производительность стала ближе к финансовой стороне вопроса: чем быстрее работает твоё приложение, тем меньше оно потребляет ресурсов, тем меньше ты платишь за аренду и обслуживание серверов. Причем эта зависимость в хорошо масштабируемых приложениях может быть просто линейная, т.е. разогнал на 50% свое приложение — тебе нужно в 2 раза меньше железа, платишь за инфраструктуру в 2 раза меньше.
Кроме того, вопрос денег возникает, когда нужно оправдать временные затраты на оптимизацию. Организации, которые занимаются коммерческой разработкой — не богадельни. Они платят инженерам, чтобы те помогали решать их бизнес-задачи, поэтому организации пытаются понять, стоит ли финансировать конкретное направление разработки; сколько бизнес-профита дадут эти результаты. Так что оптимизация — это не просто "мы тут поковырялись отверточкой, потому что нам очень интересно ковыряться именно здесь". Так могут мыслить отдельные разработчики, иногда удачно соотнося эти желания с бизнес-целями, но бизнес сам по себе в оптимизации ради оптимизации не заинтересован.
Jug.Ru: Какие будущие нововведения в JDK, на ваш взгляд, наиболее ожидаемы с точки зрения управления производительностью?
Алексей Шипилёв: Value Types — очень ожидаемое нововведение, которое на данный момент планируется реализовать к выходу Java 10. Это очень сложный проект, который требует подробного разбора, как он стыкуется со всей остальной платформой. "Священная корова" Java — это обратная совместимость. Нельзя сделать фичу, которая её сломает (точнее, можно сломать её в каких-то мелких моментах, но нужно очень хорошее обоснование, почему вы это ломаете, и какие у пользователя есть пути обхода).
Value Types решает очень простую проблему. Одним из столпов Java как языка программирования является негласное свойство, что (практически) всё — объект. В этом кроется интересная грабля: проистекает она из того, что у объектов Java есть индивидуальные свойства. Например, идентичность (identity): если вы сделали объект, у которого записано число 42 в каком-то его поле, и второй объект, в котором лежит "такое же" число 42, то эти 2 объекта — разные с точки зрения языка, и отличаются как раз за счет identity. С точки зрения реализации, это означает, что и хранить нужно две отдельные копии этих практически одинаковых объектов — например, чтобы было где сохранить метаинформацию о них. И когда в приложении появляются большие графы объектов, накладные расходы для каждого объекта пожирают существенную часть полезной памяти. Было бы неплохо, если бы была в языке были бы сущности без identity, для которых этого можно было бы избежать. И такие сущности есть: примитивы! Но их список жёстко зафиксирован. Естественное расширение — дать возможность декларировать сущности, которые записываются как классы, а работают как примитивы — это и есть value types.
Value types существенно отличаются от привычных reference-типов. Например, является ли Object супертипом для всех value-типов? Логично, что нет, и тогда появляются тонкие моменты взаимодействия с дженериками, со специализацией и т.п. Есть библиотеки, которые делают подобного рода специализацию руками (тот же GNU Trove), но всем хочется, чтобы это было реализовано в самом языке. Так что эта очень ожидаемая фича: известно, какие бонусы она принесёт; известно сейчас, какие возникнут проблемы. Однако в ходе разработки мы посмотрим ещё, сколько там реально бонусов, а сколько проблем.
Jug.Ru: Учитывая, что проблемы производительности — скорее частные, нежели глобальные, можно ли говорить о какой-то типичной схеме при оптимизации приложений?
Алексей Шипилёв: Существуют вполне конкретные методологии, которые предписывают, куда в первую очередь стоит смотреть на основании тех или иных симптомов. Мы с Сергеем Куксенко и прочими делали доклады на эту тему.
К примеру, можно говорить, что у нас очень хорошие сборщики мусора, но, как ты не крутись, если будешь очень много мусорить, ты в итоге сборка мусора будет занимать существенную часть времени. Какой рантайм ты не напиши, а если программист руками написал сортировку пузырьком или линейный поиск по массиву в 100 миллионов элементов, быстро не будет. Тут никакой магии нет — один дурак может такую задачку загадать, на которую семеро мудрецов не ответят.
По моему опыту могу сказать, что если производительностью конкретного приложения никто никогда не занимался или занимался плохо, то там почти наверняка (на 99%) есть множество идиотских или очевидных неэффективностей, которые можно быстро обнаружить и быстро исправить, подняв производительность в разы.
Jug.Ru: А кроме сборки мусора какие есть типичные проблемы, легко поддающиеся оптимизации?
Алексей Шипилёв: Мои любимые — проблемы с многопоточностью. Известно, что самый простой способ написать корректное многопоточное приложение — это щедро использовать синхронизацию. Я не говорю, что эта практика порочна, но часто встречаются проблемы с тем, что аппаратные ресурсы используются не полностью из-за постоянных блокировок. Это очень легко диагностируется, и зачастую легко исправляется (часто, правда, требует правок в архитектуре).
Очень часто встречаются алгоритмические проблемы, когда переписыванием плохих кусочков кода на хорошие кусочки, которые либо имеют лучшую алгоритмическую сложность в принципе, или каким-нибудь способом используют специфичные знания о данных в приложении, получаются гигантские приросты, которые никаким рантаймным оптимизациям и не снились.
JDK/JVM-специфичные проблемы встречаются, но редко. Сюда падают и проблемы с плотностью данных в памяти (откуда нам опять машут рукой value types), и проблемы с высокоуровневыми оптимизациями (escape-анализ и автовекторизация, привет!), и проблемы с кодогенерацией. И тут скользкий вопрос — проблема в том, что рантайм плохой и не работает "правильно", или в том, что мы не хотим в данном случае как-то изменить решение, чтобы у нас была производительность лучше (например, использовать дополнительную библиотеку). Разные люди и разные организации смотрят на это по-разному.
Вообще с моей точки зрения оптимизация производительности Java-приложений принципиально не отличается от оптимизации какого-то нативного приложения, в котором JVM не участвует. JVM — это, конечно, отдельный уровень в этой иерархии, но многие проблемы, которые там существуют, присущи разработке вообще, а не разработке конкретно на Java.
Jug.Ru: Учитывая, что определенные проблемы всё же существуют, есть ли смысл использовать Java для высокопроизводительных приложений?
Алексей Шипилёв: Знаете, когда я был школьником, один из моих преподавателей в ответ на ехидный вопрос кого-то из моих друзей, почему же мы не пишем на таком-быстром-С, сказал следующую вещь: "Я буду писать мой промышленный код на Pascal (популярном в те далёкие времена), потому что он везде мне подложит подстилки, везде всё проверит, не даст мне выстрелить себе в ногу. А в том месте, где мне важна скорость, я уж обману его так, чтобы было быстро". И эта история повторяется с разными действующими лицами и с разными языками: Pascal против C, Java против C++, C против ассемблера и т.п. На деле производительность большого приложения на горизонте вменяемых приростов, как правило, определяется производительностью довольно маленького куска в этом приложении. Поэтому может быть проще не буйствовать и не писать на языке, который вас заставляет писать низкоуровневый код, потому что вы с ума сойдете. Стоит писать на высокоуровневом языке, а там, где надо, обмануть его: сделать так, чтобы в конкретных местах было быстрее, перейдя либо к менее идиоматическому коду, повторяющему кривизну библиотек и рантайма, либо отдав тяжёлое на уровень ниже. Практика промышленной разработки на Java и история её производительности во многом этот подход олицетворяет.
Ближайший доклад Алексея состоится на конференции Joker 2016 в формате кейноута и, конечно же, он будет посвящен производительности платформы и способам повышения производительности вашего кода.
Java Performance глазами разработчика
Jug.Ru: Расскажите, пожалуйста, о себе и о своей работе.
Олег Анастасьев: Я работаю в команде платформы в компании Одноклассники. Команда платформы разрабатывает программы для того, чтобы Одноклассники работали быстро, т.е. разрабатывает и поддерживает различные хранилища данных, фреймворки для коммуникации серверов друг с другом и т.п. Кроме того, если что-то с быстродействием случается на продакшене, именно команда платформы ищет решение, как это лечить. Наша ответственность — сделать так, чтобы Одноклассники работали быстро, потому что если они не будут работать быстро, они просто работать не будут — очень быстро завалятся под нагрузкой.
Jug.Ru: Как вы считаете, есть ли смысл использовать Java для высоконагруженных приложений? Или попросту нет альтернативы?
Олег Анастасьев: Возможно, Java — не самый быстрый язык, существуют и более быстрые языки. Но если рассматривать Java, как язык для разработки больших нагруженных проектов, то здесь ей в принципе альтернатив пока нет. Можно написать более быстрый код на языках C или C++, но при этом этот код будет более дорогой — его написание, отладка и последующая поддержка будут стоить значительно дороже, чем аналогичный код на Java. Кроме того, к коду на C, который торчит в интернет, возникает очень много вопросов безопасности. Как известно, в языке С возможны всякие небезопасные конструкции, через которые потом нехорошие люди будут вас взламывать. На Java таких небезопасных конструкций меньше, поэтому в части безопасности программа на Java потребует меньших усилий.
В итоге Java имеет очень хорошее отношение цена / производительность.
У Java есть ряд проблем, в частности, нам пришлось отдельно работать над управлением памятью и поддержкой высокого трафика, но они могут быть решены при помощи небольшого количества кода — мы для этого создали отдельную библиотеку one-nio (ссылка на https://github.com/odnoklassniki/one-nio). Вся остальная масса кода обладает теми положительными чертами Java, которые у нее есть — быстрая разработка, безопасность, хороший инструментарий диагностирования проблем, встроенный в JVM, защита от ошибок и т.д.
Jug.Ru: Вы упомянули, что пришлось решать определенные проблемы производительности. Расскажите, пожалуйста о них подробнее?
Олег Анастасьев: Для нас быстродействие — это не только скорость выполнения кода на Java. Мы рассматриваем его и с ракурса эффективного использования ресурсов — то есть и объём обрабатываемых данных, и пропускная способность, и потребление памяти. И здесь в Java, действительно, много чего не хватает: коллекций примитивов, struct-ов, работы с оффхипом, прозрачного использования нативных API, управления affinity, файловыми кешами и т.д., поэтому нам приходится разрабатывать решения, которые позволяют обходить узкие места.
Для нас быстродействия Java в такой трактовке не хватает, более того, этот вопрос не может ждать несколько лет следующего релиза Java — проблемы должны решаться прямо сейчас, поэтому мы активно ищем их решения сами. Давайте остановимся на этом подробнее.
Например, одна из болевых точек Java — это быстродействие ввода-вывода, как блокирующего, так и не блокирующего (в частности, сетевого).
Это хорошо видно на примере раздачи видео. Общий исходящий трафик видео сейчас достигает 500 гигабит. Для того чтобы обслуживать такой поток мы должны раздавать видео как можно быстрее, чтобы как можно больше трафика приходилось на один сервер. Наше железо способно отдавать 40 Гбит с машины, но написать на Java при помощи стандартных решений сервер, который будет использовать все 40 Гбит у вас не получится — будет слишком много потерь в производительности внутри самой Java. Это одна из проблем, которые мы решали в составе нашей open source библиотеки.
Пример с 40 Гбитным трафиком — это своего рода экстремум. Есть и менее нагруженные сервера, но там тоже присутствуют свои проблемы. Например, еще одна болевая точка Java — это хранение большого количества объектов в памяти. У Java есть garbage collector. С одной стороны, это хорошо, поскольку он позволяет автоматически убирать мусор. Но с другой стороны, когда вам нужно кешировать в памяти очень много информации, он скорее мешает, чем помогает. Более того, если массив данных на сотню гигабайт хранится в памяти, то вы захотите, чтобы он не потерялся при перезапуске программы — его загрузка займет значительное время. Такие массивы хочется хранить в разделяемой памяти, а встроенных средств в Java для этого тоже нет. В таких местах хочется иметь ручное управление памятью. Хорошо, что в Java есть Unsafe, через который мы и сделали собственное решение.
Jug.Ru: Развивается ли JDK в направлении решения специфичных для ваших задач проблем? Появляются ли новые опции, которые вы можете использовать?
Олег Анастасьев: Последняя выпущенная в мир версия Java — 8. В ней решений упомянутых проблем нет. Есть только намерение решить какие-то из этих проблем в Java 9, часть из них — в 10 и более поздних. Но получится или не получится; насколько предложенные решения будут лучше, чем есть сейчас, говорить пока рано, т.к., например Java 9 еще не вышла. Конечно, бета-версию уже можно брать, но что изменится, пока она дойдет до релиза, не известно. Поэтому выйдет Java — посмотрим.
Jug.Ru: А есть ли какие-то ожидаемые нововведения, которые могли бы вам помочь? К примеру, VarHandles?
Олег Анастасьев: Поможет или нет нам VarHandles зависит от того, как они в итоге будут реализованы в конечной версии, и как быстро они будут работать.
VarHandles — довольно сложный способ даже с точки зрения API сделать то, что сейчас можно сделать просто и понятно через Unsafe. Это возможность объявить массив в памяти одного типа, а потом читать его, как массив памяти другого типа. Для человека, знакомого с принципами ассемблера или C, это выглядит как обращение по адресу памяти и считывание ячейки памяти, как Long или как Byte, в зависимости от ситуации. Грубо говоря, VarHandles позволяет сделать то же самое, но более сложно (это технически более сложное решение на уровне JDK), но зато он имеет больше защиты от программистов, которые периодически "стреляют себе в ноги".
К тому же, VarHandles решают только один из сценариев использования Unsafe. Но мы используем Unsafe также и совсем для других сценариев (например, для кастомной сериализации или работы с разделяемой памятью), и альтернатив этому раньше Java 10 точно не ожидается.
Jug.Ru: На основании чего в вашей компании принимается решение о необходимости работы над производительностью приложений?
Олег Анастасьев: Мы не занимаемся быстродействием ради быстродействия; всегда оцениваем экономический эффект от оптимизации.
Для нас производительность измеряется в количестве денег, которые нужно потратить на железо: сначала на закупку серверов, а затем каждый год на их поддержку в дата-центре. Чем больше быстродействие приложений — тем меньше железа необходимо на ту же задачу, т.е. меньше денег уйдет на его поддержку.
Оптимизация производительности отталкивается от поставленных задач. Можем ли мы позволить себе вот это количество техники для этой конкретной задачи?
К примеру, потратить, условно говоря, год работы высококвалифицированного программиста, чтобы улучшить быстродействие на 0,5%, экономически не эффективно до тех пор, пока у вас не затронуты некой проблемой, допустим, все сервера в дата-центре. Тогда это будет экономически эффективно, и мы будем этим заниматься. Если нет, нам проще купить новых серверов, т.е. решить проблему железом.
Для нас быстродействие — это бизнес-метрика; и она имеет четкое экономическое обоснование. Быстродействие пилится до тех пор, пока оно экономически эффективно.
Несмотря на все вопросы к производительности решений на Java, отрадно видеть, что над этими проблемами идет непрерывная работа, а сама Java, несмотря на все нападки, давно стала промышленным стандартом для масштабных высоконагруженных корпоративных проектов.
Больше интересных докладов, технических, хардкорных, вы найдете в программе Joker 2016. Предлагаем вашему вниманию несколько примеров:
- Volker Simonis, SAP: HotSpot Internals: Safepoints, NullPointers and StackOverflows.
- Тагир Валеев, ИСИ СО РАН: Stream API: рекомендации лучших собаководов.
- Jean-Philippe BEMPEL, Ullink: Low Latency & Mechanical Sympathy: issues and solutions.
- Владимир Иванов, Oracle: Native код, Off-heap данные и Java.
Автор: JUG.ru Group