Разработка
Сколько потоков использует ваш сетевой клиент?
Выбор лучшего сетевого клиента — непростая задача, здесь я представил только один аспект этого решения: сколько потоков использует сетевой клиент.
Одна из популярных причин использования корутин в Kotlin — ограничение количества потоков, используемых в приложении. Каждый поток — это затраты, особенно на память, но также и на внимание процессора. Мы вводим корутины, чтобы ограничить количество потоков, используемых в наших приложениях. В конце концов, с помощью корутинов мы можем добиться параллелизма без лишних потоков. Однако наши усилия часто срываются из-за используемых библиотек. В этой статье я расскажу, сколько потоков используют самые популярные сетевые клиенты, и как вы можете проверить это сами.
В чем проблема?
Давайте начнем с понимания проблемы и того, что было бы идеальным решением. Представьте, что вы реализуете простое приложение, которое служит шлюзом. Для каждого полученного запроса оно делает запрос к другому сервису. Это распространенная ситуация, особенно в архитектуре микросервисов. Если вы будете запускать каждый запрос в новом потоке и блокировать этот поток до получения ответа, то в итоге у вас будет много активных потоков. Это проблема, потому что каждый поток потребляет память. Объем памяти обычно составляет около 1 МБ ОЗУ на поток. Теперь представьте, что у вас 10,000 запросов в секунду, и каждый запрос занимает 100 мс, это означает, что вам нужно в среднем 1,000 потоков просто для ожидания ответов. Это если предположить идеальный случай, когда количество запросов постоянно, а что если в один прекрасный момент нагрузка будет исключительно высокой? Заметьте, что если у вас недостаточно оперативной памяти, ваше приложение получит исключение, и оно упадет (если только вы принудительно не ограничите размер пула, но тогда у вас будет узкое место).
В Spring Boot приложении, если запросы выполняются в потоках, то они блокируются при ожидании ответа от сети.
Одним из способов решения этой проблемы является использование корутинов. Корутины — это процессы, которые могут быть приостановлены, и приостановленная корутина не потребляет никакой памяти (у нее есть только ссылки на локальные переменные, необходимые для возобновления работы этой корутины). Поэтому, когда вы используете Ktor или Spring Boot с поддержкой корутин, каждый запрос запускается в корутине, которая является легковесной, и мы ожидаем, что когда мы сделаем сетевой запрос, эта корутина будет приостановлена, и не будет потреблять никакой памяти.
В Ktor Client или приложении Spring Boot, где запросы выполняются в короутинах, они приостанавливались при ожидании ответа из сети.
Это означало бы, что мы избавимся от затрат на неиспользуемые потоки. Это прекрасно, но есть одна загвоздка: не все библиотеки работают так, как ожидается. Многие библиотеки приостанавливают выполнение корутинов, но под капотом держат диспетчер с пулом потоков, которые они блокируют в ожидании ответа. Это означает, что преимущество использования корутинов практически теряется. Хорошим примером такой библиотеки является Retrofit.
Ktor Client или приложение Spring Boot, в котором запросы выполняются в корутинах, которые приостанавливаются при ожидании ответа от сети, но где используется сетевой клиент (например, Retrofit), который блокирует потоки под капотом.
Почему такие библиотеки ведут себя подобным образом? Причина в том, что они построены поверх библиотек или старых Java API, которые не поддерживают приостановку операций. Retrofit построен поверх OkHttp, который построен поверх старого Java API, предоставляющего блокирующие функции. Это означает, что Retrofit может приостанавливать корутины, но под капотом он должен блокировать потоки. Количество заблокированных потоков равно количеству активных сетевых запросов. Это означает, что корутины все еще могут быть полезны для данного приложения (количество активных сетевых запросов часто ограничено), но это не так полезно, как мы могли бы ожидать.
Итак, без лишних слов, давайте проверим, сколько потоков используют самые популярные сетевые клиенты. Затем я объясню свою методологию и оставлю вас с размышлениями о том, как выбрать сетевой клиент для вашего приложения.
Сравнение популярных сетевых клиентов
Я сравню три популярных сетевых клиента Kotlin: Retrofit, Ktor Client и Fuel. Ktor Client позволяет вам выбрать движок, который вы хотите использовать для выполнения сетевых запросов. Я сравню все доступные JVM-движки, которые я мог запустить без Android, и где я мог установить пользовательский лимит активных соединений.
Самый важный вывод из моих экспериментов — Retrofit, Fuel и Ktor Client с движком OkHttp блокируют столько потоков под капотом, сколько существует активных запросов. Все остальные движки Ktor Client используют лишь несколько потоков для обслуживания.
Вот примерный результат моих экспериментов, где измерение проводилось для 100 запросов, каждый из которых ожидал ответа 2 секунды, и каждый клиент был настроен на обработку 100 запросов в одно и то же время:
Client | Number of active threads | Execution time |
---|---|---|
Retrofit | 101 | 2264 +/- 300 |
Ktor OkHttp | 101 | 2317 +/- 300 |
Ktor Apache | 5 | 2215 +/- 300 |
Ktor Apache5 | 4 | 2198 +/- 300 |
Ktor Java | 1 | 2218 +/- 300 |
Ktor CIO | 1 | 2242 +/- 300 |
Fuel | 101 | 2312 +/- 300 |
+- 300 добавлено, чтобы уточнить, что время не является точным, я добавил это, чтобы напомнить вам, что не стоит смотреть на эти цифры с излишней точностью, вы можете предположить, что все они занимают около 2 секунд на выполнение, но точное число может отличаться. В тестах выполнялись реальные сетевые запросы, и время ответа могло меняться.
Если взглянуть на результаты, то становится ясно, что Retrofit, Ktor с движком OkHttp и Fuel берут гораздо больше потоков, то есть всегда количество активных соединений плюс 1. Ktor с движками Apache, Apache5, Java и CIO берет постоянное количество потоков, независимо от количества активных запросов. Это означает, что они хорошо реализованы. Количество потоков не может быть равно 0, так как должен быть поток, ожидающий ответов, но в Ktor CIO и Ktor Java он единственный, который используется.
Глядя на эти результаты, CIO может показаться идеальным движком. Однако я заметил, что его эффективность падает при увеличении числа активных соединений. Также Ktor Apache иногда имеет гораздо большее время выполнения. Я не знаю точно, почему так происходит, но я сообщил об этом как о проблеме. Для других движков разница во времени выполнения невелика. Это результаты для 1000 запросов и времени отклика 2 секунды:
Client | Number of active threads (500 requests) | Execution time (average) |
---|---|---|
Retrofit | 501 | 2619 +/- 300 |
Ktor OkHttp | 501 | 2425 +/- 300 |
Ktor Apache | 5 | 4725 (3000 — 10000) |
Ktor Apache5 | 4 | 2624 +/- 300 |
Ktor Java | 1 | 2511 +/- 300 |
Ktor CIO | 1 | 11118 +/- 300 |
Fuel | 501 | 2585 +/- 300 |
Какой клиент выбрать?
После всех этих экспериментов я считаю, что наиболее эффективным является клиент Ktor с движком Java или Apache5. У них также очень много фич и кажутся отличными вариантами для большинства приложений.
Однако в экспериментах я проверял конкретную ситуацию, когда мы делаем большое количество запросов, и каждый из них задерживается. Эта ситуация хорошо подходит для некоторых бэкэнд-приложений, но не для всех.
Такая ситуация не очень хорошо соотносится с приложениями, которые делают небольшое количество запросов, но каждый из них должен получить много данных. Она также не очень хорошо описывает ситуацию, когда запросов много, но каждый из них очень быстрый (например, когда они общаются через localhost или по локальной сети). В таких ситуациях возможно, что Retrofit превосходит Ktor с движками Java или Apache 5. Чтобы проверить это, нам нужно построить другой эксперимент. Если вы хотите, чтобы я сделал доклад, в котором были бы рассмотрены все аспекты сетевых клиентов, дайте мне знать об этом в Twitter или в комментариях в статье.
Кроме того, во многих приложениях мы уважаем удобство разработчиков больше, чем производительность. В таких случаях Retrofit может быть предпочтительнее, если разработчики уже знакомы с ним, и он достаточно удобен в использовании.
Выбор лучшего сетевого клиента — непростая задача, здесь я представил только один аспект этого решения: сколько потоков использует сетевой клиент.
Полный код проекта можно найти в этом проекте: https://github.com/MarcinMoskala/NetworkClientTests
Весь код теста тут: https://github.com/MarcinMoskala/NetworkClientTests/blob/master/src/main/kotlin/Test.kt
Конфигурация клиента: https://github.com/MarcinMoskala/NetworkClientTests/blob/master/src/main/kotlin/Clients.kt
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.11
-
Новости2 дня назад
Видео и подкасты о мобильной разработке 2025.14
-
Видео и подкасты для разработчиков2 недели назад
Javascript для бэкенда – отличная идея: Node.js, NPM, Typescript
-
Новости2 недели назад
Видео и подкасты о мобильной разработке 2025.12