Как значительно улучшить производительность Java?

23

Команда LMAX представила презентацию о том, как им удалось выполнить 100 тыс. Запросов в секунду с задержкой менее 1 мс . Они подкрепили эту презентацию блогом , техническим документом (PDF) и самим исходным кодом .

Недавно Мартин Фаулер опубликовал отличную статью об архитектуре LMAX и упоминает, что теперь они способны обрабатывать шесть миллионов заказов в секунду, и выделяет несколько шагов, которые команда предприняла, чтобы подняться на другой порядок производительности.

До сих пор я объяснил, что ключ к скорости процессора бизнес-логики - все делать последовательно в памяти. Просто делать это (и ничего глупого) позволяет разработчикам писать код, который может обрабатывать 10K TPS.

Затем они обнаружили, что, концентрируясь на простых элементах хорошего кода, можно довести это до 100K TPS. Для этого просто нужен хорошо продуманный код и небольшие методы - по сути, это позволяет Hotspot лучше выполнять оптимизацию, а процессорам - эффективнее кэшировать код во время работы.

Потребовалось немного больше сообразительности, чтобы подняться еще на порядок. Команда LMAX нашла несколько полезных вещей для этого. Один из них заключался в написании пользовательских реализаций Java-коллекций, которые были разработаны с учетом удобства кэширования и осторожности с мусором.

Еще один метод достижения такого высокого уровня производительности - это уделение внимания тестированию производительности. Я давно заметил, что люди много говорят о методах повышения производительности, но одна вещь, которая действительно имеет значение, это проверить это

Фаулер упомянул, что было найдено несколько вещей, но он упомянул только пару.

Существуют ли другие архитектуры, библиотеки, методы или «вещи», которые помогают достичь такого уровня производительности?

Дакота Север
источник
11
«Какие еще архитектуры, библиотеки, методы или« вещи »полезны для достижения такого уровня производительности?» Зачем спрашивать? Это цитата окончательный список. Есть много-много других вещей, ни одна из которых не оказывает такого влияния на элементы в этом списке. Все, что кто-либо может назвать, будет не таким полезным, как этот список. Зачем спрашивать плохие идеи, когда вы цитируете один из лучших когда-либо созданных списков оптимизации?
S.Lott
Было бы неплохо узнать, какие инструменты они использовали, чтобы увидеть, как сгенерированный код работает в системе.
1
Я слышал о людях, которые клянутся всеми видами техник. То, что я нашел наиболее эффективным, - это профилирование на уровне системы. Он может показать вам узкие места в том, как ваша программа и нагрузка работают с системой. Я бы предложил придерживаться общеизвестных рекомендаций относительно производительности и написания модульного кода, чтобы вы могли легко настроить его позже ... Я не думаю, что вы можете ошибиться с системным профилированием.
Ритеш

Ответы:

21

Существуют всевозможные методы для высокопроизводительной обработки транзакций, и тот, что в статье Фаулера, является одним из многих на переднем крае. Вместо того, чтобы перечислять набор методов, которые могут или не могут быть применимы к чьей-либо ситуации, я думаю, что лучше обсудить основные принципы и то, как LMAX обращается с большим количеством из них.

Для крупномасштабной системы обработки транзакций вы хотите сделать как можно больше из следующих действий:

  1. Минимизируйте время, затрачиваемое на самые медленные уровни хранения. От самого быстрого до самого медленного на современном сервере у вас есть: CPU / L1 -> L2 -> L3 -> RAM -> Disk / LAN -> WAN. Переход от даже самого быстрого современного магнитного диска к самой медленной оперативной памяти составляет более 1000x для последовательного доступа; произвольный доступ еще хуже.

  2. Минимизируйте или исключите время, потраченное на ожидание . Это означает совместное использование как можно меньшего количества состояний, и, если состояние должно быть общим, по возможности избегая явных блокировок

  3. Распределите рабочую нагрузку. Процессоры не получили гораздо быстрее , в последние несколько лет, но они уже получили меньше, и 8 ядер довольно часто встречаются на сервере. Помимо этого, вы можете даже распределить работу по нескольким машинам, что является подходом Google; Самое замечательное в этом то, что он масштабирует все, включая ввод / вывод.

По словам Фаулера, LMAX использует следующий подход к каждому из них:

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

  2. Используйте очередь без блокировки («прерыватель») для потока входных событий. В отличие от традиционных долговременных очередей сообщений, которые окончательно не блокируются и фактически включают болезненно медленные распределенные транзакции .

  3. Немного. LMAX выбрасывает его под шину на основании того, что рабочие нагрузки являются взаимозависимыми; результат одного меняет параметры для других. Это критическое предостережение, которое Фаулер явно призывает. Они делают некоторые используют параллелизм для того , чтобы обеспечить отказоустойчивости, но все бизнес - логики обрабатывается на одном потоке .

LMAX - не единственный подход к масштабному OLTP. И хотя он сам по себе довольно блестящий, вам не нужно использовать передовые методы, чтобы достичь такого уровня производительности.

Из всех вышеперечисленных принципов, # 3, вероятно, самый важный и самый эффективный, потому что, честно говоря, аппаратное обеспечение дешево. Если вы сможете правильно распределить рабочую нагрузку между полдюжиной ядер и несколькими десятками машин, то предел для традиционных методов параллельных вычислений - это предел . Вы будете удивлены тем, какую пропускную способность вы можете достичь, используя только несколько очередей сообщений и распределенный круговой распределитель. Очевидно, что он не так эффективен, как LMAX - на самом деле даже не близко - но пропускная способность, задержка и экономическая эффективность - это отдельные проблемы, и здесь мы говорим конкретно о пропускной способности.

Если у вас есть те же особые потребности, что и у LMAX - в частности, общее состояние, которое соответствует бизнес-реальности, а не поспешному выбору дизайна - тогда я бы предложил опробовать их компонент, потому что я не видел много иначе это соответствует этим требованиям. Но если мы просто говорим о высокой масштабируемости, то я бы посоветовал вам больше исследовать распределенные системы, потому что они являются каноническим подходом, используемым большинством организаций сегодня (Hadoop и смежные проекты, ESB и смежные архитектуры, CQRS, который Фаулер также упоминания и тд).

Твердотельные накопители также собираются изменить игру; возможно, они уже есть. Теперь у вас может быть постоянное хранилище с аналогичным временем доступа к ОЗУ, и, хотя твердотельные накопители серверного уровня по-прежнему ужасно дороги, они со временем снизятся в цене, как только число пользователей увеличится. Он был тщательно исследован, и его результаты ошеломляют и со временем будут только улучшаться, поэтому концепция «держать все в памяти» намного менее важна, чем раньше. Итак, еще раз, я постараюсь сосредоточиться на параллелизме, когда это возможно.

Aaronaught
источник
Обсуждение принципов - это основополагающие принципы, это здорово, и ваш комментарий превосходен и ... если только в статье Фаулера не было ссылки в примечании для кэширования забытых алгоритмов en.wikipedia.org/wiki/Cache-oblivious_algorithm (который хорошо вписывается в категория № 1 у вас выше) я бы никогда не наткнулся на них. Итак ... в отношении каждой категории, которую вы имеете выше, знаете ли вы о трех главных вещах, которые должен знать человек?
Северная Дакота,
@Dakotah: Я бы даже не начать беспокоиться о кэше местности до тех пор пока я полностью не устранен диск I / O, который является , где подавляющее большинство времени тратится на ожидание в подавляющем большинстве приложений. Помимо этого, что вы подразумеваете под "топ-3 вещей, которые человек должен знать"? Топ 3 что, знать о чем?
Aaronaught
Переход от задержки доступа к ОЗУ (~ 10 ^ -9 с) к задержке магнитного диска (в среднем ~ 10 ^ -3 с) еще на несколько порядков больше, чем 1000x. Даже твердотельные накопители имеют время доступа, измеряемое сотнями микросекунд.
Седат Чужой
@Sedate: Задержка да, но это больше вопрос пропускной способности, чем необработанной задержки, и как только вы получите время доступа и общую скорость передачи, диски не так уж и плохи. Вот почему я сделал различие между случайным и последовательным доступом; для случайных сценариев доступа , которые он делает в первую очередь становится латентной проблемой.
Аарона
@Aaronaught: Перечитав, я полагаю, что вы правы. Возможно, следует отметить, что доступ к данным должен быть как можно более последовательным; Значительные преимущества также могут быть получены при доступе к данным в порядке из ОЗУ.
Седат Чужой
10

Я думаю, что самый большой урок, который можно извлечь из этого, состоит в том, что вам нужно начать с основ:

  • Хорошие алгоритмы, подходящие структуры данных и не делать ничего «действительно глупого»
  • Хорошо продуманный код
  • Тестирование производительности

Во время тестирования производительности вы профилируете свой код, находите узкие места и исправляете их одно за другим.

Слишком много людей прыгают прямо к части «почини их один за другим». Они тратят кучу времени на написание «пользовательских реализаций java-коллекций», потому что они просто знают, что причина в том, что их система работает медленно, из-за нехватки кэша. Это может быть способствующим фактором, но если вы перейдете прямо к настройке низкоуровневого кода, вы, скорее всего, упустите большую проблему использования ArrayList, когда вы должны использовать LinkedList, или реальной причины, по которой ваша система работает. Это происходит медленно, потому что ваш ORM загружает потомков объекта и, таким образом, делает 400 отдельных поездок в базу данных для каждого запроса.

Адам Яскевич
источник
7

Я не буду особенно комментировать код LMAX, потому что я думаю, что он достаточно подробно описан, но вот несколько примеров того, что я сделал, что привело к значительным улучшениям производительности.

Как всегда, это методы, которые следует применять, если вы знаете, что у вас есть проблема и вам необходимо повысить производительность, иначе вы, скорее всего, просто будете проводить преждевременную оптимизацию.

  • Используйте правильную структуру данных и, если необходимо, создайте собственную - правильная структура структуры данных опережает улучшение, которое вы когда-либо получите от микрооптимизаций, поэтому сделайте это в первую очередь. Если ваш алгоритм зависит от производительности при большом количестве быстрых операций чтения O (1), убедитесь, что у вас есть структура данных, которая поддерживает это! Чтобы сделать это правильно, стоит перепрыгнуть через несколько циклов, например, найти способ, которым вы можете представить свои данные в массиве, чтобы использовать очень быстрое O (1) индексированное чтение.
  • Процессор быстрее, чем доступ к памяти - вы можете сделать довольно много вычислений за время, которое требуется для чтения одной случайной памяти, если память не находится в кэше L1 / L2. Обычно стоит проводить вычисления, если они экономят память.
  • Помогите JIT-компилятору в окончательном создании полей, методов и классов. Final позволяет выполнять конкретные оптимизации, которые действительно помогают JIT-компилятору. Конкретные примеры:

    • Компилятор может предположить, что конечный класс не имеет подклассов, поэтому может превратить вызовы виртуальных методов в вызовы статических методов.
    • Компилятор может обрабатывать статические конечные поля как константу для хорошего улучшения производительности, особенно если эта константа затем используется в вычислениях, которые могут быть вычислены во время компиляции.
    • Если поле, содержащее объект Java, инициализируется как окончательное, тогда оптимизатор может исключить как нулевую проверку, так и отправку виртуального метода. Ницца.
  • Замените классы коллекций массивами - это приводит к снижению читабельности кода и усложняет его обслуживание, но почти всегда быстрее, поскольку устраняет слой косвенности и извлекает выгоду из множества приятных оптимизаций доступа к массиву. Обычно это хорошая идея для внутренних циклов / кода, чувствительного к производительности, после того, как вы определили его как узкое место, но избегайте иного для удобства чтения!

  • По возможности используйте примитивы - примитивы существенно быстрее, чем их объектные эквиваленты. В частности, бокс добавляет огромное количество накладных расходов и может вызвать неприятные паузы в GC. Не допускайте упаковки каких-либо примитивов, если вы заботитесь о производительности / задержке.

  • Минимизируйте блокировку низкого уровня - замки очень дороги на низком уровне. Найдите способы либо полностью избежать блокировки, либо блокируйте на грубом уровне, так что вам нужно будет редко блокировать только большие блоки данных, и низкоуровневый код может продолжаться, не беспокоясь о проблемах блокировки или параллелизма.

  • Избегайте выделения памяти - это может на самом деле замедлить вас в целом, так как сборка мусора в JVM невероятно эффективна, но очень полезна, если вы пытаетесь достичь очень низкой задержки и вам нужно минимизировать паузы GC. Существуют специальные структуры данных, которые вы можете использовать, чтобы избежать выделения ресурсов - библиотека http://javolution.org/, в частности, превосходна и примечательна для них.
mikera
источник
Я не согласен с окончательными методами . JIT способен выяснить, что метод никогда не переопределяется. Более того, если подкласс загружается позже, он может отменить оптимизацию. Также обратите внимание, что «избегать выделения памяти» также может усложнить работу ГХ и, таким образом, замедлить работу, поэтому используйте с осторожностью.
Maaartinus
@maaartinus: в отношении finalнекоторых JIT это может быть понятно, другие - нет. Это зависит от реализации (как и многие советы по настройке производительности). Согласитесь с распределением средств - вы должны это сравнить. Обычно я обнаружил, что лучше исключить ассигнования, но YMMV.
Микера
4

Помимо того, что уже было сказано в отличном ответе от Aaronaught, я хотел бы отметить, что подобный код может быть довольно сложным для разработки, понимания и отладки. «Хотя это очень эффективно ... это очень легко испортить ...», как один из их парней упомянул в блоге LMAX .

  • Для разработчика, привыкшего к традиционным запросам и блокировкам , кодирование для нового подхода может показаться ездой на дикой лошади. По крайней мере, это был мой собственный опыт при экспериментировании с Phaser, концепция которого упоминается в технической статье LMAX. В этом смысле я бы сказал, что этот подход обменивает конкуренцию между разработчиками .

Учитывая вышесказанное, я думаю, что те, кто выбирает Disruptor и аналогичные подходы, лучше убедиться, что у них есть ресурсы для разработки, достаточные для поддержки их решения.

В целом, подход Disruptor выглядит довольно многообещающе для меня. Даже если ваша компания не может позволить себе использовать его, например, по причинам, указанным выше, подумайте над тем, чтобы убедить ваше руководство «инвестировать» определенные усилия в его изучение (и SEDA в целом) - потому что, если они этого не сделают, есть шанс, что однажды их клиенты оставят их в пользу более конкурентоспособного решения, требующего в 4, 8 и т. д. меньше серверов.

комар
источник