Недавно я и моя команда работали над тестированием производительности определенного фрагмента кода (в частности, конечной точки HTTP), который был в значительной степени унаследован, но также имел некоторые собственные дополнения.
Чтобы убедиться, что общая задержка операции соответствует требованиям SLA с новыми дополнениями, мы начали проводить нагрузочный тест с помощью Jmeter.
Унаследованный код также будет асинхронно публиковать некоторые сообщения в нашей внутренней шине сообщений, чтобы сообщение могло использоваться другими системами, а также для конвейера аналитики. Поток высокого уровня выглядел примерно так.

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

Обратите внимание на темы *default-dispatcher-*. Это потоки, которым поручено опубликовать сообщение. Однако блоки красного цвета на временных шкалах этих потоков указывают на то, что они ожидают много времени в мониторе объектов (синхронизированные блоки, эксклюзивные повторные блокировки в мире Java).
Зеленые области указывают на потоки в рабочем состоянии.
Красные области указывают на потоки, ожидающие блокировок/условий и не выполняющие никакой полезной работы.
В идеале таких красных блоков следует избегать, поскольку они серьезно влияют на параллелизм приложения, а также на общую производительность.
Это также отрицательно сказалось на размере кучи из-за накопления сообщений в очередях потоков.
Причина
Сделав дампы потоков, мы поняли, что эта библиотека внутренне использует потоковый/блокирующий ввод-вывод, что означает, что поток не может быть безопасно разделен между потоками.
Это повлекло за собой блокировку потока для чтения/записи, тем самым сериализовав доступ к нему несколькими потоками.
Чтобы смягчить эту проблему, у нас были следующие варианты.
Производитель за сообщение
Создайте нового производителя (фактически установите новое соединение с брокером сообщений) для каждого сообщения и закройте его при использовании.
Плюсы
- Легко реализовать.
- Нет совместного использования между потоками, что означает отсутствие соперничества между потоками.
Минусы
- Каждое установление соединения должно будет оплачивать стоимость рукопожатия TCP (что делает его дорогостоящим предложением).
- Это напрямую повлияет на производительность, а задержка снова увеличится, что приведет к накоплению сообщений, поскольку скорость публикации всегда будет отставать от скорости создания сообщений.
Производитель на поток
Именно здесь мы подумали о том, чтобы изучить превосходную Java ThreadLocal. ThreadLocals — это, по существу, независимые копии состояния, исключающие любое совместное использование состояния (переменных) между ними, поскольку каждый поток имеет монопольный доступ к своей копии состояния, поддерживаемой статическим ThreadLocal.
Код во фрагменте 1 выше был изменен, чтобы выглядеть так:
Измененный визуальный снимок виртуальной машины выглядел намного лучше, так как красных блоков больше не было, и это остановило медленный рост памяти. Кроме того, количество потоков (управляемых контейнером приложения) было ограничено, поэтому эффективное количество дополнительных соединений находилось в допустимых пределах.

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

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

Почему это произошло?
Пулы потоков, управляемые контейнером, обычно имеют тайм-аут простоя, что означает, что потоки становятся бездействующими после определенного периода бездействия, и пул потоков, управляемый контейнером, отбрасывает их. Это очень правдоподобный сценарий на практике, когда трафик меняется в течение дня.
Однако даже несмотря на то, что потоки были отброшены, локальный поток, поддерживающий производителя шины сообщений (который сопоставляется с физическим соединением), все еще был открыт.
Это явно означало, что физические соединения будут накапливаться, поскольку пул потоков через свою фабрику потоков будет запрашивать новые потоки для обработки будущих запросов.
Для закрытия физических соединений на сервере потребуется гораздо больше времени (в основном они находятся в состоянии CLOSE_WAIT).
ThreadLocals НЕ дают возможности очистить ресурсы, и это может привести к очень серьезным негативным последствиям, подобным приведенному выше.
Урок
При использовании динамических пулов потоков, управляемых контейнером, следует избегать включения физических ресурсов в локальные потоки, поскольку отсутствие возможности очистки может оставить эти ресурсы в подвешенном состоянии, что может привести к исчерпанию квоты общих ресурсов.
Если вы используете это для кэширования любых таких подключений к облачным брокерам сообщений, базам данных, пересмотрите свой выбор.
Если вы не имеете полного контроля над своими пулами потоков и не готовы платить цену за поддержание незанятых потоков, лучше избегать использования ThreadLocal для кэширования дорогостоящих ресурсов.
Возможные исправления
Однако нам все еще нужно было решить эту проблему. Я представлю здесь два решения.
Продюсер на актера
Это решение (исправление), которое мы внедрили для нашего варианта использования благодаря любезности моего коллеги Mahendra Chhimwal, который предложил это.
В нашем приложении активно используется отличная модель Actor и ее достойная реализация Akka для Java/Scala.
Почему это работает?
- Актеры — это программные абстракции, в отличие от физических ресурсов, таких как потоки, которыми обычно управляют контейнеры приложений.
- Актеры занимают незначительный объем памяти и других ресурсов, поэтому поддержание такого пула актеров требует незначительных затрат.
- Внутреннее состояние актера не может быть доступно вне актера, что дает преимущество threadlocal.
- Используемый пул актеров полностью контролировался приложением и мог работать без каких-либо тайм-аутов простоя, что было основной причиной отказа подхода ThreadLocal.
Пул продюсеров
Приложения, которые не хотят использовать фреймворк Актера только для этого варианта использования, могут реализовать пул для своих дорогостоящих ресурсов. Обычно мы видим реализации пула для соединений HTTP, соединений с БД, соединений LDAP и так далее.
Если у вас нет готовой реализации пула, вы можете написать ее с минимальными усилиями, используя известную библиотеку Apache Commons Pool.
Размер пула может быть настроен вашим приложением (в зависимости от нагрузки), и он может гарантировать, что доступ к объединенному ресурсу будет осуществляться только одним потоком за раз, что позволяет избежать конфликтов между потоками.
Вывод
- ThreadLocals — отличная конструкция, доступная в стандартном JDK, которая позволяет хорошо масштабировать многопоточное приложение.
- Однако следует быть очень осторожным с ресурсами, управляемыми в threadlocal.
- Избегайте: объекты, которые сопоставляются с физическими ресурсами за пределами границ JVM (особенно для управляемых пулов потоков). Примеры таких ресурсов: соединения с БД, дескрипторы файлов, сетевые сокеты и т. д.
- Хорошие кандидаты: объекты контекста/сеанса, доступ к которым требуется в разных местах приложения, или любые объекты, которые безопасно иметь для каждого потока.
- ThreadLocals используется для правильных вариантов использования с правильными объектами, которые могут быть благом, тогда как они могут серьезно повлиять на ваше приложение или любую систему зависимостей серьезным образом.
Ресурсы
- АктерМодель вычислений
- Акка Фреймворк
- Общий пул Apache
Кредиты
Большое спасибо Mahendra Chhimmwal за помощь с диаграммами, необходимыми для этой статьи. Спасибо также Гарги Дасгупте за ценный отзыв.