Недавно я и моя команда работали над тестированием производительности определенного фрагмента кода (в частности, конечной точки HTTP), который был в значительной степени унаследован, но также имел некоторые собственные дополнения.

Чтобы убедиться, что общая задержка операции соответствует требованиям SLA с новыми дополнениями, мы начали выполнение нагрузочного теста с использованием Jmeter.

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

Библиотека публикации сообщений была построена с использованием протокола Stomp и управлялась другой командой.

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

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

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

Обратите внимание на темы * default-dispatcher- *. Это потоки, которым поручено опубликовать сообщение. Однако блоки красного цвета на шкале времени этих потоков указывают на то, что они ждут много времени на мониторе объектов (синхронизированные блоки, эксклюзивные блокировки с повторным входом в мире Java).

Зеленые области указывают на резьбу в рабочем состоянии.

Красные области указывают на потоки, ожидающие блокировок / условий и не выполняющие никакой полезной работы.

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

Это также отрицательно сказалось на размере кучи из-за скопления сообщений в очередях потоков.

Причина

После создания дампов потоков мы поняли, что эта библиотека внутренне использует потоковый / блокирующий ввод-вывод, что означает, что поток не может быть безопасно разделен между потоками.

Это повлекло за собой блокировку потока для чтения / записи, тем самым сериализуя доступ к нему для нескольких потоков.

Чтобы смягчить эту проблему, у нас были следующие варианты.



Производитель в сообщении

Создайте нового производителя (эффективно установите новое соединение с брокером сообщений) для каждого сообщения и закройте его после использования.

Плюсы

  • Легко реализовать.
  • Нет разделения между потоками, что означает отсутствие конкуренции между потоками.

Минусы

  • За каждое установление соединения придется оплачивать квитирование TCP (что делает его дорогостоящим).
  • Это напрямую повлияет на производительность, и задержка снова возрастет, вызывая скопление сообщений, поскольку скорость публикации всегда будет отставать от скорости создания сообщений.

Производитель на поток

Именно здесь мы подумали об изучении превосходного Java ThreadLocal. ThreadLocals - это, по сути, независимые копии состояния, исключающие какое-либо совместное использование состояния (переменных) между ними, поскольку каждый поток имеет эксклюзивный доступ к своей копии состояния, поддерживаемой статическим ThreadLocal.

Код в фрагменте 1 выше был изменен так, чтобы он выглядел как

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

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

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

Почему это произошло?

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

Однако даже несмотря на то, что потоки были отброшены, threadlocal, поддерживающий производителя шины сообщений (который сопоставлен с физическим подключением), все еще был открыт.

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

Для закрытия физических соединений на сервере потребуется гораздо больше времени (в основном они находятся в состоянии CLOSE_WAIT).

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

Урок

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

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

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



Возможные исправления

Однако нам все же нужно было исправить эту проблему. Я представлю здесь два решения.

Продюсер на актера

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

В нашем приложении широко используется отличная модель Актер и ее достойная реализация Akka для Java / Scala.

Почему это работает?

  • Акторы - это программные абстракции, в отличие от физических ресурсов, таких как потоки, которые обычно управляются контейнерами приложений.
  • Актеры занимают незначительное количество памяти и других ресурсов, поэтому поддержание такого пула Актеров требует незначительных затрат.
  • Невозможно получить доступ к внутреннему состоянию Актера за пределами Актера, что дает преимущество threadlocal.
  • Используемый пул акторов полностью находился под контролем приложения и мог запускаться без каких-либо тайм-аутов простоя, что было основной причиной неудачи подхода ThreadLocal.

Пул продюсеров

Приложения, которые не хотят использовать среду Actor только для этого варианта использования, могут реализовать пул для своих дорогостоящих ресурсов. Обычно мы видим реализации пула для HTTP-соединений, соединений с БД, соединений LDAP и так далее.

Если у вас нет готовой реализации пула, вы можете написать ее с минимальными усилиями, используя хорошо известную библиотеку Apache Commons Pool.

Размер пула может быть настроен вашим приложением (в зависимости от нагрузки), и он может гарантировать, что доступ к объединенному ресурсу будет осуществляться только одним потоком за раз, что позволяет избежать конфликта между потоками.

Заключение

  • ThreadLocals - отличная конструкция, доступная в стандартном JDK, позволяющая хорошо масштабировать многопоточное приложение.
  • Однако следует быть очень осторожным с ресурсами, управляемыми в threadlocal.
  • Избегайте: объекты, которые сопоставляются с физическими ресурсами за пределами JVM (особенно для управляемых пулов потоков). Примеры таких ресурсов: подключения к БД, дескрипторы файлов, сетевые сокеты и т. Д.
  • Хорошие кандидаты: объекты контекста / сеанса, к которым требуется доступ в разных местах в приложении, или любые объекты, которые можно безопасно иметь в каждом потоке.
  • ThreadLocals используется для правильных вариантов использования с правильными объектами, это может быть благом, тогда как они могут серьезно повлиять на ваше приложение или любую систему зависимостей.

Ресурсы

Кредиты

Большое спасибо Махендре Чхиммвалу за помощь мне с диаграммами, необходимыми для этой статьи. Также благодарим Гарги Дасгупта за ценные отзывы.

Другие статьи о параллелизме в Java, которые могут вам понравиться

  • Дорожная карта для разработчиков Java на 2020 год (дорожная карта)
  • 10 лучших практик многопоточности и параллелизма Java (статья)
  • 50 самых популярных вопросов о многопоточности и параллелизме в Java (вопросы)
  • 5 лучших книг по освоению параллелизма в Java (книги)
  • Разница между CyclicBarrier и CountDownLatch в Java? (Ответ)
  • Как избежать тупика в Java? (Ответ)
  • Понимание потока данных и кода в программе Java (ответ)
  • Действует ли параллелизм Java на практике в 2020 году (ответ)
  • Как осуществлять межпотоковое взаимодействие в Java с помощью уведомления о ожидании? (Ответ)
  • 10 советов, как стать лучшим Java-разработчиком в 2020 году (советы)
  • 5 курсов для углубленного изучения многопоточности Java (курсы)

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