Оптимізації в 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 $ в міс.
Проект опен сорс. Глянути можна тут .
Опубліковано: 12/07/17 @ 07:00
Розділ Різне
Рекомендуємо:
Огляд ІТ-ринку праці: Житомир
Управління проектами і задачами при роботі з інформаційними сайтами. Огляд сервісу Wrike для делегування розміщення статей
Scala дайджест #7: нова середа AI на основі Scala, популярність мов для DataScience, відео ScalaUA і ScalaDays
Як вставити відео з YouTube адаптивними
Люди vs машины: построить карьеру, чтобы выжить