Ответ 1
what is a typical pattern for serving massive amounts of messages to clients?
Существует много возможных шаблонов: Легкий способ использования всех ядер без прохождения нескольких jvms:
- Попросите один поток принять соединения и прочитать с помощью селектора.
- Как только у вас будет достаточно байтов для создания одного сообщения, передайте его другому ядру, используя конструкцию, такую как кольцевой буфер. Рамка Disruptor Java подходит для этого. Это хороший шаблон, если обработка, необходимая для того, чтобы знать, что является полным сообщением, является легким. Например, если у вас есть префикс длины, вы можете подождать, пока не получите ожидаемое количество байтов, а затем отправьте его в другой поток. Если синтаксический анализ протокола очень тяжелый, вы можете подавить этот единственный поток, не позволяя ему принимать соединения или считывать байты в сети.
- В рабочем потоке (-ах), который принимает данные из кольцевого буфера, выполните фактическую обработку.
- Вы пишете ответы либо на своих рабочих потоках, либо через какой-то другой поток агрегатора.
Это суть этого. Здесь есть много возможностей, и ответ действительно зависит от типа приложения, которое вы пишете. Несколько примеров:
- Приложение с тяжелым статусом CPU говорит о приложении обработки изображений. Объем работы CPU/GPU для каждого запроса, вероятно, будет значительно выше, чем накладные расходы, генерируемые очень наивным межпоточным коммуникационным решением. В этом случае легкое решение - это куча рабочих потоков, вытягивающих работу из одной очереди. Обратите внимание, что это одна очередь, а не одна очередь для каждого рабочего. Преимущество заключается в том, что по своей сути сбалансированность нагрузки. Каждый работник завершает работу, а затем просто проверяет однопроцессорную очередь с несколькими потребителями. Несмотря на то, что это источник споров, работа по обработке изображений (секунды?) Должна быть намного дороже, чем любая альтернатива синхронизации.
- Приложение чистого ввода-вывода, например. сервер статистики, который просто увеличивает количество счетчиков для запроса: здесь вы почти не работаете с ЦП. Большая часть работы - это просто чтение байтов и запись байтов. Многопоточное приложение может не принести вам существенного преимущества. На самом деле это может даже замедлить работу, если время, затрачиваемое на очередь, больше, чем время, затрачиваемое на их обработку. Однопоточный Java-сервер должен быть способен насыщать 1G-ссылку.
-
Учетные заявления, требующие умеренных объемов обработки, например. типичное деловое приложение: здесь каждый клиент имеет определенное состояние, которое определяет, как обрабатывается каждый запрос. Предполагая, что мы идем многопоточными, поскольку обработка является нетривиальной, мы могли бы аффинировать клиентов определенным потокам. Это вариант архитектуры актера:
i) Когда клиент сначала связывает хэш с рабочим. Возможно, вы захотите сделать это с помощью некоторого идентификатора клиента, так что, если он отключится и снова подключится, он все равно будет назначен тому же работнику/актеру.
ii) Когда читательский поток читает полный запрос, поместите его в кольцевой буфер для правильного рабочего/актера. Поскольку один и тот же рабочий всегда обрабатывает конкретный клиент, все состояние должно быть потоковым локальным, делая всю логику обработки простой и однопотоковой.
iii) Рабочий поток может писать запросы. Всегда пытайтесь просто написать write(). Если все ваши данные не могут быть записаны только тогда, вы регистрируетесь для OP_WRITE. Рабочий поток должен только делать выборные вызовы, если есть что-то выдающееся. Большинство писем должны просто преуспеть, делая это ненужным. Трюк здесь заключается в балансировке между выборами и опросе буфера звонка для большего количества запросов. Вы также можете использовать один поток писем, единственная ответственность которых заключается в том, чтобы писать запросы. Каждый рабочий поток может помещать ответы в кольцевой буфер, соединяющий его с этим единственным потоком записи. Обходной поток однопользовательского потока проверяет каждый входящий кольцевой буфер и записывает данные клиентам. Опять же, оговорка о попытке записи перед выбором применяется, как и трюк о балансировке между несколькими буферами звонков и выбора вызовов.
Как вы указываете, есть много других опций:
Should I distribute networking load over several different sockets inside a single JVM and use some sort of load balancer like HAProxy to distribute load to multiple cores?
Вы можете сделать это, но IMHO это не лучшее использование для балансировки нагрузки. Это купит вам независимые JVM, которые могут выйти из строя самостоятельно, но, вероятно, будут медленнее, чем писать одно JVM-приложение, которое многопоточно. Само приложение может быть проще записать, поскольку оно будет однопоточным.
Or I should look towards using multiple Selectors in my NIO code?
Вы тоже можете это сделать. Посмотрите на архитектуру Ngnix для некоторых подсказок о том, как это сделать.
Or maybe even distribute the load between multiple JVMs and use Chronicle to build an inter-process communication between them?
Это также вариант. Хроника дает вам преимущество в том, что файлы с отображением памяти более устойчивы к процессу, выходящему посередине. Вы по-прежнему получаете много производительности, так как все общение осуществляется через разделяемую память.
Will testing on a proper serverside OS like CentOS make a big difference (maybe it is Windows that slows things down)?
Я не знаю об этом. Вряд ли. Если Java использует встроенные API Windows в полной мере, это не имеет большого значения. Я очень сомневаюсь в 40 миллионах транзакций/сек (без сетевого пространства пользователя + UDP), но перечисленные мной архитектуры должны делать очень хорошо.
Эти архитектуры, как правило, преуспевают, поскольку они представляют собой архитектуры с одним записывающим устройством, которые используют структуры данных на основе ограниченных массивов для связи между потоками. Определите, является ли многопоточный ответ даже ответом. Во многих случаях это не требуется и может привести к замедлению.
Еще одна область для изучения - схемы распределения памяти. В частности, стратегия выделения и повторного использования буферов может привести к значительным преимуществам. Стратегия повторного использования правильного буфера зависит от приложения. Посмотрите на схемы, такие как распределение памяти приятелей, распределение арены и т.д., Чтобы узнать, могут ли они вам помочь. JVM GC делает много штрафа для большинства рабочих нагрузок, поэтому всегда измерьте, прежде чем идти по этому маршруту.
Конструкция протокола также сильно влияет на производительность. Я предпочитаю длинные префиксные протоколы, потому что они позволяют вам распределять буферы правильных размеров, избегая списков буферов и/или слияния буфера. Длинные префиксные протоколы также позволяют легко решить, когда передать запрос - просто отметьте num bytes == expected
. Фактический синтаксический анализ может выполняться рабочей нитью. Сериализация и десериализация выходят за пределы протоколов с префиксом длины. Здесь можно использовать шаблоны, такие как мухи-паттерны над буферами вместо распределения. Посмотрите на SBE для некоторых из этих принципов.
Как вы можете себе представить, здесь можно написать весь трактат. Это должно привести вас в правильном направлении. Предупреждение. Всегда измерьте и убедитесь, что вам требуется больше производительности, чем самый простой вариант. Легко втянуть в бесконечную черную дыру улучшения производительности.