Пользователь
0,0
рейтинг
17 января 2012 в 22:10

Разработка → Высокопроизводительный NIO-сервер на Netty из песочницы

Java*
Преамбула

Здравствуйте. Я являюсь главным разработчиком крупнейшего в СНГ сервера Minecraft (не буду рекламировать, кому надо, те знают). Уже почти год мы пишем свою реализацию сервера, рассчитанную на больше чем 40 человек (мы хотим видеть цифру в 500 хотя бы). Пока всё было удачно, но последнее время система начала упираться в то, что из-за не самой удачной реализации сети (1 поток на ввод, 1 на вывод + 1 на обработку), при 300 игроках онлайн работает более 980 потоков (+ системные), что в сочетании с производительностью дефолтного io Явы даёт огромное падение производительности, и уже при 100 игроках сервер в основном занимается тем, что пишет/читает в/из сети.

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

Здесь я постараюсь расписать серверную часть работы с сетью через Netty, может быть это кому-то будет полезно.

Создание сервера


ExecutorService bossExec = new OrderedMemoryAwareThreadPoolExecutor(1, 400000000, 2000000000, 60, TimeUnit.SECONDS);
ExecutorService ioExec = new OrderedMemoryAwareThreadPoolExecutor(4 /* число рабочих потоков */, 400000000, 2000000000, 60, TimeUnit.SECONDS);
ServerBootstrap networkServer = new ServerBootstrap(new NioServerSocketChannelFactory(bossExec, ioExec,  4 /* то же самое число рабочих потоков */));
networkServer.setOption("backlog", 500);
networkServer.setOption("connectTimeoutMillis", 10000);
networkServer.setPipelineFactory(new ServerPipelineFactory());
Channel channel = networkServer.bind(new InetSocketAddress(address, port));

Используется OrderedMemoryAwareThreadPoolExecutor для выполнения задач Netty, по опыту французских коллег они самые эффективные. Можно использовать другие Executor'ы, например Executors.newFixedThreadPool(n). Ни в коем случае не используйте Executors.newCachedThreadPool(), он создаёт неоправданно много потоков и ни какого выигрыша от Netty почти нет. Использовать более 4 рабочих потоков нет смысла, т.к. они более чем справляются с огромной нагрузкой (программисты из Xebia-France на 4 потоках тянули более 100 000 одновременных подключений). Босс-потоки должны быть по одному на каждый слушаемый порт. Channel, который возвращает функция bind, а так же ServerBootsrap необходимо сохранить, чтобы потом можно было остановить сервер.

PipelineFactory


То, как будут обрабатываться подключения и пакеты клиента, определяет PipelineFactory, которая при открытии канала с клиентом создаёт для него pipeline, в котором определены обработчики событий, которые происходят на канале. В нашем случае, это ServerPipelineFactory:
public class ServerPipelineFactory implements ChannelPipelineFactory {
	@Override
	public ChannelPipeline getPipeline() throws Exception {
		PacketFrameDecoder decoder = new PacketFrameDecoder();
		PacketFrameEncoder encoder = new PacketFrameEncoder();
		return Channels.pipeline(decoder, encoder, new PlayerHandler(decoder, encoder));
	}
}

В данном коде PacketFrameDecoder, PacketFrameEncoder и PlayerHandler — обработчки событий, которые мы определяем. Функция Channels.pipeline() создаёт новый pipeline с переданными ей обработчиками. Будьте внимательны: события проходят обработчики в том порядке, в котором Вы передали из функции pipeline!

Протокол


Немного опишу протокол, чтобы дальше было понятно.

Обмен данными происходит с помощью объектов классов, расширяющих класс Packet, в которых определены две функции, get(ChannelBuffer input) и send(ChannelBuffer output). Соответственно, первая функция читает необходимые данные из канала, вторая — пишет данные пакета в канал.
public abstract class Packet {
	public static Packet read(ChannelBuffer buffer) throws IOException {
		int id = buffer.readUnsignedShort(); // Получаем ID пришедшего пакета, чтобы определить, каким классом его читать
		Packet packet = getPacket(id); // Получаем инстанс пакета с этим ID
		if(packet == null)
			throw new IOException("Bad packet ID: " + id); // Если произошла ошибка и такого пакета не может быть, генерируем исключение
		packet.get(buffer); // Читаем в пакет данные из буфера
		return packet;
	}

	public statuc Packet write(Packet packet, ChannelBuffer buffer) {
		buffer.writeChar(packet.getId()); // Отправляем ID пакета
		packet.send(buffer); // Отправляем данные пакета
	}

	// Функции, которые должен реализовать каждый класс пакета
	public abstract void get(ChannelBuffer buffer);
	public abstract void send(ChannelBuffer buffer);
}

Пример пары пакетов для наглядности:
// Пакет, которым клиент передаёт серверу свой логин
public class Packet1Login extends Packet {
	public String login;

	public void get(ChannelBuffer buffer) {
		int length = buffer.readShort();
		StringBuilder builder = new StringBuilder();
		for(int i = 0; i < length ++i)
			builder.append(buffer.readChar());
		login = builder.toString();
	}

	public void send(ChannelBuffer buffer) {
		// Тело отправки пустое, т.к. сервер не посылает этот пакет
	}
}

// Пакет, которым сервер выкидывает клиента с указаной причиной, или клиент отключается от сервера
public class Packet255KickDisconnect extends Packet {
	public String reason;

	public void get(ChannelBuffer buffer) {
		int length = buffer.readShort();
		StringBuilder builder = new StringBuilder();
		for(int i = 0; i < length ++i)
			builder.append(buffer.readChar());
		reason = builder.toString();
	}

	public void send(ChannelBuffer buffer) {
		buffer.writeShort(reason.length());
		for(int i = 0; i < reason.length(); ++i) {
			buffer.writeChar(reason.getCharAt(i));
		}
	}
}

ChannelBuffer очень похож на DataInputStream и DataOutputStream в одном лице. Большинство функций если не такие же, то очень похожи. Заметьте, что я не забочусь о проверке того, хватает ли в буфере байт для чтения, как будто я работаю с блокирующим IO. Об этом далее…

Работа с клиентом


Работа с клиентом в основном определяется классом PlayerHandler:
public class PlayerHandler extends SimpleChannelUpstreamHandler {
	
	private PlayerWorkerThread worker;
	
	@Override
	public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
                // Событие вызывается при подключении клиента. Я создаю здесь Worker игрока — объект, который занимается обработкой данных игрока непостредственно.
                // Я передаю ему канал игрока (функция e.getChannel()), чтобы он мог в него посылать пакеты
		worker = new PlayerWorkerThread(this, e.getChannel());
	}
	@Override
	public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
                // Событие закрытия канала. Используется в основном, чтобы освободить ресурсы, или выполнить другие действия, которые происходят при отключении пользователя. Если его не обработать, Вы можете и не заметить, что пользователь отключился, если он напрямую не сказал этого серверу, а просто оборвался канал.
		worker.disconnectedFromChannel();
	}
	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
                // Функция принимает уже готовые Packet'ы от игрока, поэтому их можно сразу посылать в worker. За их формирование отвечает другой обработчик.
		if(e.getChannel().isOpen())
			worker.acceptPacket((Packet) e.getMessage());
	}
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
                // На канале произошло исключение. Выводим ошибку, закрываем канал.
		Server.logger.log(Level.WARNING, "Exception from downstream", e.getCause());
		ctx.getChannel().close();
	}
}

Worker может посылать игроку данные просто функцией channel.write(packet), где channel — канал игрока, который передаётся ему при подключении, а packet — объект класса Packet. За кодирование пакетов будет отвечать уже Encoder.

Decoder и Encoder


Собственно, сама важная часть системы — они отвечают за формирование пакетов Packet из потока пользователя и за отправку таких же пакетов в поток.

Encoder очень прост, он отправляет пакеты игроку:
public class PacketFrameEncoder extends OneToOneEncoder {
	@Override
	protected Object encode(ChannelHandlerContext channelhandlercontext, Channel channel, Object obj) throws Exception {
		if(!(obj instanceof Packet))
			return obj; // Если это не пакет, то просто пропускаем его дальше
		Packet p = (Packet) obj;
		
		ChannelBuffer buffer = ChannelBuffers.dynamicBuffer(); // Создаём динамический буфер для записи в него данных из пакета. Если Вы точно знаете длину пакета, Вам не обязательно использовать динамический буфер — ChannelBuffers предоставляет и буферы фиксированной длинны, они могут быть эффективнее.
		Packet.write(p, buffer); // Пишем пакет в буфер
		return buffer; // Возвращаем буфер, который и будет записан в канал
	}
}


Decoder уже гораздо сложнее. Дело в том, что в буфере, пришедшем от клиента, может просто не оказаться достаточного количества байт для чтения всего пакета. В этом случае, нам поможет класс ReplayingDecoder. Нам всего лишь нужно реализовать его функцию decode и читать в ней данные из потока, не заботясь не о чём:
public class PacketFrameDecoder extends ReplayingDecoder<VoidEnum> {
	@Override
	public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
		ctx.sendUpstream(e);
	}
	@Override
	public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
		ctx.sendUpstream(e);
	}
	@Override
	protected Object decode(ChannelHandlerContext arg0, Channel arg1, ChannelBuffer buffer, VoidEnum e) throws Exception {
		return Packet.read(buffer);
	}
}

Спрашивается, как это работает? Очень просто, перед вызовом функции decode декодер помечает текущий индекс чтения, если при чтении из буфера в нём не хватит данных, будет сгенерировано исключение. При этом буфер вернётся в начальное положение и decode будет повторён, когда больше данных будет получено от пользователя. В случае успешного чтения (возвращён не null), декодер попытается вызвать функции decode ещё раз, уже на оставшихся в буфере данных, если в нём есть ещё хотя бы один байт.

Не медленно ли всё это работает, если он генерирует исключение? Медленнее, чем, например, проверка количества данных в буфере и оценка, хватит ли их для чтения пакета. Но он использует кэшированное исключение, поэтому не тратится время на заполнения stacktrace и даже создание нового объекта исключения. Подробнее об и некоторых других, повышающих эффективность, функцийя ReplayingDecoder можно почитать здесь

Вы так же можете поэкспериментировать с FrameDecoder'ом, если, например, Вы можете заранее определить размер пакета по его ID.

Кажется, это всё


Результаты получились отличными. Во-первых, сервер больше не сыпет тысячей потоков — 4 потока Netty + 4 потока обработки данных прекрасно справляются с 250+ клиентами (тестирование продолжается). Во-вторых, нагрузка на процессор стала значительно меньшей и перестала линейно расти от числа подключений. В-третьих, время отклика в некоторых случаях стало меньше.

Надеюсь кому-нибудь это будет полезно. Старалась передать как можно больше важных данных, могла переборщить. Примеров ведь много не бывает? Спрашивайте Ваши ответы и не судите строго — первый раз пишу на хабр.

Постскриптум: ещё несколько полезных вещей


У Netty есть ещё несколько интересных особенностей, которые заслуживают отдельного упоминания:

Во-первых, остановка сервера:
ChannelFuture future = channel.close();
future.awaitUninterruptibly();

Где channel — канал, который возвратила функция bind в начале. future.awaitUninterruptibly() дождётся, пока канал закроется и выполнение кода продолжится.

Самое интересное: ChannelFuture. Когда мы отправляем на канал пакет, функцией channel.write(packet), она возвращает ChannelFuture — это особый объект, который отслеживает состояние выполняемого действия. Через него можно проверить, выполнилось ли действие.

Например, мы хотим послать клиенту пакет отключения и закрыть за ним канал. Если мы сделаем
channel.write(new Packet255KickDisconnect("Пока!"));
channel.close();

то с вероятностью 99%, мы получим ChannelClosedException и пакет до клиента не дойдёт. Но можно сделать так:
ChannelFuture future = channel.write(new Packet255KickDisconnect("Пока!"));
try {
      future.await(10000); // Ждём не более 10 секунд, пока действие закончится
} catch(InterruptedException e) {}
channel.close();

То всё будет супер, кроме того, что это может заблокировать поток выполнения, пока пакет не отправится пользователю. Поэтому на ChannelFuture можно повесит listener — объект, который будет уведомлён о том, что событие совершилось и выполнит какие-либо действия. Для закрытия соединения есть уже готовый listener ChannelFutureListener.CLOSE. Пример использования:
ChannelFuture future = channel.write(new Packet255KickDisconnect("Пока!"));
furute.addListener(ChannelFutureListener.CLOSE);

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

Ещё важная информация

Как правильно было замечено в комментариях, следует предупредить о том, что в обработчиках (handler-ах, которые висят на pipeline) лучше не стоит использовать блокирующие операции или ожидание. В противном случае, Вы рискуете навсегда потерять поток обработки или просто сильно затормозить обработку событий остальных клиентов.

Так же в обработчике ни в коем случае нельзя «ждать будущего», т.е. выполнять .await() или .awaitUninterruptibly() на любом ChannelFuture. Во-первых, у Вас ничего не получится, их нельзя вызывать из обработчиков — система не даст сделать такую глупость и сгенерирует исключение. Во-вторых, если бы этого не было, Ваш поток опять же мог бы умереть оставив других клиентов без обслуживания.

Вообще, все действия, выполняемые в ChannelHandler'ах должны быть как можно более простыми и неблокирующими. Ни в коем случае не обрабатывайте данные прямо в них — кладите пакеты в очередь и обрабатывайте их в другом потоке.
Анна Дорогова @Rena4ka
карма
73,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.