Pull to refresh

Comments 64

Статья отличная. Спасибо.

P.S. Эх… Ну где же вы раньше были?
>Вы так же можете поэкспериментировать с FrameDecoder'ом, если, например, Вы можете заранее определить размер пакета по его ID.

FrameDecoder эффективнее. А длинна определяется просто, первым в пакете посылается int с длинной пакета. Тогда все начинает работать быстрее и проще. В качестве протокола удобно использовать protobuf, очень быстрая и удобная сериализация.

Если хабравчанам интересно, могу накатать статью как я у себя делал сервер на netty для реалтайм игры. Только кармы пока не хватает ))
Хотя я планировал это сделать в цикле статей с примером клиента на флеше 11 в 3D… но могу и отдельно.

На моем нетбуке (AMD 1.4ГГц) «тянет» порядка 20000 запрсов в сек.
Эффективнее — это точно. Но у меня протокол был разработан задолго до меня. Я хоть и вношу в него изменения сейчас, но времени переписывать клиентскую реализацию пока нет. Когда руки дойдут, переделаю на FrameDecoder. Там много пакетов содержит строки неизвестной заранее длины (причём обычно несколько строк) или массивы байт/шортов и тоже по-несколько. Можно добавить к таким динамическим пакетам в начало общую длину, думаю, так будет эффективнее.

Спасибо, не задумывалась об этом так серьёзно :)
А как вы определяете ID пакета? Ещё немного не понял у вас Binary протокол?
Зарание спасибо за ответ, а то уже 3 день сношаю netty и всё какие то косяки появляются.
По первым двум байтам (unsigned short), Вы можете использовать один, если у Вас не много пакетов.
Да я тоже так делаю, наверно где то криво считываю эти байты. Спасибо за ответ. Буду дальше курить маны.
Отлично… хоть бы один умник объяснил за что минусует.
напишите, было бы ОЧЕНЬ интересно. Если нужна помощь с публикацией — могу даже публиковать за вас со ссылкой на авторство — в общем был бы рад сделать все, чтобы такая статья появилась на хабре.
Немного не в тему, но лучше бы посвятили год на помощь в развитии minetest`а, чем пилить нотчевскую поделку, «годную» исключительно для сингла — результаты были бы более полезны. И 500+ онлайна там вроде бы уже давно не проблема.
Можно попросить Вас покинуть мою уютную статью? Я не собираюсь объяснять своё мировоззрение на minetest. Но сревер мы пишем только потому, что нотчевская подделка годна лишь для сингла. Зайдите к нам и поиграйте, у нас другой майнкрафт.
Мне не понятно несколько моментов.

Не совсем понятно как работает pipeline. Он пропускает все события через каждый обработчик? Тогда получается, что у нас сначало событие декодируется, потом кодируется назад, потом обрабатывается.

В первом листинге у вас есть код new PlayerHandler(decoder, encoder), но в листинге с самим PlayerHandler не приведён конструктор. Ворос: как используются эти два параметра, зачем они?

Ещё, в PlayerHandler есть аттрибут worker типа PlayerWorkerThread — что он делает? Почему он создаётся заново при каждом новом соединении и сохраняется прямо в аттрибут объекта и оттуда же берётся при отсоединении? Если он и правда поток, то это ведь всё та же модель что и со старым IO: одно соединение — один поток, нет?
Ок, по пунктам.

При создании pipeline определяется, какой обработчик в какую сторону работает — те, что наследуют интерфейс ChannelUpstreamHandler работают в сторону «пользователь -> сервер», те, что ChannelDownstreamHandler— в обратную сторону. В моём случае, ChannelDownstreamHandler только PacketFrameEncoder, который наследуется от OneToOneEncoder, который реализует этот интерфейс.

Второе: очевидно, я опустила некоторые внутренние подробности работы PlayerHandler специфические только для моеё реализации логики — deocder и encoder передаются ему, чтобы можно было управлять режимом передачи пактов.

PlayerWorkerThread не является Thread, просто название так получилось исторически. На самом деле, это просто объект, который хранит информацию об игроке. Обработкой этой информации занимается уже «бизнес-логика».
Обработчики входящих/исходящих событий укладываются в пайплайн одной цепочкой, потому что с точки зрения фреймворка между ними нет никакой разницы. Он передает методам обработчика контекст, а они с его помощью могут послать событие «выше» или «ниже», или никуда не передавать вообще, тогда обработка этого события закончится.
Upstream/DownstreamHandler — это адаптеры (в java-смысле) одного интерфейса, которые по-умолчанию просто шлют все сообщения в одну из сторон.

В приведенных выше классах методы encode/decode — шаблонные в Encoder/Decoder, проталкивание сообщения при помощи контекста осталось на уровне проверок, поэтому логика работы не очевидна.
Я уже достаточно долго (около 3-4 месяцев) нахожусь в раздумьях на тему того, как будет реализован будущий сервер приложения, которое должно в реальном времени синхронизировать данные между 2+ клиентами.

Это, конечно, не игровой сервер — максимальное предполагаемое число клиентов <100 (100 — только в страшном сне :), скорее обычно даже <10, но как мне кажется скоростью никогда не стоит пренебрегать. Да и по сути передаваемые данные будут схожи с игровыми действиями/командами — приведённая вами схема работы идеально ложится под наши задачи. Так что, думаю, благодаря вашей статье я начну первые тесты скорости и удобства использования именно с этого сервера.

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

И большое спасибо за весьма полезную статью, коих встречается мало в последнее время :)
Конечно, задавайте. Если смогу отвечу.

Вообще, в Вашем случае не факт, что Netty даст лучший результат, чем блокирующее IO по два потока на клиента. Когда клиентов мало, такая система хорошо работает. В любом случае, сделать блокирующее IO намного проще NIO, так что советую поэкспериментировать, если не лень.
Естественно экспериментов будет много, но всему есть предел. Поэтому подобные «информационные» статьи — когда всё что нужно для начала работы собрано в одном месте — на вес золота.

Если говорить более конкретно — по сути у нас будет происходить множество передач «мелких» данных вида:
Клиент1 -> Сервер -> Клиент2, Клиент3, Клиент4, Клиент5
С промежутком (в среднем) от 1 до 5-10 секунд между передачами.

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

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

Если передач мало (на одного клиента в секунду), то Netty Вам даст отличный результат производительности, т.к. у Вас не будет висеть по два потока на клиента, которые в пустую ждут данные, а будет один (или больше, если клиентов станет больше потом, то можно легко добавить потоки), который будет иногда принимать/отправлять данные.

Если передач много, клиентов мало (и больше не станет точно), то простота написания обычного IO тут может выиграть.
У нас, скорее всего, будет первый вариант (мало передач на одного клиента в секунду).

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

Впрочем, на разного качества каналах и на различных системах, как показывает практика, проблемы могут возникать либо с одноразовой передачей крупных данных, либо с постоянной отсылкой данных мелкими порциями, либо даже в обоих случаях у крайне тяжёлых «пациентов», так что, думаю, нам ещё только предстоит «увидеть врага в лицо» и узнать что лучше нам подойдёт.
Непосредственно на скорость передачи данных будут влиять только сеть между сервером и клиентом. Я на 99.5% уверена, что по скорости передачи данных разницы не будет. В NIO буферы быстрее пишутся, но сама скорость обмена от этого не зависит.
Тоже верно…
Чтож, ещё раз спасибо за дельные советы :)
Вне зависимости от того, много или мало данных, неблокируемый ввод/вывод даст преимущество перед блокируемым.
> то простота написания обычного IO тут может выиграть.
Нужно оценивать не абсолютную производительность и выигрыш в 2 наносекунды на пакет от клиента, а трудозатраты / выигрыш производительности.
Я думаю, именно так рассуждал тот, кто сделал первую версию вашего сервера :))
Идея (1 поток на ввод, 1 на вывод + 1 на обработку) просто «гениальна» :))
Ну, это отлично работало, вообще) Тем более, когда я начинала проект, я в яве была, извиняюсь, не в зуб ногой — всё учила на ходу и на примерах. И о том, что IO может быть другим, и что лучше сразу делать его нормальным, я не задумывалась.

А один поток обработки на пользователя это, практически, философия сервера — всё в своём потоке. Конечно, идея провалилась, т.к. 99.5% времени потоки игроков спят.
Знания Java тут не причем. Тут важно понимание работы с сетью, не более того.
Если внимательно посмотришь на высокопроизводительные сетевые решения, то работа строится по принципу «чем меньше потоков — тем лучше», кроме тех случаев, когда платформа (Erlang, например) предоставляет «зеленые» потоки. Во всех остальных случаях, относительно небольшое увеличение нагрузки легко кладет серверное приложение.
С сетью до этого я тоже не работала и примеров под рукой у меня было мало. В общем говоря, этот сервер мой первый серьёзный опыт программирования вообще, так что в начале было наделано достаточно архитектурных ошибок.
А не хотите взглянуть на стиль интеграции Messaging. В частности, JMS может неплохо работать с каналами типа «публикация-подписка». Если нужно что-то посложнее, то можно использовать фреймворк Apache Camel, в нем можно описывать маршруты сообщений.
Мы скорее всего остановимся на первом варианте, который будет удовлетворять нашим требованиям к скорости и удобству использования. Хотя, если такового не найдётся в знакомых нам вариантах — будем копать дальше и искать новые.

В любом случае — спасибо, добавлю в список на «посмотреть».
Думаю Camel сюда не лучший вариант, какое-то оверкилл решение с еще заранее неизвестной производительностью. Хотя быть может все дело в том, что я не смог за неделю изучить все тонкости роутинга в камеле и связки его c миной, но по сравнению с кодом на руках который у меня был, производительность скакала у меня очень сильно (хотя была в пределах в 12-15 (точно уже не помню) раз меньше самописных экзекьютора и роутера и енкодеров-декодеров).

Хотя вообще у нас весь код роутинга и енкодинга генерился при проходе apt-ом и быстрее врядли можно было бы его написать (прямые вызовы нужных методов вроде out.writeInt(message.getSomething())
Добавила в конец статьи ещё немного информации об интересных штуках в Netty, в частности про ChannelFuture — очень удобная вещь.
Если бы в статье была бы ссылка на сам этот Netty — было бы лучше. Необязательно каждый раз напрягать гугл.

А в качестве выбора Apache Mina рассматривали?
Добавила ссылку в статью.

Нет не рассматривали, т.к. дело было пока не срочное, а Netty просто попалась и понравилась.
Главный разработчик Apache Mina делает сейчас Netty.
И походу развития Apache Mina больше не будет.
Угу, учитывая какими темпами циферки в версиях наращивает мина и нетти… переход на последнюю становиться все более и более очевидным — благо в ней и плюшек больше (к примеру тот же http)
Я не совсем понял, что же такое netty и почему он работает быстрее? За счет чего? За счет своей реализации потоков, сокетов?
Netty — NIO-библиотека для Java. Быстрее она работает потому что это NIO — оно по определению быстрее Blocking IO, и потому что она использует очень хорошо реализованные буферы.
NIO — это NEW IO. Я так понял Вы подразумеваете под NIO — Non-blocking IO.
Как бы там ни было, я по-моему понял в чем фишка Netty:
В случае NioServerSocketChannelFactory есть главный поток, который принимает входящие соединения. Когда появляется новое соединение, оно оборачивается в Channel и ссылка на Channel отдается дочерним потокам для обработки. Соответственно, при работе с Channel все команды ходят через главный поток. Так?
Под NIO я подразумеваю NEW IO. И обычно оно всё-таки Non-blocking IO.

Всё так, но при работе с Channel все команды ходят через рабочий поток. Главный поток только принимает подключения. Могу ошибаться, и он занимается чем-то ещё, но за сам канал отвечают именно рабочие потоки. При чём все потоки за все каналы, нет связки канал-поток (можно сделать, если нужно).
То, что NIO быстрее — довольно распространенный миф. В этой презентации, ссылаясь на эксперименты, автор показывает, что в действительности NIO уступает в производительности thread-per-socket решению примерно на 25%. К тем же результатам привели мои собственные опыты, где в высоконагруженной системе (~2K connections, ~30K requests per second) сервер на Netty показал пропускную способность на 15-35% ниже, чем простой сервер на блокирующих сокетах. Кроме того, за счет активного создания новых ChannelBuffer'ов, Netty-сервер оказывал заметно бОльшую нагрузку на потребление памяти и GC. В данном контексте единственным преимуществом NIO-сервера над thread-per-socket решением является меньшее число потоков и, как следствие, большее число коннекций, которое способен обслуживать один сервер.
Этот «доклад» можно закрывать сразу после того, как сервер базирующийся на NIO назвали асинхронным. Асинхронное API появилось только в Java 7, что явно позднее этой презенташки. Чувак элементарных вещей не в теме, а еще что-то измерять полез :)
Наверное, вы так и сделали, не дочитав до места, где поясняется, в каком смысле сервер «асинхронный» :) Сервер, базирующийся на NIO, очень даже можно назвать асинхронным, поскольку он, как правило, эксплуатирует событийно-управляемую модель ввода-вывода. Кстати, MINA и Netty определяются как «asynchronous event-driven network application framework», несмотря на то, что ни тот, ни другой async I/O не используют.

Как бы то ни было, суть вопроса не в словах. Исходя из личного и чужого опыта, я взял на себя смелость возразить на утверждение, что «NIO по определению быстрее Blocking IO».
Сервер базирующийся на Netty можно назвать не-блокирующим, каким он, по сути, и является. Зачем авторы назвали его асинхронным для меня большая загадка, может из зависти к тем языка/платформам, на которых в то время уж был асинхронный I/O.

Асинхронный, как и неблокирующий I/O однозначно быстрее блокирующего, при грамотном использовании. А так, да, я с тобой полностью согласен — кривой асинхронный сервер может быть медленнее более продуманного синхронного.
Синхронный сервер проще написать и меньше шансов сделать что-то не так.

Но вообще-то, вы знаете… читать из потока — медленно. Читать из NIO-буфера — быстрее. NIO-буферы работаю по-другому, они быстрее.
Netty тут вообще не причем, просто удобный фреймворк. NIO тоже не решает основную проблему производительности. 980 потоков, которые создаёт java для обработки клиентов — вот где собака производительности зарыта. На переключение между этими потоками видимо уходит значительно больше времени, чем на саму полезную работу.

А Netty+NIO позволяют в 4 потоках обрабабывать всех юзеров. Тем самым, решая проблему, без создания over 9000 поток. Если интересны подробности гуглите на тему Green Threads, на хабре тоже что-то было.
тут у вас есть определенное количество неточностей, прыгающих в глаза. Например, останавливать сервер вызовом close — это не самая лучшая идея особенно в комплекте с awaitUninterruptibly(). Правильно это делать в в три этапа, с предварительным закрытием ChannelGroup (куда все активные коннекты добавлять/убивать из channelOpen/channelClosed. И не забыть bootstrap.releaseExternalResources(). Все это описано в оф.документации.

По поводу «Executors.newCachedThreadPool(), он создаёт неоправданно много потоков и ни какого выигрыша от Netty почти нет» это вы что-то путаете. В Netty можно сделать поток на коннект, но вовсе не выбором CachedThreadPool, а OioServerSocketChannelFactory.

И в качестве последней придирки «ChannelBuffer очень похож на DataInputStream и DataOutputStream в одном лице» — он скорее похож на ByteBuffer с 2мя указателями.
Соглашусь, да, кое-что упущено. Могу пооправдываться: у меня каналы клиентов закрываются отдельно, при этом им отправляются пакеты, что сервер остановлен. А т.к. после закрытия канала сервера обычно приложения выключается, я лично не вижу особого смысла выполнять какие-либо действия ещё.

По поводу «Executors.newCachedThreadPool()»: да, тут я тоже не права. Максимальное число рабочих потоков, кажется, определяется аргументом в NioServerSocketChannelFactory.

Про ChannelBuffer я имела ввиду именно его интерфейс для записи/чтения в/из него. Его функции по синтаксису больше всего похожи именно на простые и понятные DataInputStream и DataOutputStream.
ну это я в порядке придрок и общего брюзжания. А вообще, полезная статья может оказаться, особенно для начинающих. Я бы добавил пару пунктов, не очевидных для тех, кто пришел из мира блокирующих коммуникаций и у кого не очень ясная картина как там потоки и что там может заблокироваться. Во первых, важно помнить главное — никогда и ни при каких обстоятельствах в обработчики нельзя ставить нечто блокирующее (например put в BlockingQueue) и, в общем случае, избегать любых долгих операций в этих обработчиках. А, во вторых, полезно помнить, что обработчики потоко-безопасные, и никакой дополнительной синхронизации в них не надо, конечно если нет доступа к общим данным.
Спасибо, добавила в статью про блокировку, а так же про то, что .await() тоже нельзя вызывать из обработчика.
Товарищ умпутун, а не порекомендуете ли что вообще полезного про Netty почитать есть в сети кроме их сайта с довольно таки невнятной документацией и простейшими примерами?

Или вопросы вам можно позадавать, раз вы, насколько я понял, глубоко этот нетти копали?

Вот, к примеру, каков православный путь реализации сложных протоколов на нетти (не хттп-шного типа — запрос-ответ, а многоступенчатых, да ещё с каким-нибудь «сердцебиением» по дороге)? Например, сначала согласование поддерживаемых версий протокола, потом согласование методов аутентификации, потом собственно аутентификация, и тд… Вставкой-удалением хендлеров в пайплайн? Сложным здоровенным конечным автоматом?

А если вставкой-удалением, то как это делать правильно? В тьюториалах есть и простой метод — «вставили другой хендлер, удалили себя», и с обвесом флажками — «ставим флаг, вставляем хендлер, удаляем себя, а если вдруг опять попали в себя и флаг оказался взведён — отправляем сообщение дальше в пайп». Видел и совершенно безумную вариацию последнего в виде — «когда флаг взведён, складываем сообщения в коллекцию, а потом в какой-то момент выплёвываем её в пайп».

И как правильнее — сразу создавать длинную цепочку, а потом выкидывать оттуда отработавшие хендлеры (или занавешивать флажками) или вставлять новые по мере продвижения по протоколу?
примеры у них действительно не самые сложные. Однако, чтение исходников сильно помогает понять как оно должно работать. Вопросы можно и нужно задавть на stack overflow. Их форум туда переехал.

Что касается сложных протоколов, то это да, без поллитры не поднять. Я делал это частично конечным автоматом, частично форсированием начальнай части коммуникации в синхроный вид. Что касается серцебиений и всяких прочих PING/PONG в процессе — то я это сажал на idle события (у них есть IdleStateAwareChannelHandler для этого).

Вообще, сложные протоколы в Netty стоит реализовывать только если очень надо. Т.е. если у вас разумное количество паралельных соеденений то не стоит себе морочить голову, OIO реализция будет сильно проще
Эмуляторы Lineage 2 на java давно используют nio (известный mmocore engine). Мы в своей команде в своё время ввели netty, но до конца не довели.

На текущей реализации до нескольких тысяч людей сервер держит.
А ещё NIO ввели в Java 1.4…

В l2j очень сложная реализация NIO, по примерам которой, по-моему, свой проект не построишь. Mmocore engine не удобный в использовании, я считаю.
Годная практическая статья, разве что вводной по NIO не хватает и побольше разжеванности, те кто с Netty не сталкивался могут не понять.
Может быть когда высплюсь, подумаю о возможности написания общей статьи по NIO или добавления в эту статью. Просто именно практическими знаниями по голому NIO я почти не обладаю, т.к. его сложно у меня применять «из коробки».
я у себя использую Apache MINA, еще мне очень нравится naga NIO для простых проектов
Последним реально эффективным ускорением тяжелого приложения на java был переезд на две новые железки (каждая 2 xeon E5630, 24G RAM, зеркальные рейды), один — app, второй — sql. Два месяца работы сравнимы со стоимостью железки, память «оптимизируется» только в случае существенных утечек, дешевле купить ещё пару планок памяти.
Интерес оптимизации часто остаётся именно спортивным. 100к руб профессионального программиста java в месяц — 10 плашек по 16Gb (Kingston KVR1066D3Q4R7S/16G). Аналогичная ситуация по другим узким местам.
Хотя, конечно, я смотрю на это с точки зрения системного администрирования и опыта менеджмента нагруженных проектов.
Да, это в общем не дало особого толка. На пике нагрузке сервер всё равно тормозит, хотя и на 20% меньше.
я правильно понимаю, что вы умудрились утилизировать процессорные ресурсы или ресурсы шины данных на объёме ~100 юзеров. Или, может быть, дело в сцепленности и используется 100% одного ядра.
Без потерь производительности, когда все системы работают на номинальной скорости, тянется около 170 пользователей. Но Вы, возможно, не знаете, что сами пользователи составляют лишь процентов 5 от всей нагрузки. Там множество других данных, которые требуют регулярной обработки. С приходом пользователей и загрузкой ими зон, количество этих данных возрастает (обработка новых зон, новых животных, предметов и т.д.), но зависимость не линейная.
никогда и ни при каких обстоятельствах в обработчики нельзя ставить нечто блокирующее (например put в BlockingQueue)

Объясните тогда как вклинивается бизнес логика без блокировки потоков нетти
Используйте add, если хотите добавить в BlockingQueue. В этом случае, если очередь заполнена, сгенерируется исключение и Вы можете отключить клиента с Rcv Queue Overloaded, например.
Честно говоря не понял момента:
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
worker = new PlayerWorkerThread(this, e.getChannel());

у вашего сервера столько потоков, сколько и подключено клиентов?
А кто Вам сказал, что это поток? Он, конечно, называется Thread, но это просто элемент обработки действий пользователя, который может быть реализован так, как этого требует популярность сервера, моя загруженность чтобы что-то делать и сложность реализации. В данный момент — это поток, но ничего не мешает повесить всех на пул потоков или вообще на один поток. Кроме моей лени и отсутствия необходимости в данный момент.
Настоятельно рекомендую ознакомиться с эрлангом =) Вы будете удивлены тому, насколько изящно и хорошо решится большая часть ваших проблем.
Sign up to leave a comment.

Articles