
(Не обращайте внимание на пики, это баги при деплое)
Эта статья будет полезна всем тем кто работает с netty или только начинает. Итак, поехали.
Нативный Epoll транспорт для Linux
Одна из ключевых оптимизаций, которую стоит использовать всем — это подключение нативного Epoll транспорта вместо реализации на java. Тем более, что с netty это означает добавить лишь 1 зависимость:
<dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-epoll</artifactId> <version>${netty.version}</version> <classifier>linux-x86_64</classifier> </dependency>
и автозаменой по коду осуществить замену следующих классов:
- NioEventLoopGroup → EpollEventLoopGroup
- NioEventLoop → EpollEventLoop
- NioServerSocketChannel → EpollServerSocketChannel
- NioSocketChannel → EpollSocketChannel
Дело в том, что java реализация для работы с не блокирующими сокетами реализуется через класс Selector, который позволяет вам эффективно работать с множеством соединений, но его реализация на java не самая оптимальная. Сразу по трем причинам:
- Метод selectedKeys() на каждый вызов создает новый HashSet
- Итерация по этому множеству создает iterator
- И ко всему прочему внутри метода selectedKeys() огромное количество блоков синхронизации
В моем конкретном случае я получил прирост производительности около 30%. Конечно же, эта оптимизация возможна только для Linux серверов.
Нативный OpenSSL
Не знаю как на просторах СНГ, но ТАМ — безопасность ключевой фактор для любого проекта. “What about security?” — неминуемый вопрос, который Вам обязательно зададут, если заинтересуются Вашим проектом, системой, сервисом или продуктом.
В аутсорс мире, из которого я пришел, в команде всегда обычно был 1-2 DevOps на которых я всегда мог переложить данный вопрос. Например, вместо добавлять поддержку https, SSL/TLS на уровне приложения, всегда можно было попросить администраторов настроить nginx и с него уже прокидывать обычный http на свой сервер. И быстро и эффективно. Сегодня, когда я и швец и жнец и на дуде игрец — мне все приходится делать самому — заниматься разработкой, деплоить, мониторить. Поэтому подключить https на уровне приложения гораздо быстрее и проще чем разворачивать nginx.
Заставить openSSL работать с netty немного сложнее чем подключить нативный epoll транспорт. Вам понадобится подключить в проект новую зависимость:
<dependency> <groupId>io.netty</groupId> <artifactId>netty-tcnative</artifactId> <version>${netty.tcnative.version}</version> <classifier>linux-x86_64</classifier> </dependency>
Указать в качестве провайдера SSL — openSSL:
return SslContextBuilder.forServer(serverCert, serverKey, serverPass) .sslProvider(SslProvider.OPENSSL) .build();
Добавить еще один обработчик в pipeline:
new SslHandler(engine)
И наконец, собрать нативный код для работы с openSSL на сервере. Инструкция тут. По сути, весь процесс сводится к:
- Выкачать исходники
- mvn clean install
Для меня прирост производительности составил ~15%.
Полный пример можно глянуть тут и тут.
Экономим на системных вызовах
Очень часто приходится отправлять несколько сообщений в один и тот же сокет. Это может выглядеть так:
for (Message msg : messages) { ctx.writeAndFlush(msg); }
Этот код можно оптимизировать
for (Message msg : messages) { ctx.write(msg); } ctx.flush();
Во втором случае при write нетти не будет сразу отсылать сообщение по сети, а обработав положит его в буфер (в случае если сообщение меньше буфера). Таким образом уменьшая количество системных вызовов для отправки данных по сети.
Лучшая синхронизация — отсутствие синхронизации.
Как я уже писал в предыдущей статье — netty асинхронный фреймворк с малым количеством потоков обработчиков логики (обычно n core * 2). Поэтому каждый такой поток-обработчик должен выполнятся как можно быстрее. Любого рода синхронизация может этому помешать, особенно при нагрузках в десятки тысяч запросов в секунду.
С этой целью netty каждое новое соединение привязывает к одному и тому же обработчику (потоку) чтобы снизить необходимость кода для синхронизации. Например, если пользователь присоединился к серверу и выполняет некие действия — допустим, изменяет состояние модели, которая связана только с ним, то никакой синхронизации и volatile не нужно. Все сообщения этого пользователя будут обрабатываться одним и тем же потоком. Это отлично и работает для части проектов.
Но что, если состояние может изменятся из нескольких соединений, которые вероятней всего будут привязаны к разным потокам? Например, для случая, когда мы делаем игровую комнату и команда от пользователя должна менять окружающий мир?
Для этого в netty существует метод register, который позволяет перепривязать соединение из одного обработчика к другому.
ChannelFuture cf = ctx.deregister(); cf.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture channelFuture) throws Exception { targetEventLoop.register(channelFuture.channel()).addListener(completeHandler); } });
Этот подход позволяет обрабатывать события для одной игровой комнаты в одном потоке и полностью избавится от сихронизаций и volatile для изменения состояния этой комнаты.
Пример перепривязки на логин в моем коде тут и тут.
Переиспользуем EventLoop
Netty довольно часто выбирают для серверного решения, так как сервера должны поддерживать работу разных протоколов. Например, мое скромное IoT облако поддерживает HTTP/S, WebSockets, SSL/TCP сокеты для разного hardware и собственного бинарного протокола. Это значит, что для каждого из этих протоколов должен быть IO поток (boss group) и потоки обработчики логики (work group). Обычно создание нескольких таких обработчиков выглядит так:
//http server new ServerBootstrap().group(new EpollEventLoopGroup(1), new EpollEventLoopGroup(workerThreads)) .channel(channelClass) .childHandler(getHTTPChannelInitializer(()) .bind(80); //https server new ServerBootstrap().group(new EpollEventLoopGroup(1), new EpollEventLoopGroup(workerThreads)) .channel(channelClass) .childHandler(getHTTPSChannelInitializer(()) .bind(443);
Но в случае netty чем меньше лишних потоков вы создаете, тем больше вероятность создать более производительное приложение. К счастью, в netty EventLoop можно переиспользовать:
EventLoopGroup boss = new EpollEventLoopGroup(1); EventLoopGroup workers = new EpollEventLoopGroup(workerThreads); //http server new ServerBootstrap().group(boss, workers) .channel(channelClass) .childHandler(getHTTPChannelInitializer(()) .bind(80); //https server new ServerBootstrap().group(boss, workers) .channel(channelClass) .childHandler(getHTTPSChannelInitializer(()) .bind(443);
Off-heap сообщения
Ни для кого уже не секрет, что для высоконагруженных приложений одним из узких мест является сборщик мусора. Netty быстра, в том числе, как раз за счет повсеместного использования памяти вне java heap. У netty есть даже своя экосистема вокруг off-heap буферов и система обнаружения утечек памяти. Так можете поступить и Вы. Например:
ctx.writeAndFlush(new ResponseMessage(messageId, OK, 0));
изменить на
ByteBuf buf = ctx.alloc().directBuffer(5); buf.writeByte(messageId); buf.writeShort(OK); buf.writeShort(0); ctx.writeAndFlush(buf); //buf.release();
В этом случае, правда, Вы должны быть уверены, что один их обработчиков в pipeline освободит этот буфер. Это не значит, что вы должны сразу же бежать и изменять свой код, но про такую возможность оптимизиции Вы должны знать. Несмотря на более сложный код и возможность получить утечку памяти. Для горячих методов это может идеальным решением.
Надеюсь эти простые советы позволят Вам ускорить ваше приложение.
Напомню, что мой проект open-source. Поэтому если Вам интересно как эти оптимизации выглядят в существующем коде — смотрите тут.

