Оптимізації в 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>

і автозаміну за кодом здійснити заміну наступних класів:

В нашому випадку ми отримали приріст у 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 $ в міс.

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

Опубліковано: 12/07/17 @ 07:00
Розділ Різне

Рекомендуємо:

Огляд ІТ-ринку праці: Житомир
Управління проектами і задачами при роботі з інформаційними сайтами. Огляд сервісу Wrike для делегування розміщення статей
Scala дайджест #7: нова середа AI на основі Scala, популярність мов для DataScience, відео ScalaUA і ScalaDays
Як вставити відео з YouTube адаптивними
Люди vs машины: построить карьеру, чтобы выжить