Статьи

Неблокирующие серверы Java и что я ожидаю от node.js, если он станет зрелым

node.js сейчас привлекает много внимания. Его цель — предоставить простой способ создания масштабируемых сетевых программ, например, веб-серверов. Это отличается, двумя способами. Прежде всего, это приносит Javacript на сервер. Но что более важно, он основан на событиях, а не на потоках, поэтому полагается на ОС, чтобы посылать ей события, когда, скажем, установлено соединение с сервером, чтобы он мог обработать этот запрос.

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

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

Что ж, чтение этих утверждений заставляет меня думать, что у меня есть проблемы в моем мире, которые нужно решать! Но потом я смотрю на свой мир и понимаю, что у меня нет проблем. Как это может быть? Ну, во-первых, потому что у нас нет потока на запрос, скорее мы используем пул потоков, уменьшая накладные расходы памяти и ставя в очередь входящие запросы, если мы не можем сразу запустить их в потоке, пока поток не освободится в бассейн.

Вторая причина, по которой у меня нет проблем с потоками, заключается в том, что я работаю с Java EE. Я пишу программное обеспечение для бизнеса, которое написано как компоненты, которые развертываются на сервере приложений. Именно этот сервер приложений выполняет всю сложную работу с потоками. Все, что мне нужно сделать, это следовать некоторым простым правилам и правилам здравого смысла, чтобы избежать проблем с многопоточностью, таких как обеспечение того, чтобы общие ресурсы, такие как Map (словарь), создавались с использованием поточно-безопасного API, который предлагает Java. В более экстремальных случаях я использую ключевое слово synchronized для защиты разделяемых методов или объектов.

Все это означает, что мы, Java-программисты, стали очень эффективными в написании программного обеспечения для решения бизнес-задач, а не в написании программного обеспечения для решения технических проблем, таких как параллелизм, масштабируемость, безопасность, транзакции, ресурсы, объединение объектов и ресурсов и т. Д. И т. Д. И т. Д.

Итак, когда мне нужно решить те проблемы, которые создает множество потоков, а именно проблемы с памятью? Любой сервер, которому необходимо поддерживать открытое соединение с клиентом для передачи потокового видео или передачи голоса по IP (VOIP) или обработки мгновенных сообщений, — это сервер, на котором у меня могут возникнуть проблемы с памятью, если у меня есть поток за соединение.

Теперь спецификации Java EE, как правило, ориентированы на многопоточные серверы. Насколько мне известно, не существует спецификации Java EE, которая бы сообщала производителям, как создать сервер, который позволит людям развертывать на нем программные компоненты, которые обрабатываются неблокирующим образом. Конечно, вы могли бы написать совместимый с сервлетами сервер, такой как Tomcat, с неблокирующим механизмом под капотом, но Java EE не говорит о потоковой передаче. И, вероятно, 99% веб-сайтов не выполняют потоковую передачу, которая может вызвать проблемы с памятью.

Поэтому, безусловно, бывают случаи, когда что-то вроде node.js будет полезно. Или, по крайней мере, идея неблокирующего сервера. Но, глядя на детали, node.js я бы не использовал всерьез, если бы мне нужен был такой сервер. Основные проблемы заключаются в том, что:

  1. вы развертываете Javascript на сервере
  2. Кажется, он не определяет стандартизированный способ написания компонентов, поэтому я могу сосредоточиться на написании бизнес-программ, а не на решении технических проблем.
  3. хотя ходят слухи, что он быстрый, но исследования, подобные этому, предполагают, что он в два раза медленнее, чем Java
  4. По сравнению с Java, доступные сегодня API-библиотеки node.js и Javascript являются незрелыми — подумайте о таких вещах, как отправка электронной почты, ORM и т. д.
  5. У node.js есть политические проблемы, потому что он зависит от Google. Если Google не хочет поддерживать Javascript на сервере (их ядро ​​V8 разработано для Chrome, на стороне клиента), а для node.js требуется исправление или разработка для V8 для решения проблемы на стороне сервера, они могут его никогда не получить. Хорошо, у меня есть шанс исправить ошибку Java в Oracle ?

Это началось для меня с этого фрагмента кода, взятого с сайта node.js:

    var net = require('net');

    var server = net.createServer(function (socket) {
      socket.write("Echo server\r\n");
      socket.pipe(socket);
    });

    server.listen(1337, "127.0.0.1");

Что ж, если цель node.js состоит в том, чтобы действительно облегчить создание сервера и передать ему функцию для обработки запросов, я могу сделать это и в Java:

TCPProtocol protocol = new TCPProtocol(){

    public void handleRequest(ServerRequest request, 
                              ServerResponse response) {

        //echo what we just received
        try {
            response.write(request.getData());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
};

Configuration config = new Configuration(1337, protocol);
ServerFactory.createServer(config).listen();

ОК, это немного сложнее. Но причина в том, что я решил написать неблокирующий сервер, который можно настроить для обработки любых запросов. Конфигурация принимает номер порта для сервера и объект протокола. Объект протокола является подклассом абстрактного протокола, и его задача состоит в том, чтобы посмотреть на данные, поступающие из сети, и решить, как их обработать. TCPProtocol, как показано выше, является самым простым протоколом в том смысле, что он ничего не делает. Вы должны указать ему, что делать, переопределив метод handleRequest () (аналогично предоставлению функции в Javascript), например, как показано выше, вернув клиенту то, что он отправил на сервер.

Но я хотел сделать что-то более полезное, поэтому я расширил то, что я написал, для обработки голоса по IP или, по крайней мере, для клиента, передающего MP3 на другой клиент, для имитации телефонного звонка между ними.

Первым шагом была разработка протокола. На уровне байтов первый байт в пакете, отправляемом на сервер, содержит команду или действие, второй байт содержит длину полезной нагрузки, если таковая имеется, и последующие байты содержат полезную нагрузку.

Затем протокол позволяет войти в систему вызывающему абоненту (возвращая идентификатор сеанса), начать вызов (возвращая OK / NOK), отправить данные для вызова, завершить вызов и выйти (выйти из системы).

Через несколько часов мой ноутбук воспроизводил мои любимые MP3-файлы, передаваемые на мой ноутбук с другого компьютера. Но это выглядело неуклюже. Я подумал и черпал вдохновение из спецификаций Servlet и EJB в Java EE. А именно, я хотел иметь развертываемые компоненты, единственной задачей которых было выполнение бизнес-требований.

В .NET и Java вы можете определить веб-сервис или веб-страницу (сервлет) с помощью аннотирования класса. В аннотации вы указываете URL-путь, по которому сервер определяет, когда вызывать ваш сервлет или веб-службу. Это похоже на «команду» или «действие», как в моем протоколе VOIP.

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

В совокупности это выглядит следующим образом. Первый бит — это метод обработчика протокола, который определяет команду и отправляет ее обработчику:

    public void handleRequest(ServerRequest request,
	                          ServerResponse response) {

        String criterion = String.valueOf(request.getData()[0]);
        try{
            //call through to the context for help - it
            //will call the handler for us.
            getContext().handleRequest(criterion, request, response);
        } catch (UnknownHandlerException e) {
            //send NOK to client because of unknown command
            response.write(new Packet(UNKNOWN_COMMAND).marshal());
        }
    }

Объект Packet в приведенном выше коде — это просто инкапсуляция данных, передаваемых по проводам, но он знает, что первый байт — это команда, второй — длина, а последующие байты — полезная нагрузка. В HTTP-запросе этот объект Packet должен быть чем-то, что знает об атрибутах заголовка HTTP и полезной нагрузке HTTP, а не объектами ServerRequest и ServerResponse, которые знают только о байтовых массивах и сокетах / каналах.

Вторая часть решения — это обработчики. Они содержатся в конфигурации, либо создаются программно, как показано в примере TCP выше, либо создаются с использованием XML, который сервер читает при запуске. Затем контейнер находит, создает экземпляры и вызывает эти обработчики от имени протокола, когда объект протокола вызывает getContext (). HandleRequest (критерий, запрос, ответ) в приведенном выше фрагменте. Типичный обработчик выглядит так:

@Handler(selector=VoipProtocol.LOGIN)
public class VoipLoginHandler extends VoipHandler {

    public void service(VoipRequest request,
                        VoipResponse response)
                        throws IOException {
		
        String name = request.getPacket().getPayloadAsString();
        Participant p = new Participant(
                        name, response.getSocketChannel());
        getProtocol().getModel().addParticipant(p);
        request.getKey().attach(p);
		
        //ACK
        String sessId = p.getSessId().getBytes(VoipProtocol.CHARSET);
        Packet packet = new Packet(sessId, VoipProtocol.LOGIN)
        response.write(packet);
	}

}

Таким образом, метод обслуживания является единственным в обработчике и отвечает за ведение бизнеса. Здесь нет ничего дурного. Код определяет, кто входит в систему, создает их экземпляр (объект участника) в модели. Модель содержится в объекте протокола, который существует только один раз на сервере. По сравнению с сервлетом вызов для получения модели аналогичен вводу данных в область приложения. Затем код присоединяет объект-участник к клиентскому соединению с помощью метода attach (Object) (см. Java NIO Package), чтобы мы всегда могли сразу перейти к соответствующей части модели данных из объекта соединения, когда поступят последующие запросы. Наконец, код отвечает клиенту с идентификатором сеанса в качестве подтверждения. Обратите внимание, что здесь у меня нетне удосужился аутентифицировать пароль — полезная нагрузка содержит только имя пользователя. Но если бы я писал полное приложение, у меня было бы больше данных в моей полезной нагрузке, возможно, даже что-то вроде XML или JSON, и я бы аутентифицировал имя пользователя и пароль.

Аннотация @Handler в верхней части класса-обработчика аналогична аннотации @WebServlet, применяемой к сервлетам Java EE. Он имеет атрибут «селектор», который контейнер использует для сравнения с критерием, который протокол извлекает из запроса. Это аналогично атрибуту шаблонов URL в @WebServlet, который сообщает веб-контейнеру Java EE, на какой путь должен быть отображен сервлет. Также есть немного скрытой магии — метод обслуживания в обработчике уже знает VoipRequest и VoipResponse, а не ServerRequest и ServerResponse. Суперкласс совершает эту магию, реализуя стандартный метод обслуживания и вызывая специализированный абстрактный метод обслуживания, реализованный в подклассах.

Поэтому, проявив творческий подход, я добавил еще один атрибут в аннотацию @Handler. Он называется runAsync и по умолчанию имеет значение false. Но, если для него установлено значение true, контейнер отправляет обработчик в пул потоков для выполнения в будущем. На самом деле я не использую это в примере VOIP, но я сделал это, чтобы показать, что вполне возможно, что сервер приложений может делать такие вещи. Разработчику не нужно беспокоиться о потоках или о чем-либо — они просто настраивают аннотацию, а контейнер обрабатывает сложные части. Это типично для Java EE! И это становится чрезвычайно полезным в тех случаях, когда для выполнения запроса требуется немного больше времени. В неблокирующем однопоточном процессе, хотя соединения одновременно подключены к серверу, они обслуживаются последовательно, что означает, что они ДОЛЖНЫ возвращаться быстро, если вы нене хочу, чтобы те в очереди ждали слишком долго. Это то, что node.js НЕ МОЖЕТ сделать, потому что у него нет способа запуска потоков. Их решение — отправить асинхронный запрос другому процессу. Но чтобы сделать это, разработчик тратит время на работу над техническими проблемами, вместо того чтобы взяться за использование контейнера и заставить его сделать это для них, чтобы они могли тратить больше времени на написание экономичного бизнес-кода. Одна из основных причин использования Java EE заключается в том, что разработчик может тратить больше времени на написание бизнес-программ и меньше времени на решение технических проблем. Нет причины, по которой у обработчика не могло быть и других аннотаций:разработчик тратит время на работу над техническими вопросами, а не на то, чтобы заставить контейнер делать это за них, чтобы они могли тратить больше времени на написание экономичного бизнес-кода. Одна из основных причин использования Java EE заключается в том, что разработчик может тратить больше времени на написание бизнес-программ и меньше времени на решение технических проблем. Нет причины, по которой у обработчика не могло быть и других аннотаций:разработчик тратит время на работу над техническими вопросами, а не на то, чтобы заставить контейнер делать это за них, чтобы они могли тратить больше времени на написание экономичного бизнес-кода. Одна из основных причин использования Java EE заключается в том, что разработчик может тратить больше времени на написание бизнес-программ и меньше времени на решение технических проблем. Нет причины, по которой у обработчика не могло быть и других аннотаций:

@Handler(selector=VoipProtocol.QUIT)
@RolesAllowed({"someRole", "someOtherRole"})
@TransactionManagement(TransactionManagementType.CONTAINER)
public class VoipQuitHandler extends VoipHandler {

    @PersistenceContext(unitName="persistenceUnitName")
    private EntityManager em;
    .
    .
    .
}

Аннотация @ RolesAllowed @ TransactionManagement означает, что транзакции будут обрабатываться контейнером, а не программистом. Когда обработчик выполняется, аннотация @PersistenceContext заставляет контейнер внедрить диспетчер сущностей JPA, чтобы у бизнес-кода был доступ к базе данных. Это точно так же, как сервлет или EJB получает ресурсы из контейнера. Эти ресурсы создаются на основе конфигурации сервера приложений стандартным способом и управляются также контейнером (объединение в пул, повторное подключение и т. Д.), Опять же, освобождая программиста от этой нагрузки.

Так что у нас сейчас? Вместо низкоуровневого API, такого как node.js, у нас есть высокоуровневый контейнер для запуска программных компонентов на неблокирующем сервере. Чего у нас нет, так это того, что Javascript работает на сервере, потому что он казался идеальным языком для обработки обратных вызовов в неблокирующей среде, потому что он имеет очередь событий и указатели на функции.

Может ли это небольшое упражнение стать основой JSR? Ну, это действительно полезно? Только в тех случаях, когда у вас есть клиенты, которым необходимо длительное время держать открытыми соединения с сервером, и у вас есть тысячи клиентов. Для этого не нужно будет много людей, и есть более важные JSR, ожидающие принятия и реализации. Но кто знает, что принесет будущее.

Подводя итог, мне кажется, что node.js беспокоит меня, потому что мне не нравится идея Javascript и его незрелого стека библиотек, развертываемых на рабочем сервере. Всякий раз, когда я разрабатываю с помощью Javascript, я трачу на отладчик гораздо больше времени, чем хотелось бы, потому что язык не полностью проверяется с помощью статического анализа, потому что он типизирован по утке. Но идея построения неблокирующего сервера с использованием Java — теперь это интересно (по крайней мере, мне). Но, как я уже сказал, я просто не уверен, как часто это будет полезно.

Мне кажется, что революция, которую запускает node.js, на самом деле касается не Javascript на сервере. Это больше об использовании неблокирующего сервера. Но проблема в том, что, вероятно, 99% наших потребностей уже удовлетворены стандартными многопоточными серверами. И мы уже можем писать масштабируемые веб-сайты без всех тех проблем, которые, как заявляет node.js, у нас есть. Поэтому давайте не будем перестраивать мир на основе неблокирующих операций ввода-вывода только потому, что прибыл node.js. Давайте построим / пересоберем только те особые случаи, когда неблокирующий ввод-вывод нам действительно поможет.

Заслуживает ли node.js шумиху, которую он получает? Я не думаю, что это заслуживает ажиотажа, потому что вы можете запустить Javascript на сервере — это плохо. Дуглас Крокфорд (старший архитектор JavaScript в Yahoo!) даже намекает на это, когда
говорит :

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

Кажется, он говорит, что Javascript
лидируетпуть в самом важном шаге, который мы когда-либо предпринимаем, а не то, что Javascript — это самый важный шаг, который мы делаем. То есть неблокирующие серверы — это самый важный шаг, а наличие цикла событий на сервере — это путь вперед. Насколько я понимаю, он говорит, что неблокирование — это революция. Проблема, с которой я сталкиваюсь в этой революции, заключается в том, что неблокирующие серверы не всегда являются лучшими. Они действительно помогают, когда у вас есть тысячи клиентов, которым нужно держать свои подключения к серверу открытыми. HTTP (99% Интернета) не нуждается в неблокирующем вводе / выводе, чтобы стать технологическим лидером сети — это уже есть. Поэтому вместо того, чтобы присоединиться к этой революции и всей шумихе, которую вызывает node.js, я проигнорирую ее и продолжу создавать программное обеспечение таким, каким я был.

Код для приведенных выше примеров находится в двух проектах Eclipse, которые вы можете скачать
здесь, Первый проект — это сама структура, включающая неблокирующий сервер, контейнер и соответствующие классы и интерфейсы. Второй проект является примером того, как использовать инфраструктуру для создания (бизнес) приложений. Он содержит класс TCPServerRunner для запуска эхо-сервера TCP. Он также содержит VoipServerRunner, который запускает VOIP-сервер. Чтобы запустить пример потоковой передачи, сначала запустите ListeningClient, а затем SendingClient. Измените строку 74 в SendingClient для использования вашего любимого MP3, и вы должны услышать, как он воспроизводится в течение первых 20 секунд. Сам VOIP-сервер не является надежным на 100% — особенно после отключения клиентов. Но я думаю, что node.js не был так стабилен после всего лишь нескольких часов разработки. Удачи!

PS. Производительность: потоковая передача со скоростью 192 килобита в секунду, сервер сказал мне, что он работает примерно на половине процента нагрузки (то есть цикл событий простаивает в 99,5% времени). GSM, а не высококачественный MP3, использует скорость около 30 Кбит / с, поэтому для реального VOIP-сервера вы, вероятно, можете обойтись с 1200 одновременными вызовами. Это не так уж много, но я понятия не имею. Я предполагаю, что у меня есть 2 ядра ЦП, которые я мог бы использовать для распределения нагрузки между двумя процессами, что является способом node.js для использования всех ядер. Но балансировщик нагрузки не будет выполнять намного меньше работы, чем мой сервер, поэтому может не справиться с какой-либо дополнительной нагрузкой. Или я могу вставить свои обработчики в пул потоков, используя атрибут runAsync моей аннотации @Handler. Таким образом, недорогой товарный сервер может обрабатывать почти 2500 одновременных вызовов.Все еще не так много? Опять же, я действительно не знаю. Но я должен сказать, что я не пытался оптимизировать сервер. Размеры моего пакета основаны на отправке одного пакета звуковых данных каждые 12 миллисекунд. Возможно, я мог бы посылать их реже, что могло бы улучшить пропускную способность и при этом иметь хорошее качество вызовов с низкой задержкой. Кто знает — кто хочет оптимизировать его, дайте мне знать результаты! Одно можно сказать наверняка, что сервер, использующий один поток на соединение с 2500 одновременными соединениями, будет бороться. Пока мойКто знает — кто хочет оптимизировать его, дайте мне знать результаты! Одно можно сказать наверняка, что сервер, использующий один поток на соединение с 2500 одновременными соединениями, будет бороться. Пока мойКто знает — кто хочет оптимизировать его, дайте мне знать результаты! Одно можно сказать наверняка, что сервер, использующий один поток на соединение с 2500 одновременными соединениями, будет бороться. Пока мой
предыдущая статья в блоге показала, что возможно иметь много тысяч открытых потоков, переключение контекста может стать узким местом. Неблокирующий сервер — это, безусловно, путь для этого варианта использования.

Загрузите код
здесь .

От http://blog.maxant.co.uk/pebble/2011/05/22/1306092969466.html