njs — это JavaScript-интерпретатор в легковесном веб-сервере, с помощью которого можно создавать новые nginx-переменные и обработчики стадий запроса. Чем njs хорош? Чего не умеет? И зачем вообще его сделали? На эти и другие вопросы ответит Дмитрий Волынцев (xeioex), разработчик nginx и основной разработчик интерпретатора njs.



— Дмитрий, зачем понадобился скриптинг в конфигах nginx?


— Первая причина — директива if. Люди, которые первый раз ее увидели, думают, что можно использовать ее императивно. На самом деле это не так — конфигурация nginx является декларативной. В примере ниже можно подумать, что в ответе будут два header: X-First и X-Second. Но в ответ попадет только второй header, потому что так устроен nginx: если мы напишем две if-директивы, то выберется самая последняя.

location /only-one-if {
    set $true 1;

    if ($true) {
        add_header X-First 1;
    }

    if ($true) {
        add_header X-Second 2;
    }

Вторая причина — то, к чему nginx пришел сейчас. Раньше он использовался для кэширования статики и запросов, а также балансировки нагрузки — классический набор прокси. Распространение микросервисов размыло сферу применения nginx. Если раньше настройка конфигураций заканчивалась на паре location на нескольких бэкендах на каких-то языках, то с микросервисной архитектурой у нас появляется больше движущихся частей. Бэкенд превратился в кучу маленьких компонентов. Логику авторизации, например, необходимо дублировать на каждом микросервисе или выносить ее, скажем, на фронтенд. Чтобы реализовать продвинутую авторизацию, встроенных механизмов решения в nginx хватает не всегда.

В-третьих, в nginx многие директивы принимают динамически вычисляемые выражения, например:

proxy cache bypass   $cookie_nocache $arg_nocache;

Вы можете сделать конкатенацию переменных друг с другом или с литеральными строками. Но этого недостаточно, и хотелось бы иметь более мощные инструменты, например, для вычисления хэша, для работы с числовыми данными, для приведения к верхнему и нижнему регистру.

Чтобы расширить все узкие места в nginx, нужно либо разработать свой синтаксис, либо использовать что-то готовое. Мы пришли к выводу, что лучше всего взять уже имеющийся скриптовой язык программирования. Таким образом, разработчикам не нужно учить новый язык, что также сэкономит время и понизит порог входа. Мы выбрали JavaScript.

— А почему именно JavaScript?


— Мы выбрали JavaScript по нескольким причинам:

  • Современный диалект, что хорошо для разработчиков, переходящих с других языков.
  • С-подобный стиль. Это важно, потому что в конфиге nginx используются фигурные скобки, и в будущем мы хотим добавить возможность написания кода на JS прямо внутри конфига. Фигурные скобки нам в этом помогут. В Lua, например, роль фигурных скобок выполняют begin и end — это неудобно.
  • Модель JavaScript хорошо ложится на архитектуру nginx.

— Значит, Lua тоже рассматривали? Неужели из-за begin и end?


— Уже существует готовый сторонний проект OpenResty. Если не вдаваться в подробности, то это, по сути, nginx + Lua, но он имеет архитектуру, которая идет вразрез с nginx. Мы хотели избежать пересечений с этой экосистемой. Кроме того, есть еще несколько причин:

  • Lua имеет pascal-подобный синтаксис.
  • Массивы индексируются с 1.
  • Lua — этой все-таки нишевый язык программирования.

— Как njs работает в сравнении с конкурентами?


— Мы оценили njs в сравнении с известными движками — V8 и SpiderMonkey. Они неэффективны для задач внутри nginx, потому что заточены под браузеры и очень тяжеловесны, а nginx необходима высокая скорость работы. Кроме того, оба этих движка быстро эволюционируют, их API нестабилен. Наконец, njs может более эффективно встраиваться в nginx:


Количество контекстов, создаваемых в секунду

— Какие стандарты поддерживает njs?


— На данный момент реализованы практически все основные элементы спецификации ECMAScript 5.1 с некоторым вкраплением элементов спецификаций 6 и 7. То есть стандартные объекты типа Object, Array, String, Number, Date, RegExp, JSON. Полноценно поддерживаются замыкания, анонимные функции, работа с исключениями.

Мы не ставим своей первоочередной целью полное соответствие спецификации языка. Так что на данный момент отсутствует поддержка eval(), и пока что добавлять ее мы не планируем. Зато планируем добавить поддержку ключевых слов const и let, а также стрелочных функций.


Что умеет и чего не умеет njs на данный момент

Важно упомянуть еще кое-что: отсутствие сборки мусора. Большинство современных языков самостоятельно следят за временем жизни объектов. Если объект больше не используется, то он автоматически удаляется. Без этого механизма нельзя обойтись, но обычно для него приходится чем-то жертвовать — работа программы замедляется или вообще приостанавливается. В njs память не освобождается до тех пор, пока объект запроса не будет освобожден.

У этого подхода есть свои плюсы и минусы. Основной недостаток в том, что он не позволяет эффективно работать с длительными запросами. Поэтому в будущем мы планируем добавить сборку мусора как опцию, чтобы включать ее по мере необходимости.

— Чем njs не является?


— Перед тем как ответить на этот вопрос, хотел бы еще раз повторить, что основная задача njs — это расширение возможностей по гибкому конфигурированию nginx и решению задач на стороне прокси.

Теперь непосредственно сам вопрос. Что стоит заранее учитывать?
  • njs — это не замена Node.js.
  • Связка nginx + njs — это не application server.
  • njs не реализует полноценно стандарты ECMAScript, поскольку здесь нет поддержки eval().



Если для вас эта тема крайне актуальна и вы жаждите больше деталей, рекомендуем посмотреть видеозаписью доклада Дмитрия Волынцева на HighLoad++ Siberia 2018, где он раскрыл ее со всех сторон.


Также мы приглашаем всех профи к подаче своих докладов на ноябрьскую конференцию HighLoad++ 2018, которая пройдет в Сколково 8 и 9 ноября. Если у вас есть уникальный и интересный опыт и вы готовы им поделиться — до 1 сентября регистрируйтесь и заполняйте форму.

Если боитесь выступать публично — у нас есть так называемая школа докладчиков, где мы бесплатно помогаем прокачивать эти скилы.

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


  1. Ghost_nsk
    20.08.2018 12:53
    +2

    еще несколько причин:
    — Lua имеет pascal-подобный синтаксис.
    — Массивы индексируются с 1.
    — Lua — этой все-таки нишевый язык программирования.

    веские причины, что бы начать все с нуля


    1. SirEdvin
      20.08.2018 13:40

      Учитывая, что у nginx в последнее время новомодные бесплатные прокси откусывают все больше рынка, то маркетинг тоже важен.


    1. farcaller
      20.08.2018 16:48

      — Массивы индексируются с 1.
      веские причины, что бы начать все с нуля

      как-то да :-)


    1. Tangeman
      20.08.2018 23:39
      +1

      JS это как-бы не совсем с нуля, если уж говорить честно и откровенно, а Lua — таки нишевый (на TIOBE он на 22 позиции ниже JS и уступает даже Fortran). Его единственный плюс — это JIT компиляция (которая нерелевантна для nginx), и на этом всё.

      Мне почему-то думается, что любому кто имел дело только с C/C++/C#/Java/Perl/PHP (а это > 45% из наиболее популярных) JS будет намного понятней и проще чем Lua.

      Единственный существенный минус JS — отсутствие эффективной работы с целыми числами (32/64 bit), но это тоже не очень релевантно в случае nginx. Хотя, если это + эффективная работа с целочисленными Array ((u)int 8/16/32/64) будет в njs добавлены, это будет просто супер для прокси-скриптинга.


      1. Ghost_nsk
        21.08.2018 06:05

        с нуля не в плане языка (хотя судя по тексту тут своя реализация, которая наверняка будет иметь свои нюансы), а в плане готовых библиотек. Для Lua уже есть куча либ OpenResty именно под Nginx, для JS тоже есть тонны кода, но едва ли он будет полезен в рамках Nginx. Lua сильно проще ECMAScript в изучении, да со своими табличными тараканами, но сам синтаксис очень прост, едва ли вызывает трудности у любого программиста. Да он нишевый, но его ниша — быть встроенным в любое ПО.


        1. Tangeman
          21.08.2018 13:59
          +2

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

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


          1. apapacy
            21.08.2018 14:05

            Язык lua практически близнец javascript и программирующие на js изучит его за несколько часов. Только вместо закрывающей фигурной скобки end. А begin в lua к слову сказать нет.


  1. zuborg
    20.08.2018 13:03
    +2

    Короче, это как nginx_http_perl_module, только JS.


    1. VBart
      20.08.2018 16:45
      +1

      Perl невозможно было полноценно встроить, поэтому он всегда был в состоянии экспериментального и не развивался. С JS-модулем совсем другая история, это уже полноценный скриптиг с растущими от релиза к релизу возможностями.


      1. zuborg
        20.08.2018 17:25

        Думаю, полноценность js-модуля будет ровно такой же как и у перла — парсить строки, выставлять хидеры, регекспы, md5 какой-нибудь посчитать…
        Работа с файлами, базами, сетью на уровне конфига не предполагается.

        Кстати, у этого js-модуля есть общая память хотя бы для процесса (а ещё лучше для всех воркеров)? У перла глобальные переменные общие для процесса, а возможно и заработает какой-то модуль shared memory для разных процессов, не пробовал.


        1. xeioex
          20.08.2018 18:30

          >Кстати, у этого js-модуля есть общая память хотя бы для процесса (а ещё лучше для всех воркеров)?

          На данный момент нет. Доступ к общей памяти процесса не предполагается. Тогда как мы думаем над тем как и в каком виде добавить функционал аналогичный ngxshareddict (глобальный словарь в разделяемой памяти доступный всем воркерам).

          >Работа с файлами, базами, сетью на уровне конфига не предполагается.
          Тут вы ошибаетесь. Работа с файлами уже есть (правда на данный момент блокирующая).
          Чего в perl модуле нет так это асинхронного выполения кода, тогда как njs это уже есть в виде таймеров и асинхронных подзапросов (пример).

          >Работа с файлами, базами, сетью на уровне конфига не предполагается.
          Причем тут конфиг? Модель выполнения несколько другая. Вам доступно несколько хуков для различных фаз выполнения запроса где вы можете написать произвольный код который будет выполнятся для каждого запроса. Вот практический пример использования для openId Connect.


          1. zuborg
            20.08.2018 19:13

            Причем тут конфиг?
            Я имел ввиду, собственно, js_set — управлять переменными конфига, используя блокирующие операции на файлах, базе или сети, нельзя.

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


            1. xeioex
              20.08.2018 19:35
              +1

              >Я имел ввиду, собственно, js_set — управлять переменными конфига, используя блокирующие операции на файлах, базе или сети, нельзя.

              тут проблема скорее не в njs, а в том что nginx не умеет асинхронно ждать пока переменная вычислится. Модули тупо зовут ngx_http_complex_value() для вычисление значения переменной (пример), и ожидают что она либо вычислится либо нет. Если сюда добавить некий NGX_AGAIN это все сломает в куче мест.


        1. VBart
          20.08.2018 20:07
          +2

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

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

          Но мой комментарий про неполноценность встраиваемости перла относился скорее к тому, что Perl не умеет нормально обрабатывать ошибки и при любом удобном случае просто завершает весь процесс, что неприемлемо в рамках асинхронного сервера, обрабатывающего тысячи запросов.


          1. zuborg
            20.08.2018 20:56

            Работа то возможна, но не думаю, что дизайн nginx-а предусматривает асинхронный процессинг конфига, блокировать на чтении файла весь воркер с оставшимися в очереди необработанными запросами никому не надо.

            Другое дело обработчик запроса (а не установка переменной конфига), там проще сделать асинхронную обработку блокирующих операций, но все равно внедрять в nginx тяжелую логику не есть здравая идея — для этого есть лучшие инструменты.
            А внедрить простую логику в обработчик запроса nginx-а иногда бывает очень полезно — убираются накладные расходы на проксирование, уменьшается задержка обработки запроса… Но для этого обычно и встроенного перла хватает )

            Я вовсе не противник njs, но пока что он не предоставляет принципиальных преимуществ перед perl-модулем.


            1. VBart
              20.08.2018 21:08
              +2

              В конфигурации nginx все переменные работают и устанавливаются в рамках обработки запроса, а не при чтении конфигурации. Переменных на уровне конфигурации нет, она читается единожды на старте. И в этом смысле вообще никакой «асинхронный процессинг конфига» не требуется.

              Я вовсе не противник njs, но пока что он не предоставляет принципиальных преимуществ перед perl-модулем.
              Принципиальное приемущество в том, что njs — это решение пригодное для использования в продакшене под высокой нагрузкой, а Perl — нет. Когда интерпретатор Perl-а не может выделить себе память, он убивает весь nginx, поэтому использовать его можно только на свой страх и риск, в надежде, что памяти всегда хватит, особенно под высокой нагрузкой.


            1. playnet
              21.08.2018 18:03

              У перла много недостатков, банальный парсинг json в POST превращается в 3 экрана кода. На openresty это 3 строки. Да и вообще nginx по сути не умеет обрабатывать post, а также нет контроля вида «если post менее 30кб то обрабатывать»…

              Вообще, новый вариант nginx тоже интересен, со встроенными интерпретаторами который.


              1. Tangeman
                22.08.2018 01:40

                У перла много недостатков, банальный парсинг json в POST превращается в 3 экрана кода.

                Откуда там 3 экрана? JSON это две строки:
                use JSON::XS;
                $perl_hash_or_arrayref  = decode_json $utf8_encoded_json_text;


                Если речь про ngx_http_perl, то обработка собственно POST займёт ещё несколько строк, на этом всё. Другой вопрос, насколько это стабильно, с учётом того что Perl в nginx до сих пор «experimental»…


          1. Tangeman
            22.08.2018 01:28

            Perl не умеет нормально обрабатывать ошибки и при любом удобном случае просто завершает весь процесс

            Это неправда — Perl отлично умеет обрабатывать ошибки, просто некоторые привыкли писать как модули так и приложения где предпочитают завершать процесс в случае ошибок (djb style). Сделать приложение/модуль который будет вести себя корректно — вообще не проблема, достаточно эти самые ошибки обрабатывать.

            Когда интерпретатор Perl-а не может выделить себе память, он убивает весь nginx...

            Да, это одна из немногих ситуаций которую невозможно предотвратить простыми средствами в Perl (сюда также входят SIGSEGV/SIGFPE/SIGILL).

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

            Для embedded систем ситуация может быть иной, но в типичный сценариях frontend proxy или static file serving суицид — это единственно верное решение. Можно попытаться освободить буфера, уменьшить количество workers, etc — но это вряд-ли спасёт ситуацию (если нехватка глобальная, а не обусловленная лимитом на процесс). Опять-таки, при недостаче памяти можен начаться проблема с открытием файлов/сокетов, буферов для сокетов внутри ядра, etc — дело однозначно швах.


            1. xeioex
              22.08.2018 09:51

              Но если уж дошло до нехватки памяти, то вполне логично убить весь процесс — ибо ситуация уже нестабильна, и скорее всего любое другое действие тоже приведет к проблемам (или будет периодически приводить пока система балансирует на границе голодания)


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

              Сам nginx вполне себе справляется с ситуацией нехватки памяти и корректно её обрабатывает.


            1. VBart
              22.08.2018 15:45
              +1

              Это неправда — Perl отлично умеет обрабатывать ошибки, просто некоторые привыкли писать как модули так и приложения где предпочитают завершать процесс в случае ошибок (djb style).
              Я имел в виду как раз ошибки выделения памяти.

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

              Более того, ошибка выделения памяти может быть связана не с нехваткой памяти, а с ошибкой в коде или недостаточно качественной фильтрацией данных от пользователя. Как результат какой-то HTTP-запрос может приводить к попытке выделить гигантский объем памяти и это не должно ронять целиком сервер, а завершать ошибкой только один конкретный запрос.


  1. ReklatsMasters
    20.08.2018 14:03

    А njs поддерживает jit?


    1. xeioex
      20.08.2018 14:55
      +2

      Что вы именно под jit понимаете?
      Если компиляцию в байт-код по типу CPython то да.
      Если компиляцию в нативный код процессора то нет. JIT малоактуален, если у вас мало кода, или ваш код высокоуровневый (дергаете методы окружения, что как раз типично для кода на стороне прокси) потому что никакого преимущества вы от него не получите. Если же у вас, например, много математических расчетов то JIT будет кстати, но для этого есть другие инструменты.


  1. fukkit
    20.08.2018 23:02

    Куда катится этот чертов мир?
    Радует, что пока Линус жив, жаваскрипта не будет хотя бы в ядре.


    1. khim
      21.08.2018 01:10

      У них своя игрушка. Причём как раз с JIT'ом.


  1. apapacy
    21.08.2018 00:32

    Хотелось бы все же более подробно услышать про кейсы когда лучше openresty а когда njs.
    При этом исходя из того что openresty все же уже используется широко и в некоторых высоконагружпнных проектах. Где будет njs лучше lua и в чем.