Статьи

Принципы обработки тысяч соединений в Java с помощью Netty

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

В сегодняшней статье я собираюсь обсудить общие принципы, которым необходимо следовать, если мы хотим создать масштабируемое приложение, которое может обрабатывать тысячи соединений. Я буду ссылаться на Netty Framework, TCP и внутренние компоненты Socket и некоторые полезные инструменты, если вы хотите получить представление о вашем приложении и базовой системе.


Вам также может понравиться:
Модели программирования для серверов на Java

Отказ от ответственности : я буду немного педантичен с точки зрения памяти, потому что если мы хотим умножить наш поток обработки на количество соединений (10 000>), то каждый килобайт в одном конвейере обработки соединений становится очень дорогим 🙂

Принцип 1. Сделайте ваше приложение подходящим для проблемы C10K

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

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

Иногда не очень легко определить это поведение в вашем стеке приложений. Иногда вам нужно будет переупорядочить свои приложения / код, добавить дополнительную внешнюю очередь (RabbitMQ) или тему (Kafka), чтобы изменить распределенную систему, чтобы иметь возможность буферизовать задачи и разделять неблокирующий код от код, который не может быть легко переопределен с использованием неблокирующих методов (например, использование драйвера JDBC, в настоящее время нет официального неблокирующего драйвера для реляционных баз данных — насколько я знаю, работа над ADBC ​​- API асинхронного доступа к базам данных — уже была остановлен).

Однако, по моему опыту, стоит переписать мой код и сделать его более неблокирующим по следующим причинам:

  • Я разделил свое приложение на два разных приложения, которые, скорее всего, не используют одни и те же стратегии развертывания и разработки, даже если они совместно используют один и тот же «домен» (например, одна часть приложения является конечной точкой REST, которая может быть реализована с использованием потокового пула). HTTP-сервер на основе и вторая часть является потребителем из очереди / темы, которая записывает что-то в БД с использованием неблокирующего драйвера).

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

Что может означать, что мы используем надлежащие инструменты для такого рода приложений:

  • Мы сохраняем количество потоков как можно ниже. Не забудьте проверить не только потоки вашего сервера, но и другие части вашего приложения: потребитель очереди / темы, настройки драйвера БД, настройки ведения журнала (с асинхронным микропакетированием). Всегда делайте дамп потока, чтобы увидеть, какие потоки созданы в вашем приложении и сколько (не забудьте сделать это под нагрузкой, иначе ваши пулы потоков не будут полностью инициализированы, многие из них создают ленивые потоки). Я всегда называю свои собственные потоки из пула потоков (гораздо проще найти жертву и отладить ваш код).

  • Помните о блокировке вызовов HTTP / DB для других служб, мы можем использовать реактивные клиенты, которые автоматически регистрируют обратный вызов для входящего ответа. Подумайте об использовании протокола, который больше подходит для связи сервис-2, например, RSocket.

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

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

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

Принцип 2: Кэшированные соединения, а не потоки

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

Это не значит, что TCP-соединение абсолютно бесплатно. Наиболее важной частью является  TCP рукопожатие . Поэтому вы всегда должны использовать постоянное соединение ( keep-alive ). Если бы мы использовали одно TCP-соединение только для отправки одного сообщения, мы заплатили бы за 8 сегментов TCP (соединение и закрытие соединения = 7 сегментов).

Принятие нового соединения TCP

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

На картинке выше мы видим, что резервы SYN и LISTEN . В  журнале SYN Backlog  мы можем найти соединения, которые просто ожидают подтверждения с помощью TCP Handshake . Однако в LISTEN  Backlog  у нас уже есть полностью инициализированные соединения, даже с буферами отправки и получения TCP , которые только и ждут, чтобы быть принятыми нашим приложением.

Прочитайте SYN Flood DDoS Attack,  если вы хотите узнать больше, почему нам на самом деле нужны две резервные копии. 

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


Джава