java и php взаимодействие
PHP и JavaScript
— клиентский язык сценариев, который является очень эффективным средством решения многих задач, не требующих серверной обработки. Безусловно, JavaScript может применяться для создания далеко не всех приложений, поскольку в нем до сих пор не устранены некоторые недостатки, касающиеся удобства и простоты, а также защиты информации, но он предоставляет возможность использования таких сочетаний клиентского и серверного языков программирования, которые открывают целый ряд привлекательных функциональных возможностей.
В языках PHP и JavaScript предусмотрены разные объектные системы обозначений. В языке JavaScript используется так называемая уточняющая запись через точку в стиле C#, тогда как в языке PHP используются стрелки, иначе говоря, система обозначений в стиле C++. Язык JavaScript полностью объектно-ориентирован, тогда как в языке PHP объекты рассматриваются как необязательное средство. Такое различие систем обозначений имеет свое преимущество в том, что невозможно когда-либо спутать объект JavaScript с объектом PHP, или наоборот. А недостатком является то, что отсутствует возможность обратиться к одному и тому же объекту из обоих языков.
Вывод кода JavaScript с помощью сценария PHP
Язык PHP является серверным (поскольку для интерпретации кода на этом языке используется серверное приложение), а язык JavaScript — клиентским (аналогичное замечание), поэтому может показаться, что при использовании того и другого языка в одном и том же сценарии формирования страницы будут возникать проблемы. Но в действительности именно благодаря указанному различию эти два языка хорошо дополняют друг друга.
Безусловно, язык PHP обладает большими возможностями по созданию динамически формируемых веб-страниц, но PHP — исключительно серверный язык. К тому же есть одна общая категория задач, решаемых на веб-сайте, для которых чаще всего не требуется вся обрабатывающая мощь сервера, и поэтому эти задачи быстрее и проще решаются в клиентском программном обеспечении. К таким задачам, например, относится изменение внешнего вида кнопки при перемещении над ней курсора мыши. Для решения многих подобных задач можно легко применить язык JavaScript, который представляет собой чисто клиентский язык (безусловно, имеется и серверная версия этого языка, но предполагается, что для этой цели уже выбран язык PHP) и легко интегрируется с языком PHP.
С другой стороны, клиентский язык JavaScript сам по себе характеризуется многими ограничениями. Например, будучи клиентским языком, JavaScript не позволяет организовать непосредственное взаимодействие с базой данных, поэтому сценарии JavaScript не способны обновлять сами себя путем получения новых данных, зависящих от условий просмотра страницы.
Еще более важный недостаток состоит в том, что невозможно полностью полагаться на клиентские технологии, поскольку язык поддержки этих технологий может оказаться не установленным или запрещенным в браузере пользователя. Сознательные разработчики клиентских приложений для веб вынуждены либо выбирать вариант реализации кода наугад (и получать жалобы от пользователей менее распространенных браузеров), либо одновременно сопровождать несколько версий одного и того же сайта. (Несознательные разработчики просто берут за основу все возможности браузера, наиболее распространенного в среде пользователей, и, как один старый моряк, говорят: «К черту торпеды. «, но это — совсем другая история.) Язык PHP позволяет смягчить эффекты, связанные с неопределенностью поддержки клиентского программного обеспечения.
Широкие возможности вывода данных в языке PHP
Одной из наиболее важных особенностей языка PHP является то, что с его помощью можно обеспечить вывод кода в различных форматах. В частности, PHP может применяться (и применяется) для вывода открытого текста, кода HTML, XHTML, JavaScript, XML, MathML, файлов в различных графических форматах, кода CSS, XSL и даже (что трудно сначала представить себе) кода ASP.NET.
Кроме того, нет каких-либо реальных технических сложностей, которые не позволяли бы выводить с помощью языка PHP код C, хотя, по-видимому, такой способ применения PHP не находит слишком широкого распространения. Следует помнить, что язык PHP не используется исключительно для вывода кода HTML — конечным продуктом применения этого языка обычно становится код, предназначенный для обработки другим приложением, обычно браузером.
Для вывода кода JavaScript с помощью языка PHP может применяться целый ряд способов. Простейшим из них является выход из режима PHP каждый раз, когда требуется непосредственно включить какой-то текст в код страницы, передаваемый в клиентскую программу. Данный способ осуществляется точно по такому же принципу, как происходит выход из режима PHP для формирования кода HTML. Например:
Как и при формировании в сценарии PHP кода HTML, в некоторых случаях код JavaScript можно формировать, не выходя из режима PHP. К тому же подобный стиль организации работы сценариев PHP может соответствовать личным предпочтениям разработчика. В этом случае для вывода кода JavaScript может использоваться оператор echo или print языка PHP, как в следующем примере:
Стиль, в котором вывод кода JavaScript осуществляется полностью в режиме PHP, нельзя считать совсем неправильным, но при его использовании значительно затрудняется создание безошибочно действующих сценариев. Поэтому такой стиль следует отдать в распоряжение опытных программистов, а менее опытные программисты должны применять его исключительно в тех случаях, когда в код формируемой страницы требуется включить вызовы заранее определенных функций JavaScript, например, касающихся событий onsubmit.
Области применения языка JavaScript
Клиентский код JavaScript не обладает чрезвычайно большими функциональными возможностями, но позволяет быстрее решать некоторые задачи, а также создавать такие эффекты, которые довольно сложно воспроизвести с помощью PHP. Ниже перечислены такие функциональные средства кода, которые действительно следует дополнить или заменить в коде HTML-страницы функциями JavaScript:
Простые арифметические операции в формах и калькуляторах (такие как вычисление промежуточной суммы по товарам, собранным в тележку для покупок, и расчет платежей по ипотечной задолженности).
Простая проверка правильности формы (например, определение наличия символов @ в адресах электронной почты).
Реализация средств навигации сайта (например, создание разворачивающихся меню навигации).
Создание всплывающих меню (предупреждений, окон поиска).
Обработка событий, обнаруживаемых в браузере (mouseover, onclick).
Резервирование функциональных средств кода JavaScript с помощью кода PHP
Безусловно, как показывают приведенные выше рекомендации, язык JavaScript позволяет расширить функциональные возможности сайта, но если язык JavaScript не установлен в браузере или его использование запрещено, то компенсировать отсутствие поддержки JavaScript можно с помощью PHP. Дело в том, что иногда существует возможность безукоризненно выполнить одну и ту же задачу и с помощью клиентского, и с помощью серверного метода.
Если в браузере посетителя будет разрешено применение языка JavaScript, то посетитель получает возможность воспользоваться более быстрым методом. В противном случае применяется менее быстрый, резервный метод, но обычно посетители даже не замечают, что в коде сайта реализуется подобный выбор. А если бы не было возможности вместо кода JavaScript переходить к использованию кода PHP, то пользователи браузера без поддержки JavaScript были бы полностью лишены доступа к функциональным средствам сайта.
Идеальным примером резервирования кода JavaScript с помощью кода PHP является дважды зарезервированное разворачивающееся меню для навигации по страницам сайта без использования ссылок. Код JavaScript обеспечивает мгновенное перенаправление, а если в браузере не разрешено использование JavaScript, то в действие вступает код PHP, с помощью которого достигается такой же результат, но после более продолжительного ожидания.
В этом примере взят за основу тот факт, что в коде JavaScript имеются обработчики событий (например, onchange), которые выполняют свои действия в результате прохождения курсора над элементами формы HTML, поэтому фактически не требуют передачи данных формы щелчком на кнопке. Это означает, что кнопка Submit может быть зарезервирована для использования в коде PHP. В примере ниже показан сценарий формирования HTML-страницы, в котором используется перенаправление с помощью события onchange языка JavaScript, а если этот код не работает, то применяется обработчик формы PHP:
Обработка формы с помощью JavaScript
В файле обработчика формы PHP, называемом redirect.php, достаточно предусмотреть только две строки:
Аналогичное разделение труда можно предусмотреть при проверке правильности формы. Если разрешено использование языка JavaScript, то с его помощью можно убедиться в том, что номера телефонов имеют десять цифр, а адреса электронной почты включают и символ @ и еще какие-то символы. Если же применение JavaScript не разрешено, то можно написать небольшой сценарий PHP, который выполняет такие же действия при передаче данных формы и возвращает сообщение с предупреждением, если введенные значения являются неправильными.
Сравнение методов статического и динамического формирования кода JavaScript
Безусловно, статическая форма JavaScript — PHP, приведенная в примере выше, удобна для использования во многих приложениях, но она имеет один существенный недостаток — ее приходится сопровождать вручную. Каждый раз, когда потребуется добавить новую страницу к сайту, необходимо будет вспоминать о том, что ссылку на эту страницу необходимо ввести вручную в код формирования раскрывающегося списка. На первый взгляд может показаться, что в этом нет ничего особенного, но из массы таких мелочей складывается невероятно трудоемкая работа, когда приходится сопровождать огромный сайт с интенсивным трафиком.
Используя язык PHP в сочетании с базой данных, вы можете обеспечить автоматическое обновление определенной части кода JavaScript каждый раз после ввода новых данных в базу данных, иначе говоря, обеспечить динамическое обновление. К достижению такой цели следует стремиться при любой возможности, поскольку в долговременном плане это помогает экономить время. В примере ниже показано, как можно модифицировать форму, чтобы обеспечить лучшую интеграцию клиентского и серверного кода:
Исходная страница с динамически генерируемой формой
Несомненно, читатель уже понял, что аналогичный метод может оказаться буквально бесценным, даже если создается простая форма JavaScript (например, такая, в которой используется обработчик событий onsubmit, а не onchange). В частности, этот метод позволяет модифицировать вызовы обычных функций JavaScript, поскольку язык PHP позволяет изменять значения переменных в вызове функции, до того как код этого вызова будет отправлен в браузер. Поэтому при желании сценарии PHP могут использоваться непосредственно для вывода кода JavaScript, в котором предварительно осуществляется подстановка значений переменных, взятых из какого-то источника данных.
Передача данных в обратном направлении, из кода JavaScript в код PHP
Наконец, рассмотрим, как завершить цикл обработки данных, снова передав значения формы в код PHP из кода JavaScript. В примере ниже показано, как использовать код JavaScript, для передачи в сценарий PHP простого объекта:
Передача объекта из кода JavaScript в код PHP с использованием формата JSON Взаимодействие JavaScript и PHP Безопасность PHP и XML
Мост между PHP и Java
С тех пор, как я написал пост, и в особенности в последний месяц, было опубликовано пару материалов, которые демонстрируют важность этой технологии/ Но я все еще должен углубиться сильнее. тут приводится короткое разъяснение что к чему.
Что это такое
ПО сути, мост между PHP и Java дает разработчикам доступ к Java коду из их приложения и наоборот. Польза в том, что вы можете использовать библиотеки или службы воплощенные в одной платформе приложением другого окружения.
Это метод отличается от стандартного использования фронтэндп HTTP серверов для индивидуальных вызовов PHP скриптов или J2EE приложений. PHP и Java приложения могут вызывать друг-друга без вовлечения окружения HTTP-сервера.
Пока вы используете связь между API Веб сервисов поверх HTTP, PHP-Java мосты позволяют вызывать API удаленного приложения прямо из кода программы. ПО этому они на порядок результативнее обычных сетевых запросов.
Где узнать больше
Хорошо документированный OpenSource мост между PHP и Java производит впечатление наиболее зрелого в даной технологии. Вы можете найти много информации по ссылкам на теме проекта.
IBM developerWorks сделали первый взнос в серию «Разарбатывай на Java и PHP на AIX 5.3». Но это требует Unix окружения.
Пока нет большого количества информации, но я надеюсь узнать больше, когда\если Энди Гутман вывесит презентацию с JavaOne, которую он описывал в своем болге.
Я не уверен в информации о PHP Integration Kit и не уверен, что она изменилась со времен публикации alphaWorks технологии в прошлом году.
Производительность I/O бэкэнда: Node vs. PHP vs. Java vs. Go
Понимание модели ввода/вывода вашего приложения может привести и к пониманию различий между приложением, работающим с нагрузкой, под которой оно создавалось, и тем, которое лицом к лицу столкнулось с реальным способом своего применения. Возможно, если ваше приложение невелико и не создаёт большой нагрузки, то для него это не так важно. Но по мере роста трафика использование ошибочной модели ввода/вывода может погрузить вас в мир боли.
Как и в большинстве других ситуаций с несколькими возможными решениями, дело не в том, какой из вариантов лучше, дело в понимании компромиссов. В этой статье мы сравним Node, Java, Go и PHP из-под Apache, обсудим модели ввода/вывода в разных языках, рассмотрим достоинства и недостатки каждой модели и прогоним простенькие бенчмарки. Если вас волнует производительность ввода/вывода вашего следующего веб-приложения, то эта статья для вас.
Основы ввода/вывода: освежим знания
Для понимания факторов, относящихся к вводу/выводу, сначала нужно вспомнить некоторые концепции, используемые на уровне ОС. Маловероятно, что со многими из них вам придётся иметь дело напрямую, скорее всего, вы будете работать с ними опосредованно, через runtime-окружение приложения. И подробности играют важную роль.
Системные вызовы
Возьмём сначала системные вызовы, которые можно описать так:
Ваша программа (в так называемом пользовательском пространстве) должна просить ядро операционной системы выполнить операцию ввода/вывода от имени вашей программы.
Системные вызовы — это способ, с помощью которого программа просит ядро что-то сделать. Специфика их реализаций зависит от ОС, но базовый принцип везде один и тот же. Должна быть какая-то конкретная инструкция для передачи управления из вашей программы через ядро (как вызов функции, только со специальной «добавкой» для работы в такой ситуации). В целом системные вызовы блокирующие, т. е. программа ждёт, пока ядро не вернётся к вашему коду.
Блокирующие и неблокирующие вызовы
Выше говорилось, что системные вызовы — блокирующие, и в целом это так. Однако некоторые вызовы можно охарактеризовать как неблокирующие. Это означает, что ядро принимает ваш запрос, кладёт его в очередь или какой-то буфер, а затем безо всякого ожидания немедленно возвращается к выполняемому в данный момент вводу/выводу. Так что «блокирование» происходит лишь на очень небольшой период времени, достаточный для постановки вашего запроса в очередь.
Чтобы было понятнее, вот некоторые примеры (системных вызовов Linux):
Важно понимать различия в тайминге. Если ядро процессора работает на частоте 3 ГГц, без каких-либо оптимизаций, то оно выполняет 3 миллиарда тактов в секунду (3 такта в наносекунду). Для неблокирующего системного вызова могут потребоваться десятки тактов, то есть несколько наносекунд. Вызов, блокирующий получение информации по сети, может выполняться гораздо дольше: например, 200 миллисекунд (1/5 секунды). То есть если неблокирующий вызов длится 20 наносекунд, то блокирующий — 200 миллионов наносекунд. Процесс ждёт выполнения блокирующего вызова в 10 миллионов раз дольше.
Ядро предоставляет средства для выполнения как блокирующих («считай данные из сетевого подключения и дай их мне»), так и неблокирующих («скажи мне, когда в этих сетевых подключениях появятся новые данные») вводов/выводов. И в зависимости от выбранного механизма длительность блокировки вызывающего процесса будет разительно отличаться.
Диспетчеризация (Scheduling)
Диспетчеризация также крайне важна, если у вас есть много потоков выполнения или процессов, которые начинают блокировать.
Для нашей задачи разница между процессом и потоком выполнения невелика. В реальной жизни самое главное отличие между ними с точки зрения производительности заключается в том, что поток выполнения использует одну и ту же область памяти, а процессы получают собственные области. Поэтому отдельные процессы требуют гораздо больше памяти. Но если мы говорим о диспетчеризации, то всё сводится к тому, сколько потокам и процессам нужно времени выполнения на доступных ядрах процессора. Если у вас есть 300 потоков и восемь ядер, то придётся поделить время так, чтобы каждый поток получил свою долю: каждое ядро недолго выполняет один поток, а затем переходит к следующему. Это делается с помощью переключения контекста, когда процессор переключается с одного выполняемого потока/процесса на другой.
Но с этими переключениями контекста связаны определённые затраты — они занимают какое-то время. Иногда это может происходить меньше, чем за 100 наносекунд, но нередко переключение занимает 1000 наносекунд и больше, в зависимости от особенностей реализации, скорости/архитектуры процессора, его кеша и т. д.
И чем больше потоков выполнения (или процессов), тем больше переключений контекста. Если речь идёт о тысячах потоков, когда на переключения с каждого из них уходят сотни наносекунд, то всё выполняется очень неторопливо.
Однако неблокирующие вызовы по существу говорят ядру: «Вызови меня только тогда, когда появятся новые данные или событие в одном из этих подключений». Эти вызовы созданы для эффективной обработки большой нагрузки по вводу/выводу и уменьшения количества переключений контекста.
Ещё не потеряли нить рассуждения? Сейчас начинается самое интересное: мы рассмотрим, что некоторые популярные языки могут делать с вышеописанными инструментами, и сформулируем выводы о компромиссах между простотой использования и производительностью… и другими интересными пикантностями.
В качестве примечания: в статье приведены тривиальные примеры (в некоторых случаях неполные, когда будут показаны только релевантные биты). Обращения к базе данных, внешние системы кеширования (Memcache и т. д.) и многие другие вещи в результате будут выполняться под капотом, но, по сути, это те же вызовы операций ввода/вывода, который окажут то же влияние, что и приведённые в статье простенькие примеры. В сценариях, в которых ввод/вывод описан как блокирующий (PHP, Java), HTTP-запросы и операции чтения и записи сами по себе являются блокирующими вызовами: в системе скрыто немало операций ввода/вывода, что приводит к проблемам с производительностью, которые надо учитывать.
На выбор языка программирования для проекта влияет много факторов. Также немало факторов влияет на производительность. Но если вас беспокоит, что ваша программа в основном упрётся во ввод/вывод, если производительность ввода/вывода жизненно важна для проекта, то вам нужно знать обо всех этих факторах.
В 1990-х многие носили обувь Converse и писали CGI-скрипты на Perl. Затем появился PHP, и хотя его любят критиковать, но этот язык сильно облегчил создание динамических веб-страниц.
PHP использует очень простую модель. Существует ряд вариаций, но среднестатистический PHP-сервер выглядит так.
Конечно, реальный код просто встроен прямо в вашу страницу, а операции являются блокирующими:
Вот как это выглядит с точки зрения интеграции в систему:
Всё просто: один процесс на запрос. Вызовы ввода/вывода просто блокируют. Достоинства? Схема простая, и она работает. Недостатки? После 20 тысяч параллельных клиентских обращений сервер просто расплавится. Этот подход плохо масштабируем, потому что не используются предоставляемые ядром ОС инструменты для работы с большим объёмом ввода/вывода (epoll и пр.). Ситуацию усугубляет то, что выполнение отдельного процесса на каждый запрос приводит к потреблению большого объёма системных ресурсов, особенно памяти, которая зачастую заканчивается в первую очередь.
Примечание: в Ruby используется очень похожий подход, и в широком, общем смысле в рамках нашей статьи они могут считаться одинаковыми.
Многопоточный подход: Java
Java пришёл в те времена, когда вы как раз купили своё первое доменное имя, и было так круто везде в разговоре вставлять словечки «точка ком». В Java встроена многопоточность (multithreading) — отличная функция (особенно на момент своего создания).
Большинство Java веб-серверов создают новый поток выполнения для каждого поступающего запроса, и уже в этом потоке в конце концов вызывают функцию, которую написали вы, разработчик приложения.
Выполнение ввода/вывода в Java Servlet выглядит так:
Поскольку наш метод doGet соответствует одному запросу и выполняется в собственном потоке, то вместо отдельного процесса для каждого запроса, требующего отдельной памяти, мы получаем отдельный поток выполнения. Это даёт приятные возможности, например можно поделиться состоянием или закешировать данные между потоками, потому что они способны обращаться к памяти друг друга. Но оказываемое при этом влияние на взаимодействие с диспетчером почти аналогично тому, что и в примере с PHP. Каждый запрос получает новый поток, и различные операции ввода/вывода блокируют внутри потока до тех пор, пока запрос не будет полностью выполнен. Потоки объединяются (pooled), чтобы минимизировать стоимость их создания и уничтожения, но в любом случае если у нас тысячи подключений, то создаются тысячи потоков, что плохо сказывается на работе диспетчера.
Важным поворотным моментом в Java 1.4 (и значительным апгрейдом в 1.7) стала возможность выполнения неблокирующих вызовов ввода/вывода. Большинство приложений, веб- и прочих, их не используют, но они хотя бы есть. Некоторые Java веб-серверы пытаются как-то применять преимущества неблокирующих вызовов, однако подавляющее большинство развёрнутых Java-приложений всё ещё работает так, как описано выше.
Java из коробки обладает некоторыми хорошими возможностями с точки зрения ввода/вывода, но они всё же не решают проблем, которые возникают в приложениях, активно использующих ввод/вывод и сильно тормозящих из-за обработки многих тысяч блокирующих потоков выполнения.
Неблокирующий ввод/вывод: Node
Node.js снискал себе популярность с точки зрения хорошей производительности ввода/вывода. Любой, кто хотя бы вскользь познакомился с Node, говорит, что он неблокирующий, что он эффективно обрабатывает операции ввода/вывода. И в целом это так. Но дьявол в деталях, точнее в колдовстве, с помощью которого достигается хорошая производительность.
Суть сдвига парадигмы, реализуемого Node, такова: вместо того чтобы сказать: «Напиши здесь свой код для обработки запроса», он говорит: «Напиши здесь свой код для начала обработки запроса». Каждый раз, когда вам нужно использовать ввод/вывод, вы делаете запрос и отдаёте callback-функцию, который Node вызовет по окончании работы.
Типичный код Node для выполнения операции ввода/вывода по запросу выглядит так:
Здесь есть две callback-функции. Первая вызывается, когда стартует запрос. Вторая — когда становятся доступными данные файла.
По сути, это позволяет Node эффективно обрабатывать ввод/вывод между двумя callback-функциями. Более подходящий сценарий: вызов базы данных из Node. Но я не буду приводить конкретный пример, потому что там используется тот же принцип: вы инициируете вызов базы данных и даёте Node callback-функцию; он с помощью неблокирующих вызовов отдельно выполняет операции ввода/вывода, а когда запрошенные вами данные становятся доступны, вызывает callback-функцию. Этот механизм постановки в очередь вызовов ввода/вывода с последующим вызовом callback-функции называется циклом событий (event loop). И он хорошо работает.
Но под капотом модели есть уловка. По большей части она связана с реализацией JS-движка V8 (JS-движок Chrome, используемый Node). Весь JS-код, который вы пишете, выполняется в одном потоке. Задумайтесь над этим. Это означает, что в то время как ввод/вывод происходит с помощью эффективных неблокирующих методик, ваш JS выполняет все связанные с процессором операции в одном потоке, когда один кусок кода блокирует следующий. Характерный пример того, к чему это способно привести: циклический проход по записям базы данных, чтобы неким образом обработать их, прежде чем отдавать клиенту. Вот как это может работать:
Вся эта концепция основана на предпосылке, что операции ввода/вывода — самая медленная часть, а значит, важнее всего обрабатывать их как можно эффективнее, даже за счёт последовательной обработки всех остальных операций. В каких-то случаях это справедливо, но не во всех.
Другое дело — это лишь мнение, — что может быть весьма утомительным писать кучу вложенных колбэков, и кто-то считает, что это сильно ухудшает читабельность кода. Нередко в Node-коде можно встретить четыре, пять и даже больше уровней вложенности.
И мы снова вернулись к компромиссам. Модель Node хорошо работает в том случае, если основная причина плохой производительности связана с вводом/выводом. Но её ахиллесова пята в том, что вы можете использовать функцию, которая обрабатывает HTTP-запрос, вставить код, зависящий от процессора, и это приведёт к тормозам во всех сетевых подключениях.
Естественное неблокирование: Go
Прежде чем перейти к обсуждению Go, должен сообщить, что я его поклонник. Я использовал этот язык во многих проектах, открыто пропагандирую предоставляемые им выигрыши в производительности, причём отмечаю их роль в своей работе.
И всё-таки давайте посмотрим, как Go работает с операциями ввода/вывода. Одна из ключевых особенностей языка — в нём есть собственный диспетчер. Вместо привязки каждого потока выполнения к одному потоку на уровне ОС Go использует концепцию горутин. В зависимости от задачи, выполняемой горутиной, среда выполнения языка может приписывать горутину к потоку ОС и заставлять исполнять её — или переводить её в режим ожидания и не ассоциировать с потоком ОС. Каждый запрос, поступающий от HTTP-сервера Go, обрабатывается в отдельной горутине.
Схема работы диспетчера:
Под капотом это реализовано с помощью разных ухищрений в runtime-среде Go, которая реализует вызовы ввода/вывода, делая запросы на запись/чтение/подключение и т. д., затем переводя текущую горутину в спящий режим с информацией, позволяющей снова активировать горутину, когда можно будет предпринять следующее действие.
Фактически runtime-среда Go делает нечто, не слишком отличающееся от того, что делает Node. За исключением того, что механизм колбэков встроен в реализацию вызовов ввода/вывода и автоматически взаимодействует с диспетчером. Также Go не страдает от проблем, возникающих из-за того, что вам приходится помещать весь обрабатывающий код в один поток выполнения: Go автоматически распределяет горутины по такому количеству потоков ОС, какое он считает подходящим в соответствии с логикой диспетчера. Код выглядит так:
Как видите, базовая структура кода того, что мы делаем, напоминает структуру более простых подходов, но под капотом использует неблокирующий ввод/вывод.
В большинстве случаев нам удаётся «взять лучшее от двух миров». Для всех важных вещей используется неблокирующий ввод/вывод; при этом код выглядит как блокирующий, но всё же получается более лёгким в понимании и сопровождении. Остальное решается при взаимодействии между диспетчерами Go и ОС. Это неполное описание магии, и если вы создаёте большую систему, то рекомендуется уделить время более глубокому изучению работы с вводом/выводом. В то же время окружение, полученное вами из коробки, хорошо работает и масштабируется.
У Go есть свои недостатки, но в целом они не относятся к работе с вводом/выводом.
Ложь, наглая ложь и бенчмарки
Трудно привести конкретные тайминги переключения контекста при использовании вышеописанных моделей. К тому же эта информация вряд ли была бы вам полезна. Вместо этого предлагаю прогнать простенькие бенчмарки и сравнить общую производительность HTTP-сервера в разных серверных окружениях. Но имейте в виду, что на результирующую производительность «HTTP-запрос/ответ» влияет много факторов, и приведённые здесь данные позволят получить лишь общее представление.
Более подробную информацию о тестируемых средах можно почитать здесь.
Сначала рассмотрим примеры с небольшим распараллеливанием (low concurrency). Прогоним 2000 итераций с 300 одновременными запросами и применением только одного хеширования к каждому запросу (N = 1):
Сколько миллисекунд потребовалось на выполнение всех одновременных запросов. Чем меньше, тем лучше
На основании одного графика трудно делать какие-то выводы. Но создаётся впечатление, что при таком объёме подключений и вычислений мы видим результаты, которые больше похожи на общую продолжительность выполнения самих языков, а не длительность обработки операций ввода/вывода. Обратите внимание, что медленнее всего работают так называемые скриптовые языки (слабая типизация, динамическая интерпретация).
Увеличим N до 1000, оставив 300 одновременных запросов — нагрузка та же, но нужно выполнить в сто раз больше операций хеширования (значительно повышается нагрузка на процессор):
Сколько миллисекунд потребовалось на выполнение всех одновременных запросов. Чем меньше, тем лучше
Неожиданно значительно упала производительность Node, потому что операции, активно использующие процессор в каждом запросе, блокируют друг друга. Любопытно, что PHP стал гораздо лучше по производительности (по сравнению с другими) и обогнал Java. Нужно отметить, что реализация SHA-256 в PHP написана на Си, и в этом цикле путь выполнения (execution path) занимает гораздо больше времени, потому что теперь нам нужны 1000 итераций хеширования.
Теперь сделаем 5000 одновременных запросов (N = 1) или как можно ближе к этому количеству. К сожалению, в большинстве сред частота отказов была значительной. На графике отражено общее количество запросов в секунду.
Общее количество запросов в секунду. Чем выше, тем лучше
Картина совсем другая. Это предположение, но похоже на то, что в связке PHP + Apache при большом количестве подключений доминирующим фактором становятся удельные накладные расходы, связанные с созданием новых процессов и выделением им памяти, что негативно влияет на производительность PHP. Go стал победителем, за ним идут Java, потом Node, и последний — PHP.
Несмотря на многочисленность факторов, влияющих на общую пропускную способность, и их варьирование в зависимости от приложения, чем больше вы будете знать о внутренностях протекающих процессов и сопутствующих компромиссах, тем лучше.
В итоге
Подводя итог вышесказанному, очевидно, что по мере развития языков развиваются и решения по работе с масштабными приложениями, обрабатывающими большое количество операций ввода/вывода.
Честно говоря, несмотря на данные в этой статье описания, в PHP и Java есть реализации неблокирующих вводов/выводов, доступных для использования в веб-приложениях. Но они не так распространены, как вышеописанные подходы, и потому нужно принимать в расчёт сопутствующие этим подходам накладные операционные расходы. Не говоря уже о том, что ваш код должен быть структурирован так, чтобы работать в подобных средах. Ваше «нормальное» PHP или Java веб-приложение без серьёзных модификаций вряд ли будет работать в такой среде.
Для сравнения, если выбрать несколько важных факторов, влияющих на производительность и простоту использования, то получается такая таблица:
Язык | Потоки vs. процессы | Неблокирующие I/O | Простота использования |
---|---|---|---|
PHP | Процессы | Нет | |
Java | Потоки | Доступно | Нужны колбэки |
Node.js | Потоки | Да | Нужны колбэки |
Go | Потоки (горутины) | Да | Колбэки не нужны |
С точки зрения потребления памяти потоки выполнения должны быть гораздо эффективнее процессов. Если также учесть факторы, относящиеся к неблокирующим операциям ввода/вывода, то по мере движения вниз по таблице общая ситуация с вводом/выводом улучшается. Так что если бы я выбирал победителя, то предпочёл бы Go.
Но в любом случае выбор среды для создания проекта тесно связан с тем, насколько хорошо ваша команда знакома с той или иной средой, а значит, и с общей потенциальной продуктивностью. Поэтому не для каждой команды будет целесообразно с головой погрузиться в разработку веб-приложений и сервисов на Node или Go. Одна из частых причин неиспользования тех или иных языков и/или сред — необходимость поиска разработчиков, знакомых с данным инструментом. Тем не менее за последние 15 лет многое изменилось.
Надеюсь, всё вышесказанное поможет вам лучше понять, что происходит под капотом нескольких языков и сред, и подскажет, как лучше решать проблемы масштабирования ваших приложений. Удачного ввода и вывода!