Всем привет. Эта статья продолжение 10к на ядро с конкретными примерами оптимизаций, которые были проделаны для повышения производительности сервера. С написания первой части прошло уже 5 мес и за это время нагрузка на наш продакшн сервер выросла с 500 рек-сек до 2000 с пиками до 5000 рек-сек. Благодаря netty, мы даже не заметили это повышение (разве что место на диске уходит быстрее).
![Blynk load](https://habrastorage.org/files/ef4/316/e14/ef4316e14f434ed8b00d9c9e41403084.png)
(Не обращайте внимание на пики, это баги при деплое)
Эта статья будет полезна всем тем кто работает с netty или только начинает. Итак, поехали.
Одна из ключевых оптимизаций, которую стоит использовать всем — это подключение нативного Epoll транспорта вместо реализации на java. Тем более, что с netty это означает добавить лишь 1 зависимость:
и автозаменой по коду осуществить замену следующих классов:
Дело в том, что java реализация для работы с не блокирующими сокетами реализуется через класс Selector, который позволяет вам эффективно работать с множеством соединений, но его реализация на java не самая оптимальная. Сразу по трем причинам:
В моем конкретном случае я получил прирост производительности около 30%. Конечно же, эта оптимизация возможна только для Linux серверов.
Не знаю как на просторах СНГ, но ТАМ — безопасность ключевой фактор для любого проекта. “What about security?” — неминуемый вопрос, который Вам обязательно зададут, если заинтересуются Вашим проектом, системой, сервисом или продуктом.
В аутсорс мире, из которого я пришел, в команде всегда обычно был 1-2 DevOps на которых я всегда мог переложить данный вопрос. Например, вместо добавлять поддержку https, SSL/TLS на уровне приложения, всегда можно было попросить администраторов настроить nginx и с него уже прокидывать обычный http на свой сервер. И быстро и эффективно. Сегодня, когда я и швец и жнец и на дуде игрец — мне все приходится делать самому — заниматься разработкой, деплоить, мониторить. Поэтому подключить https на уровне приложения гораздо быстрее и проще чем разворачивать nginx.
Заставить openSSL работать с netty немного сложнее чем подключить нативный epoll транспорт. Вам понадобится подключить в проект новую зависимость:
Указать в качестве провайдера SSL — openSSL:
Добавить еще один обработчик в pipeline:
И наконец, собрать нативный код для работы с openSSL на сервере. Инструкция тут. По сути, весь процесс сводится к:
Для меня прирост производительности составил ~15%.
Полный пример можно глянуть тут и тут.
Очень часто приходится отправлять несколько сообщений в один и тот же сокет. Это может выглядеть так:
Этот код можно оптимизировать
Во втором случае при write нетти не будет сразу отсылать сообщение по сети, а обработав положит его в буфер (в случае если сообщение меньше буфера). Таким образом уменьшая количество системных вызовов для отправки данных по сети.
Как я уже писал в предыдущей статье — netty асинхронный фреймворк с малым количеством потоков обработчиков логики (обычно n core * 2). Поэтому каждый такой поток-обработчик должен выполнятся как можно быстрее. Любого рода синхронизация может этому помешать, особенно при нагрузках в десятки тысяч запросов в секунду.
С этой целью netty каждое новое соединение привязывает к одному и тому же обработчику (потоку) чтобы снизить необходимость кода для синхронизации. Например, если пользователь присоединился к серверу и выполняет некие действия — допустим, изменяет состояние модели, которая связана только с ним, то никакой синхронизации и volatile не нужно. Все сообщения этого пользователя будут обрабатываться одним и тем же потоком. Это отлично и работает для части проектов.
Но что, если состояние может изменятся из нескольких соединений, которые вероятней всего будут привязаны к разным потокам? Например, для случая, когда мы делаем игровую комнату и команда от пользователя должна менять окружающий мир?
Для этого в netty существует метод register, который позволяет перепривязать соединение из одного обработчика к другому.
Этот подход позволяет обрабатывать события для одной игровой комнаты в одном потоке и полностью избавится от сихронизаций и volatile для изменения состояния этой комнаты.
Пример перепривязки на логин в моем коде тут и тут.
Netty довольно часто выбирают для серверного решения, так как сервера должны поддерживать работу разных протоколов. Например, мое скромное IoT облако поддерживает HTTP/S, WebSockets, SSL/TCP сокеты для разного hardware и собственного бинарного протокола. Это значит, что для каждого из этих протоколов должен быть IO поток (boss group) и потоки обработчики логики (work group). Обычно создание нескольких таких обработчиков выглядит так:
Но в случае netty чем меньше лишних потоков вы создаете, тем больше вероятность создать более производительное приложение. К счастью, в netty EventLoop можно переиспользовать:
Ни для кого уже не секрет, что для высоконагруженных приложений одним из узких мест является сборщик мусора. Netty быстра, в том числе, как раз за счет повсеместного использования памяти вне java heap. У netty есть даже своя экосистема вокруг off-heap буферов и система обнаружения утечек памяти. Так можете поступить и Вы. Например:
изменить на
В этом случае, правда, Вы должны быть уверены, что один их обработчиков в pipeline освободит этот буфер. Это не значит, что вы должны сразу же бежать и изменять свой код, но про такую возможность оптимизиции Вы должны знать. Несмотря на более сложный код и возможность получить утечку памяти. Для горячих методов это может идеальным решением.
Надеюсь эти простые советы позволят Вам ускорить ваше приложение.
Напомню, что мой проект open-source. Поэтому если Вам интересно как эти оптимизации выглядят в существующем коде — смотрите тут.
![Blynk load](https://habrastorage.org/files/ef4/316/e14/ef4316e14f434ed8b00d9c9e41403084.png)
(Не обращайте внимание на пики, это баги при деплое)
Эта статья будет полезна всем тем кто работает с 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. Поэтому если Вам интересно как эти оптимизации выглядят в существующем коде — смотрите тут.
ruslanys
Спасибо за статью!
Расскажите, пожалуйста, а как у вас в проекте реализован протокол общения между устройством и сервером?
Я некоторое время назад тоже писал сервер на Netty для IoT и воодушевившись этой статьей реализовал примерно так:
doom369
У меня нечто похожее. Pipeline. Decoder. Encoder.
А вообще рекомендую Вам просто скачать исходники и проглянуть в IDE. Так будет проще.
ruslanys
Даже не знал, что можно посмотреть исходники! Спасибо!!
doom369
Не за что. Если возникнут вопросы — буду рад ответить.