Рубрики
Без рубрики

Оптимизация лямбд AWS Java

Советы по получению максимальной производительности от ваших бессерверных приложений, написанных на Java. Помеченный как aws, бессерверный, java, разработка.

Первоначально это было написано автором Стефани Яворски (Xer) на Блог канала ввода-вывода .

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

Существует множество методов оптимизации, которые вы можете использовать, и некоторые из них будут работать лучше, чем другие, в зависимости от ситуации. В этой статье мы сосредоточимся на оптимизации, которая может улучшить ваши функции Java AWS Lambda.

Самое главное при оптимизации – знать, где проводить оптимизацию. В этом вам может помочь профилировщик; он может очень подробно рассказать вам обо всех потенциально проблемных областях вашего кода с точки зрения производительности.

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

Конечно, вам не обязательно использовать профилировщик, вы потенциально можете измерить, сколько времени занимает выполнение, используя System.nanoTime() в начале и конце блока кода. Естественно, это была бы простая метрика, и у нее было бы гораздо меньше накладных расходов, чем при использовании полноценного профилировщика.

Выполнение этого может помочь вам сосредоточиться только на определенной части кода, а не на просмотре всех данных, представляющих весь ваш код. Это может быть очень примитивно, но, как и отладочные сообщения, создаваемые System.err.printf() , его быстро и легко добавлять, и единственное, что вам будет предоставлено, – это информация, которую вы специально запрашиваете. Вы могли бы даже использовать комбинацию этого с профилированием, чтобы дать вам лучшее представление о том, что происходит с вашим кодом, и с помощью printf() вы можете обнаружить, что такая ситуация возникает только при определенных обстоятельствах — профилировщики обычно не содержат никакой информации о входных данных для метода, поэтому им не хватает некоторого контекста.

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

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

В моей предыдущей статье было много обсуждений этого решения Библиотеки Java – Ваш Лямбда-враг . Когда класс инициализируется виртуальной машиной, он анализирует класс и должен загрузить всю информацию для класса, однако сам по себе это не сложный процесс. Сложность обычно возникает из-за того, что JIT-компилятору необходимо скомпилировать весь код, чтобы он быстрее выполнялся на виртуальной машине. Однако большое количество простых классов может замедлить этот процесс, поскольку виртуальная машина должна обрабатывать гораздо больше классов; особенно когда существует много зависимостей от классов. Таким образом, вы должны снизить сложность вашего класса и уменьшить количество классов.

Это кажется очевидным решением — меньшее количество классов означает меньшую нагрузку на виртуальную машину. Но легко случайно добавить то, что кажется небольшими библиотеками, которые, как оказывается, нуждаются во многих зависимостях. Одна вещь, о которой следует особенно помнить, – это ServiceLoader — любые классы, обнаруженные загрузчиком служб, будут инициализированы при их повторении. Таким образом, это означает, что службы, которые вы, возможно, никогда не будете использовать, могут просто инициализироваться. Если вам нужно использовать serviceloader для обнаружения служб, то эти реализации классов служб должны быть простыми заводскими классами.

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

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

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

Правильный способ реализации этого кэша – использовать SoftReference , эта ссылка будет продолжать указывать на объект до тех пор, пока на него не останется надежных ссылок (полей и переменных) и когда память будет исчерпана (во время сборки мусора). Если вам нужно знать, когда ссылка будет очищена, вы можете использовать ReferenceQueue ; обратите внимание, что при этом используется опрос, который может либо возвращаться немедленно, либо ждать, пока объект не станет доступным, и поэтому, если используется очередь, ее, скорее всего, лучше всего поместить в свой собственный поток.

Одна из худших вещей, которые вы можете сделать в своем коде, когда он должен иметь низкую задержку, – это блокировка. Эта блокировка происходит, когда вы ожидаете данных в другом потоке ( synchronized , Object.wait() и Lock.lock() ) или когда вы ожидаете результата от удаленного запроса (например, HTTP). Каждый раз, когда ваша программа просто сидит и ждет, когда что-то произойдет, это время, потраченное впустую. Поскольку одновременно выполняется только одно выполнение лямбды, на самом деле не будет никакой пользы, если процессор вообще не используется, потому что ваша стоимость одинакова независимо от того, сколько или как мало процессора вы используете (если вы Thread.sleep () с длительной продолжительностью, которая будет стоить вам, несмотря на то, что ничего не происходит).

Если это возможно в вашем коде, одним из способов сократить время, затрачиваемое на блокировку, является создание потока, который работает в фоновом режиме, а затем, когда вам действительно нужен результат, вы можете заблокировать его до тех пор, пока он не станет доступен. У вас может быть рабочий объект, который хранит результат вычисления и имеет внутри него атомарный тип (например, AtomicInteger или Atomicobject ), считывает это значение, чтобы увидеть, доступно ли оно, затем, если нет, блокирует монитор, а затем ожидает вычисления результата, считывая в цикле и игнорируя InterupptedException .

В качестве альтернативы более производительная реализация будет использовать ReadWriteLock и Условие для выполнения той же функции (поскольку блокировки более низкого уровня, несмотря на то, что требуют больше работы по реализации, могут работать лучше, чем volatile и синхронизированный ).

Конечно, если поток должен запуститься и завершиться, но результат не требуется, тогда он может просто дождаться блокировки, а затем, как только она будет получена, он может просто разблокировать и выйти. Естественно, если вы хотите избежать использования блокировок и вместо этого просто постоянно считывать данные из атомной переменной до тех пор, пока не будет установлено некоторое значение, ваш цикл занятости должен Thread.доходность() чтобы мы могли сообщить операционной системе, что мы хотим отказаться от остальной части нашего фрагмента процессора и передать его другому потоку. Получение результатов может иметь или не иметь эффекта в зависимости от количества потоков, которые могут выполняться одновременно в контейнере, в котором выполняется лямбда, однако, если в этом случае недостаточно ресурсов для одновременного запуска такого количества потоков, это освободит фрагменты процессора, чтобы поток, который действительно что-то делает, мог делать то, что ему нужно.

Надеюсь, вы сочтете эту информацию полезной и сможете использовать ее для оптимизации ваших лямбд, чтобы они работали лучше, снижали ваши затраты и позволяли быстрее обслуживать запросы.

Оригинал: “https://dev.to/adjohn/optimizing-aws-java-lambdas-4kmo”