njs в NGINX: где JavaScript действительно работает, а где — нет

njs добавляет JavaScript в NGINX, но не превращает его в Node.js. Код выполняется только в строго заданных точках обработки запроса и должен укладываться в миллисекунды. Два движка — устаревший встроенный njs и современный QuickJS — различаются по скорости и возможностям.

njs в NGINX: где JavaScript действительно работает, а где — нет

njs: где JavaScript в NGINX действительно работает, а где — нет

njs не превращает NGINX в Node.js. Он позволяет написать несколько строк JavaScript, которые выполнятся в строго определённый момент: когда NGINX решает, пустить ли запрос дальше, какой ответ вернуть или какие заголовки добавить. Контекст живёт только во время обработки запроса или таймера, а затем исчезает. Никакого постоянного процесса, никакого event loop — только микро-вспышки кода, которые должны уложиться в миллисекунды. Если скрипт зависнет хоть на секунду, замрёт весь воркер. Это не гибкость, а жёсткое ограничение.


Два движка: QuickJS против устаревшего встроенного njs

njs поддерживает два JavaScript-движка. Встроенный njs работает по ES5.1 и объявлен deprecated с версии 1.0.0 — он лишён современных фич, но быстрее создаёт контексты. QuickJS — это современный движок с поддержкой ES2023, модулей, асинхронных генераторов и BigInt. Переключение между ними делается одной строчкой в конфигурации NGINX:

js_engine qjs;

QuickJS медленнее создаёт контексты — это критично для систем с короткими запросами и высокой нагрузкой. В документации нет бенчмарков, зато есть предупреждение: если гонять скрипты с QuickJS на сотнях запросов в секунду, задержки вырастут.

Ещё один нюанс — переиспользование контекстов. При js_context_reuse on; QuickJS может брать готовые контексты из пула, но глобальные переменные при этом остаются между запросами. Если забыть их сбросить, один запрос испортит следующий. Это не баг, а следствие устройства движка: контекст живёт дольше одного запроса, но не гарантирует чистоту.


Пять операционных деталей, которые решают исход

  1. Объект r — единственный интерфейс к NGINX. Он даёт доступ к переменным (r.variables), заголовкам (r.headersIn, r.headersOut), а также позволяет запускать подзапросы (r.subrequest()) и выполнять внутренние редиректы (r.internalRedirect()).

  2. Точки интеграции жёстко заданы. Например, js_header_filter и js_body_filter не поддерживают асинхронность — они должны вернуть результат сразу. А js_access обязан завершиться мгновенно, иначе блокирует обработку запроса.

  3. Shared-словарь (js_shared_dict_zone) — единственный способ делиться состоянием между воркерами NGINX. Без него данные между запросами не сохраняются.

  4. Нет доступа к файловой системе за пределами встроенных модулей. Хотите прочитать конфиг? Придётся делать это через переменные NGINX. Работа с большими файлами невозможна без предварительной загрузки в память.

  5. Нет поддержки npm-пакетов и потоков. Долгий синхронный код блокирует весь воркер, даже если обёрнут в async/await.


Три примера, которые можно копировать и адаптировать

1. Динамическая маршрутизация для A/B-тестирования или canary-релизов

function choose_upstream(r) {
  const mark = (r.headersIn['X-Exp'] || getCookie(r, 'exp') || '').toLowerCase();
  if (mark === 'v2') return 'app_v2';
  const bucket = Math.abs(hashCode(r.variables.request_id)) % 10;
  return bucket === 0 ? 'app_v2' : 'app_v1';
}

Ограничение: если r.variables.request_id нестабилен, распределение по бакетам сломается. Для продакшн-кода стоит использовать более надёжный идентификатор, например, хэш от r.remoteAddress + r.uri.


2. Канонизация URL: убираем www, переводим на HTTPS, чистим параметры

function canonical_redirect(r) {
  const host = (r.headersIn['Host'] || '').toLowerCase();
  if (host.startsWith('www.')) {
    r.return(301, `https://${host.slice(4)}${r.uri}${r.args ? '?' + r.args : ''}`);
    return;
  }
  if (r.variables.scheme !== 'https') {
    r.return(301, `https://${host}${r.uri}${r.args ? '?' + r.args : ''}`);
    return;
  }
  r.internalRedirect('@upstream');
}

Edge case: если заголовок Host отсутствует или искажён, скрипт упадёт. В продакшн-коде стоит добавить проверку и fallback.


3. Безопасность на границе сети: добавляем заголовки CSP и HSTS

function add_security_headers(r) {
  r.headersOut['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains';
  r.headersOut['Content-Security-Policy'] = "default-src 'self'";
}

Ограничение: если r.headersOut уже содержит заголовки, они будут перезаписаны. Для сохранения существующих заголовков нужно сначала их прочитать и объединить.


Что njs не потянет: три причины не строить на нём бизнес-логику

  1. Однопоточность. Долгий синхронный код блокирует весь воркер. Даже с async/await это не спасёт, если операция реально долгая. njs не Node.js: здесь нет отдельного event loop.

  2. Нет нормального доступа к данным. Нет работы с файловой системой, нет npm-пакетов, нет потоков. Хотите обработать большой JSON-файл? Придётся тащить его в память через переменные NGINX — что не всегда возможно.

  3. Нет изоляции между воркерами. Shared-словарь — единственный способ делиться состоянием, но он не атомарен и не транзакционен. Race condition между воркерами — типичная проблема.


Практическое правило: используйте njs для микро-обработки, а не для приложений

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

QuickJS удобнее для современного кода, но медленнее в создании контекстов. Встроенный njs быстрее, но deprecated. Выбор движка — это trade-off между удобством и производительностью.

Если решитесь использовать njs, начните с малого: возьмите простой скрипт для редиректов или добавления заголовков. Убедитесь, что он не блокирует воркер. И только потом расширяйте логику. Иначе рискуете обнаружить, что ваш "микро-скрипт" стал узким местом.

Read more

Пять тысяч человек видят ИИ, которого не существует для остальных шести миллиардов

Андрей Карпати оценивает, что лишь пять тысяч человек имеют доступ к вершине возможностей языковых моделей, тогда как шесть миллиардов почти не сталкивались с ними. Разрыв в понимании ИИ растёт, и дело не в доступе, а в сложности задач, которые мы перед ним ставим.

Строительный сектор сжимается: главный риск — в «серой зоне»

Строительный сектор сжимается: главный риск — в «серой зоне»

Число строительных компаний в России сократилось на 4,3% за год. Но главная угроза — не в ликвидированных фирмах, а в десятках тысяч компаний с заблокированными счетами и высоким риском банкротства, которые могут обрушить подрядчиков.

Drex: модель, которая решает, а не говорит

Drex: модель, которая решает, а не говорит

В индустрии, где главным продуктом стал текст, появляется модель, которая отказывается от текста как такового. Drex принимает на вход факты и за один проход выдаёт распределение вероятностей по всем возможным вариантам. Никаких рассуждений вслух, никаких промежуточных выкладок — только числа.

Воронка Карпати: почему экспонента ИИ проходит мимо 99,8% человечества

Воронка Карпати: почему экспонента ИИ проходит мимо 99,8% человечества

Шесть миллиардов человек никогда не взаимодействовали с языковыми моделями. Еще один-два миллиарда хотя бы раз написали запрос в бесплатный ChatGPT. Около двадцати миллионов используют агентов в работе. И примерно пять тысяч человек имеют доступ к внутренним системам OpenAI. Андрей Карпати объясняе…