Оптимизации в Netty. 10 советов по улучшению производительности

Всем привет. Вот уже третий год работаю с Netty. За 3 года узнал очень многое, даже начал контрибьютить и хочу поделиться советами по тюнингу, так как у себя в проекте я проделывал это довольно часто.

Нетти в топе бенчмарков

Итак, поехали.

1. Нативный epoll транспорт для Linux

Первая и самая мощная оптимизация — это переключение на нативный epoll транспорт под Linux вместо Java реализации. В нетти сделать это довольно просто — достаточно лишь добавить одну зависимость в проект:

<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

В нашем случае мы получили прирост в 30 % сразу после переключения. Детали.

2. Нативный OpenSSL

Безопасность — ключевой фактор для любого коммерческого проекта. Поэтому все, так или иначе, у себя в проектах используют https, ssl/tls. Раньше в java.security пакете все было плохо и, что самое главное, медленно (да и сейчас не намного лучше). Поэтому классический сетап продакшн сервера в яве часто включал в себя nginx, который обрабатывает ssl/tls и отдает дешифрованный трафик уже в конечные приложения. С нетти этого делать не нужно. Так как в нетти есть готовые биндинги на нативные OpenSSL либы.

Более того, нетти предлагает несколько разных реализаций этих биндингов. Мы, например, используем биндинги на boringssl — форк OpenSSL, который был оптимизирован командой из гугла для лучшей производительности.

Для подключения нужно добавить 1 зависимость:

<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-tcnative-boringssl-static</artifactId>
            <version>${netty.boring.ssl.version}</version>
            <classifier>${epoll.os}</classifier>
        </dependency>

Указать в качестве провайдера SSL — OpenSSL:

return SslContextBuilder.forServer(serverCert, serverKey, serverPass)
                .sslProvider(SslProvider.OPENSSL)
                .build();

Добавить еще один обработчик в pipeline, если еще не добавили:

new SslHandler(engine)

Для нас прирост производительности составил ~15%. Детали.

3. Экономим на системных вызовах

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

for (PinState pinState : pinStates) {
    ctx.writeAndFlush(pinState);
}

Этот код можно оптимизировать:

for (PinState pinState : pinStates) {
    ctx.write(pinState);
}
ctx.flush();

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

4. Алоцируем меньше с помощью ByteBuf

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

ctx.writeAndFlush(
    new ResponseMessage(messageId, OK)
);

можно ускорить, избежав создания ява объекта и создав буфер вручную без необходимости передавать объект ResponseMessage в конкретный обработчик дальше в пайплайне. Например, так:

ByteBuf buf = ctx.alloc().buffer(3); //direct pooled buffers
buf.writeByte(messageId);
buf.writeShort(OK);
ctx.writeAndFlush(buf);

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

Pooled Direct Buffer очень оправданы, хоть и увеличивают сложность

5. Переиспользуем ByteBuf

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

for (Channel ch : targets) {
   ch.writeAndFlush(hardwareState);
}

Проблема тут в том, что сообщение hardwareState будет обработано в пайплайне для каждого из сокетов. Это можно оптимизировать, создав массив байтов для отправки 1 раз:

ByteBuf msg = makeResponse(hardwareState);
msg.retain(targets.size() - 1);
for (Channel ch : targets) {
   ch.writeAndFlush(msg);
   msg.resetReaderIndex();
}

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

6. ChannelPromise

Так как нетти асинхронна и реактивна, каждая операция записи в сокет возвращает Future. В нетти это специальный расширенный класс — ChannelPromise. Всегда, когда вы используете:

ctx.writeAndFlush(
    response
);

внутри неявно создается новый DefaultChannelPromise. Если результат записи вам не нужен, этого можно избежать, передав существующий объект VoidChannelPromise:

ctx.writeAndFlush(
    response, ctx.voidPromise()
);

Экономя таким образом на создании лишнего объекта, который мы и не используем.

7. @Sharable

Многие хендлеры в нетти не хранят никакого состояния. Такие хендлеры обычно помечены через аннотацию @Sharable. Это означает, что вместо постоянного создания таких хендлеров для каждого соединения:

void initChannel(SocketChannel ch) {
    ch.pipeline().addLast(new HttpServerCodec());
    ch.pipeline().addLast(new SharableHandler());
}

вы можете переиспользовать один и тот же объект (как синглтон):

SharableHandler sharableHandler = new SharableHandler();
...
void initChannel(SocketChannel ch) {
    ch.pipeline().addLast(new HttpServerCodec());
    ch.pipeline().addLast(sharableHandler);
}

Это может быть особенно критично для не keep-alive соединений.

8. Используем контекст

Сразу рассмотрим небольшой пример «плохого» кода:

ctx.channel().writeAndFlush(msg);

Его недостаток в том, что у вас есть в наличии контекст, а значит, вы можете выполнить:

ctx.writeAndFlush(msg);

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

9. Отключаем Leak Detection

Не все знают, но нетти ВСЕГДА, по умолчанию, использует дополнительные счетчики ссылок на объекты байт буферов (так как в нетти довольно легко выстрелить в ногу и написать код, который течет). Эти счетчики не бесплатны, поэтому для продакшн систем их желательно отключать в коде:

ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.DISABLED);

или через среду переменных:

-Dio.netty.leakDetection.level=DISABLED

10. Переиспользуем пулы событий

Если у вас IoT-проект, это значит, что вы должны поддерживать много разные протоколов. И у вас почти наверняка будет такой код:

new ServerBootstrap().group(
    new EpollEventLoopGroup(1), 
    new EpollEventLoopGroup()
).bind(80);
new ServerBootstrap().group(
    new EpollEventLoopGroup(1), 
    new EpollEventLoopGroup()
).bind(443);

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

EventLoopGroup boss = new EpollEventLoopGroup(1);
EventLoopGroup workers = new EpollEventLoopGroup();
new ServerBootstrap().group(
boss, 
   workers
).bind(80);
new ServerBootstrap().group(
boss, 
   workers
).bind(443);

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

О проекте

Наш проект Blynk — IoT-платформа с мобильными приложениями. Текущая нагрузка на систему 11000 рек-сек. 5000 девайсов постоянно в сети. Всего периодически подключается около 40K девайсов. Вся система обходится в 60 $ в мес.

Проект опен сорс. Глянуть можно тут.

Похожие статьи:
Компания Samsung продолжает развивать линейку собственных чипсетов Exynos, которые ложатся в основу флагманских аппаратов и моделей средней...
Виконавча директорка асоціації IT Ukraine Марія Шевчук в інтерв’ю для Економічної Правди розповіла про виклики, з якими зараз стикається...
В выпуске: Отчет о конференции @Scale 2016, анонс dotScale и dotSecurity 2016, несколько хороших релизов. Статьи SQL Server можно теперь устанавливать...
Видання DW записало розмову з Михайлом Федоровим незадовго до того, як Верховна Рада cпочатку тимчасово звільнила його з уряду,...
Що таке спеціалізація для ІТ-компанії? Як це — працювати в аутсорсингу в конкретній ніші? Я Ігор Цинман, співзасновник...
Яндекс.Метрика