Всем привет. Этот пост о серверном решении для интернета вещей, который я написал на асинхронных сокетах с использованием всем известной Netty. Я расскажу о задаче, которую мы ставили перед собой, о том почему я выбрал Netty, почему у нее нету альтернатив, какие у нетти недостатки и преимущества и как можно выжать максимум. Сейчас наш сервер в среднем обрабатывает 1.5 млрд сообщений в месяц и нагрузка с каждым месяцем растет на 20%. Для привлечения внимания — нагрузка на один продакшн сервер с 4-мя ядрами Xeon CPU E5-2630L v2 @ 2.40GHz при лоаде в 500 рек-сек.

Blynk load - для привлечения внимания

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

Все началось около 2-х лет назад, когда мне подарили arduino. Я всегда мечтал сделать какое-то интересное устройство своими руками. Но все эти паяльники, резисторы, вольты-амперы меня постоянно отпугивали. Так было, пока не появились arduino. С ардуиной я смог наконец-то управлять электроникой. Сказать, что это было очень круто — не сказать ничего. Я был счастлив. Но, как это часто бывает, после освоения базовых навыков в микроконтроллерах, захотелось большего — управлять устройствами через интернет с телефона. Быстрый гуглинг показал (дело было 2 года назад), что на текущий момент нет ни одного решения, которое бы решало эту задачу. Не считая IoT облака с HTTP API, которые было не очень удобно использовать.

К счастью, я не был один. Совершенно случайно, на своей работе, я познакомился с людьми, которых волновали те же проблемы. Так появился наш проект.

Проблема


Отдаленно задача была ясна — управлять ардуиной через интернет с мобильного или планшета. Ну например, чтобы иметь возможность менять цвет юбки ниже.


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

Много параллельных соединений


Я не считаю себя гиком. Но уже тогда у меня было 2 ардуинки, 2 телефона и планшет, которые я бы хотел использовать для управления. Небольшой опрос среди знакомых показал, что у среднего хардварщика 3-4 микроконтроллера. Стало вполне очевидно, что один потенциальный пользователь нашей системы с легкостью может открывать 5-10 соединений с разных устройств. То есть сервер должен одновременно обрабатывать тысячи параллельных соединений даже для нескольких сотен пользователей.

Pushing / Polling


Не смотря на свой небольшой опыт работы с микроконтроллерами, было очевидно что серверное решение должно поддерживать 2 режима работы клиентов — pushing и polling. То есть мобильный клиент должен иметь возможность запросить с железки состояние и железка со своей стороны должна иметь возможность в любой момент послать сообщение, а мобильный клиент его получить.

Поддержка разных протоколов


Выбрать протокол было трудно. Очень хотелось пойти старой протоптанной дорожкой — HTTP. Но у HTTP есть несколько архитектурных проблем. Минимально возможный пакет — 26 байт (без учета TCP/IP заголовков), но в реальной жизни минимальный пакет будет не меньше 50 байт, а учитывая слабые возможности микроконтроллеров (у Arduino UNO например 2кб RAM и 16 MHz процессора) и недолгую жизнь элементов питания это показалось излишним. Ну и если с полингом у http все хорошо, то с пушингом нужны уже web-scokets, а это по сути старый добрый TCP/IP. В среде хардварщиков очень популярен протокол MQTT. Но от него я тоже отказался. Он показался мне слишком мудренным. Да, его продумывали на все случаи жизни. Но тогда нам это было не нужно. Поэтому сервер должен был поддерживать как минимум несколько протоколов, чтобы мы могли переключится на нужный в случае, если бы наш выбор оказался не верным.

Простота


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

Выбираем решение


Немного поразмыслив, было принято решение об использовании асинхронных сокетов. Я руководствовался 2-мя факторами. Нужно обрабатывать много параллельных соединений, с чем блокирующие сокеты не очень хорошо справляются в виду большого стека (256кб) в яве и затрат на переключение контекста между потоками. Бизнес логики будет немного. Ведь всего лишь нужно переслать сообщение с одного сокета на другой (ну кто не ошибается). Все работа сервера по сути сводится к «взять сообщение из сокета А» и «послать сообщение в сокет Б» + немного бизнес логики сверху.

Что на рынке?


Быстрый анализ существующих решений показал следующее:

  • Apache MINA — мертвый проект, который не развивается уже года 4.
  • Grizzly — Я его отбросил так как я не нашел проектов, которые его используют.
  • Xnio — отбросил по причине 60-ти звезд на гитхабе. И те, судя по всему разработчики.
  • Vert.x — на тот момент позиционировался как прямой конкурент Netty. И тогда у него было то ли закрытое то ли платное ядро. Дело было 2 года назад и я уже подзабыл детали. Сейчас ситуация поменялась. Так что может имеет смысл опять присмотреться.
  • Akka — очень интересная штука. И вполне годное решение, отдал предпочтение netty по простой причине — на netty построить Akka можно, а на Akke netty — нет. Ну и сам факт того что распределенные акторы в Акке используют нетти, как бы намекает. Ну и netty используют тысячи проектов по всему миру включая фейсбук, линкедин и твиттер.
  • Netty — выбор очевиден



Недостатки Netty


Вот уже как 2 года я работаю с нетти. И мне есть что сказать. Начну с минусов.

Плохая документация


Есть отличный getting started и вроде как все хорошо. Но как только начинаешь делать шаг влево, шаг вправо, сразу возникают десятки вопросов. Ответов на которые в документации нету и ответы на которые разбросаны по всему интернету. Сейчас ситуация гораздо лучше, чем была 2 года назад. Но проблема все еще остается.

Сложность


Как асинхронный фреймворк — нетти упрощает работу с асинхронными сокетами. Но все-таки с наскоку сразу перейти к бизнес логике не получится. Нужно изучить, понять и переосмыслить много моментов. Возможно в моем случаем это был результат работы многих лет с блокирующим АПИ. Не знаю. Но я до сих узнаю что-то новое о нетти.

Слишком много абстракций


Часто во время работы с нетти возникает необходимость заглянуть внутрь готовых обработчиков, чтобы понять как они работают. Но довольно большая и сложная иерархия наследования очень сильно усложняет понимание кода. Не знаю как у вас, но в случае если у класса иерархия больше 3-х уровней вложенности, то у меня переполняется мой и так не большой стек. Нетти вся пропитана этой сложной иерархией. В дополнение ко все всему у нетти есть свой отдельный пакет с 4-мя разными типами буферов, которые использует сама нетти.

Легко выстрелить в ногу


Поначалу с нетти очень просто выстрелить себе в ногу. Небольшой пример:

ByteBuf in ...
in.readBytes(length);  // утечка памяти

ByteBuf in ...
in.readSlice(length);  // все ок

Оказывается в 4-й netty (я начинал с 3-й) по дефолту используются direct буфера, поэтому за буферами надо хорошенько следить. Проблема утечек в netty вообще стоит очень остро. Как подтверждение — в нетти есть решение которое позволяет отслеживать утечки внутри фреймворка. Хотя если вы будете использовать готовые хендлеры (то есть используете готовые протоколы HTTP, MQTT, SPDY, ...), то Вы с этим не столкнетесь.

До сих пор есть баги


Благодаря своей популярности, в нетти довольно часто находят разного рода баги. Сейчас на гитхабе открыто около 280 тикетов. Я лично столкнулся с 2-мя, которые пришлось заводить. К счастью, с фиксами все происходит очень быстро.

Плюсы Netty


Теперь о хорошем.

Полный контроль


С нетти у вас есть абсолютно полный контроль над сетевым стеком. И это круто. Потому что у Вас есть возможность работать в абстракциях фреймворка при этом избегая всей сложности прямой работы с сокетами. Помимо этого у нетти из коробки есть куча фич, которых в принципе на рынке никто не предлагает, например возможность включать TCP_FASTOPEN, TCP_MD5SIG, TCP_CORK, SPLICE, поддержку нативного Epoll транспорта, OpenSSL и кучу других штук. И все это работает из коробки!

Поддержка разных протоколов


TCP, UDP, UDT, SCTP? Легко. HTTP, HTTPS, HTTP/2, SPDY, Memcached, MQTT? Уже есть. Подключить любой из этих протоколов вопрос нескольких десятков минут. И все это уже давно написано, оптимизировано и протестировано. Интересный момент с HTTP/2. Его добавили в нетти уже около 3-х месяцев назад. В nginx например его поддержка появилась буквально 2 недели назад. Я думаю это о многом говорит.

Много готовых обработчиков


Нужно добавить SSL? Пожалуйста — SslHandler. Нужно сжимать весь трафик? Выбирайте любой — JdkZlibEncoder, ZlibEncoder, SnappyFramedEncoder. Нужно парсить джейсон на ходу? — JsonObjectDecoder. Нужно закрывать неактивные сокеты? Пожалуйста — ReadTimeoutHandler. Нужно полностью вычитать из сокета данные и только потом передать управление в бизнес логику? — ну вы поняли…

Быстрая


100к рек-сек на ядро для простейшего HTTP GET? С нетти это действительно так.



В основном благодаря тому факту, что она используется в таких монстрах как Facebook, Twitter, поэтому оптимизациям в нетти уделяется очень много внимания. Еще один немаловажный фактор почему нетти быстрая — это использования direct буферов, что очень сильно снижает нагрузку на GC. При этом всю сложность управления этими буферами берет на себя нетти.

Нет synchronize


Еще одна из причин, почему нетти довольна быстрая — это ее асинхронная природа, а значит фактически полное отсутствие блокировок внутри обработчиков IO операций (workers) и отсутствие потерь при переключении контекста между потоками. Сама природа нетти вынуждает Вас писать код, которые должен быть потокобезопасным. Конечно полностью избежать синхронизаций у Вас вряд ли получится, но ниже я расскажу, как этого максимально избежать.

Отличное сообщество


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

Продолжение следует…

Комментарии (36)


  1. voidnugget
    16.10.2015 18:43

    Vert.x сейчас находится в зачаточном состоянии и позиционируется как фреймворк для построения реактивных приложений и микросервисов. Естественно он чуть более чем полностью обёртка поверх Netty, а HA класстерные решения строятся поверх Hazelcast'a. Нужно обратить внимание на наличие автоматической кодогенерации, которая генерит Ruby / Groovy / JS API на основе существующих аннотаций Java API, т.е. в первую очередь это мультиязычное решение.

    По поводу «Direct-буферов» — Netty имеет свой Arena-аллокатор на подобие того что используется в jemalloc, и самостоятельно производит менеджмент offheap'a.

    А я сижу, пишу асинхронный PostgreSQL коннектор на vert.x'e :3 который можно было бы использовать в более-менее продакшенах.


  1. sergeylanz
    16.10.2015 18:52
    -2

    Netty смотрится хорошо на тестах. Мне не приходилось с ней работать но как я понел вам было не совсем просто написать на этом проложения которое берет сообщения с одного сокета и ложет в другой. Есть другие асинхроные более простые решения. Это и Node.js не очень быстрый но дешевый в плане разработки. Есть GO у которого все IO асинхроные и код простой синхронный. Язык быстрый и нет всяких заморочек. Я сам переписывал решение одно с Node.js на GO. Там логики по больше и редис и монго но сейчас это обрабытывает 1350 HTTP в секунду 3 сервера(меньше двух не ставим и так) по 4 ядра. Думаю с TCP это бы работало не хужею чем Netty. Вопрос стоило ли заморачиватся с Netty?


    1. voidnugget
      16.10.2015 19:00
      +2

      Думаю с TCP это бы работало не хужею чем Netty.
      Перечитывал несколько раз, так и не понял что подразумевалось. Всё остальное golang/node.js успешно дохнет на 15-20К запросов, «на всех ядрах».

      Вопрос стоило ли заморачиватся с Netty?
      Стоит, в первую очередь из-за производительности. Если вы не строите приложений критических к скорости обработки запросов, и вам 5ms погоды не сделают — понятное дело можно использовать что-то проще. C Netty, при правильной допилке, можно выжать под 150-200K запросов на ядро с задержками в 2-3ms. Естественно для большей части простых обывателей подобные вещи бесполезны.

      А вообще, сетевых библиотек с нормальным offheap'ом в природе больше замечено не было.


      1. doom369
        16.10.2015 19:15
        +2

        C Netty, при правильной допилке, можно выжать под 150-200K запросов на ядро


        Может поделитесь опытом, о какой допилке речь? У меня больше 20к рек-сек на ядро никак не полуачалось и это было без SSL.


      1. sergeylanz
        16.10.2015 19:23
        +2

        мне тоже интересно как получить 200K на ядро с 2-3ms. Как машина справляется с таким количеством сокетов?
        Цифры с в студию тесты и так далее а то я тоже могу писать 200K


      1. voidnugget
        16.10.2015 19:58
        +3

        Перво-наперво нужно подтюнинговать JVM.
        По хорошему — желательно вообще вырубить GC и использовать Disruptor, но я до такого сам не доходил, хотя были примеры из жизни и довольно позитивные отзывы. Я обычно выставляю выставляю низкий размер новых генераций для GC и максимальное переиспользование существующих. Как минимум, нужно поменять дефолтную реализацию hashcode что бы не было зависимости от размера пула энтропии — это уже даст довольно большой прирост производительности. Всё остальное — тюнинг ядрышка, нужно тюнинговать виртуальную память, что бы можно было запить больше файловых дескрипторов, ну там увеличить размер mmap/munmap'a посредством увеличения vm.max_map_count, после чего уже можно увеличивать всё остальное. Ещё можно увеличить количество ipv4 сокетов net.ipv4.ip_local_port_range, что иногда может приводить к довольно странным последствиям, и fs.epoll.max_user_watches для увеличения количества событий в epoll'e. Для обхода ограничения по количеству сокетов можно ещё маркировать пакеты, только «руками» без netfilter'a, и рассылать их по нескольким процессам — это позволит дублировать дескрипторы. Может чего и забыл, но я сейчас не особо то и слежу за текущим состоянием VM'a и сетевых плюшек в линуксе — переехал на более приземлённые задачи.

        Очень желательно использовать netmap или dpdk для мапинга памяти.
        Я писал как-то dpdk маппер для одного проекта, но заопенсорсить я его не могу по понятным причинам.

        p.s. 200K на ядро это я ещё помелочился :3 — можно гораздо больше.


        1. gurinderu
          16.10.2015 20:27

          Для того, чтобы отрубить GC придется очень постараться, да и disruptor не серебряная пуля. Не совсем понял как поможет низкий размер new generation, да и с hashcode не понятно. Уточните пожалуйста как они помогают?

          P.S. главное тут все же по моему мнению это тюнинг ядра


          1. voidnugget
            16.10.2015 21:03
            +2

            1. Есть много примеров контор которые действительно гоняли OpenJDK без сборки мусора — патчили System.gc(), хотя я не приветствую такой способ и считаю его большим костылём с ещё большими рисками. Как и почему нужно тюнинговать GC можно глянуть в презентации Рогозина, так же у него был хороший CheatSheet.
            2. Disruptor помогает в реализации стратегии «Share Memory By Communicating» — не нужно заморачиваться с пулами объектов и multiton'ами, размерами очередей сообщений в разнообразных вариациях модели актёров.
            3. Hashcode по умолчанию использует псевдослучайную генерацию хешей, которая быстро истощает пул энтропии в системе. По этому такие вещи как Haveged могут увеличить производительность виртуальной машины, но делают её random предсказуемым. Даже банальное Android приложение в 4.4 при замене дефолтного hashcode работает в 1.5-2 раза шустрее, по моим личным наблюдениям.

            Тюнинг ядра сводится к увеличению размера mmap'a что влияет на максимальное количество дескрипторов (сокетов) в системе в целом, всё остальное — это увеличение epoll пула и обоюдный тюнинг TCP стэка, влияние на производительность которого не сравнимо ни в каких рамках с overhead'ом виртуальной машины.

            dpdk / netmap нужны для того чтобы обойти netfilter и лишние memcpy для сетевых буферов.

            Понятное дело что тут есть много «но», и большую часть «кусков» приложения нужно правильно профилировать под нагрузкой — подобные рассуждения и скептицизм не очень то и конструктивны.


            1. gurinderu
              16.10.2015 22:40

              1. О таких конторах мы наслышаны. Проще сделать для serial gc большой хип и делать перезапуски приложения, когда его никто не использует. Для выпиливания gc нужны адские средства, на мой взгляд проще тогда переписать на c++
              2. Ну disruptor вроде как не представляет api для работы с пулами(этоже просто кольце кольцевой буфер). Замарочится скорее всего придётся.
              3. Если есть пул объектов наверное и не проблема вообще, ибо hashcode вычислить нужно один раз при создании объекта.

              P.S. А сколько сетевых интерфейсов на вашем сервере была если не секрет?


              1. voidnugget
                16.10.2015 23:06
                +2

                1. Приложение обновить/перезапустить можно только в горячую, и то не всегда. Задержка на перезапуск обычно не может быть больше 7-10ms. Обычно использую 1G. Для выпиливания GC нужен патч в 300 строчек С++.
                2. Если в этом самом кольцевом буфере выделять статические объекты для работы с различными сущностями — отпадает потребность в реализации пулов и мультитонов, но это требует специфического подхода в разработке, и позволяет один раз выделить память «под всё» и полностью остановить сборку мусора.
                3. На самом деле время вычисления hashcode может быть в 2-3 раза более ресурсоёмко нежели создание самого объекта и выполнения нужного кода, и для абстрактных Netty в вакууме его реализация без псевдо-случайностей часто бывает на много шустрее.

                4 канала по 40ГБит на 4ёх Virtex7 картах :|


        1. sergeylanz
          16.10.2015 20:41
          +1

          Половина оптимизаций не имеет отношение к Netty. Все оптимизация ядра помогут любой платформы. Работать без GC продакшине не hello world как то не очень.


          1. voidnugget
            16.10.2015 21:06

            Я и не предлагаю всем вподряд вырубать GC.


        1. sergeylanz
          16.10.2015 20:50

          вот тесты с оптимизацией машины с GC и без всяких извращений
          gist.github.com/hgfischer/7965620#go-standalone-1


          1. voidnugget
            16.10.2015 21:24

            Эм, ну бэнчмарк малость неактуален и «туговат».
            Нету ничего странного что при upstream'e на TCP'шном сокете производительно в 1.5-2 раза падает, но подобные бэнчмарки непосредственно к производительности самого golang'a отношения не имеют, хотя бы из-за «100 потоков по 500 соединений» и радости от 17-120ms задержек на древнем 1.2 runtime. А сабж вообще о нецелесообразности использования nginx балансера поврех golang'a, что вообщем-то должно быть совсем «как бы кэп» очевидно.

            В общем я и не думаю что мне стоит здесь так распинаться по поводу производительности, которая в 99% случаев никому не нужна.
            Буду в очередной раз оскорблять чувства «верующих» и читать разнообразные бесполезности.

            Попробуйте аргументировать свой скептицизм и разобраться в личных целях подобных комментариев.
            Фраза по поводу «серебряной пули» мной расценена как психологическая проекция.


    1. doom369
      16.10.2015 19:11

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


      1. voidnugget
        16.10.2015 20:07

        Если сравнивать высокую доступность — тут у Erlang'a есть преимущество, так как в нём почти всё в Beam машине написано с использованием wait-free (но не lock-free) алгоритмов. Если сравнивать по производительности — она ниже плинтуса, так что для задач посложнее memcpy он не очень то и подходит. А в «дикой природе» получается так что гораздо проще прикрутить ту же Akka и, при правильной допилке, получить ту же производительность — это касается любых проектов которые отправляют что-то сложнее HelloWorld по Http.

        Жду когда допилят порт erlang'a под llvm.


  1. antonpv
    17.10.2015 19:23

    но ниже я расскажу, как этого максимально избежать.

    Так все-таки расскажете? :)


    1. doom369
      17.10.2015 21:54
      +1

      Да, конечно. Чуть позднее. Сейчас как раз имплементирую это для нашего продакшена =).


  1. unicast
    17.10.2015 21:43
    +1

    А что за проект?


    1. doom369
      17.10.2015 21:56
      +2

      www.blynk.cc сервер кстати, опенсорсный github.com/blynkkk/blynk-server


      1. unicast
        17.10.2015 21:58
        +2

        Ого!
        Этот проект явно тянет на отдельную статью, а то и не одну.


        1. doom369
          17.10.2015 22:03
          +4

          На самом деле я уже их написал =). Правда на другом ресурсе — dou.ua/lenta/columns/blynk-30-days-after


          1. ZigFisher
            18.10.2015 00:28

            Хмм…
            Хотелось-бы почитать статью, однако РКН блочит этот домен.
            Есть-ли у вас статья на альтернативном ресурсе?

            P.S. Про VPN знаю, умею, но сейчас в командировке с чужим планшетом.


            1. doom369
              18.10.2015 00:38

              Есть в гугл доке, где она готовилась docs.google.com/document/d/18vw4dZY53GF1XLYWULFRaH5sSYIlko-biyIo_JlsKzs/edit?usp=sharing


        1. doom369
          17.10.2015 22:06
          +1

          Cначало я написал сюда. Но тут меня забанили =). Так что я теперь не рискую публиковать информацию о проекте.


  1. j_wayne
    17.10.2015 22:06

    А CoAP рассматривали? coap.technology


    1. doom369
      17.10.2015 22:09

      Смотрел. Я прогуглил все популярные решения на тот момент. Но если MQTT используется очень часто, то CoAP я вообще не встречал. Даже теперь. Спустя 6 мес. после запуска. Вообще как показала наша практика — людям всеравно, что вы реализуете на низком уровне и какие протоколы используете, если работает хорошо и проект решает реальную проблему. Даже не смотря на тот факт, что наш продукт исключительно для железячников.


      1. j_wayne
        17.10.2015 22:18

        Спасибо, меня интересовала как раз критика и основную причину я уловил. Но может еще что-то? Интерес не праздный, я пишу систему, использующую CoAP для зарубежного клиента (телеметрия/управление). Мне довольно таки понравился сам протокол и его реализация — Californium (не идеал, но вполне годно). Правда, до внедрения еще не дошли, так что основные грабли наверняка еще впереди)


        1. doom369
          17.10.2015 22:25

          Тут не подскажу. Я лишь поверхностно ознакомился с ним. Могу по MQTT только рассказать. Несмотря на то, что это довольно популярный протокол, многие железячники его недолюбливают (по крайней мере сложилось такое впечетление при общении с американцами, когда мы презентовали Блинк). Основная причина — туда заложили всего и вся на все случаи жизни, хотя в реалных кейсах надо лишь 20%.


  1. random1st
    18.10.2015 11:24

    Amazon недавно представил свою версию IoT, правда, пока только в бете. Пока не щупал, но думаю, стоит рассмотреть, как альтернативу.


    1. doom369
      18.10.2015 11:31

      Альтернативу чему? У нас решения для мобильных устройств. Облако — это побочный продукт =).


  1. igor_suhorukov
    18.10.2015 12:54
    +1

    Похвально, в нужном направлении работаете! Раз уж тема java, поделюсь ссылкой на kura и iot.eclipse.org, может кому окажется полезно


  1. vsb
    18.10.2015 20:37

    В качестве документации могу порекомендовать книгу Netty in Action от одного из авторов библиотеки. Хорошее последовательное изложение. Описывается netty 5, но особых отличий от netty 4 вроде бы нет.


    1. doom369
      18.10.2015 21:19

      Я ее прочитал =).


    1. doom369
      18.10.2015 22:04

      Кстати, книга уже устарела. В нетти много чего поменялось с тех пор. Ну и на некоторые вопросы там всеравно нету ответов =). Хотя судя по всему это пофиксят в следующей книге.


  1. man4j
    21.10.2015 10:20

    В своё время тестировал Grizzly. Очень понравилась штука. Используется насколько я понимаю в GlassFish. По сравнению с нетти более понятный API.