在 Nginx Stream 层玩转 JavaScript——全面解读ngx_stream_js_module
摘要 NGINX Stream模块新增JavaScript支持,借助njs/QuickJS嵌入式解释器,可在L4层实现协议解析、ACL鉴权、报文改写等能力。关键特性包括:二进制流处理、统一L4/L7开发栈、轻量级高性能运行。典型应用场景涵盖自定义协议网关、动态ACL、灰度发布和长连接健康检查。通过js_preread/js_access/js_filter等生命周期钩子,结合共享字典和Fetch
·
一、为什么要在 L4 跑 JavaScript?
- 协议灵活:Stream 模块工作在传输层,可代理任何二进制流。以前要拦截报文只能写 C 模块或 Lua,如今用 JS 即可快速迭代。
- 统一栈:HTTP 层已有
ngx_http_js_module;新增的 stream 版本让同一套 njs 代码横跨 L4/L7,方便复用工具库与研发体系。 - 原生性能:njs/QuickJS 嵌入式解释器体积 < 1 MB,启动快、内存足够小;对 IO 密集的网关类业务非常友好。
二、快速上手:零基础示例
# nginx.conf(精简片段)
stream {
js_import stream.js; # ① 加载脚本
js_set $bar stream.bar; # 动态变量
js_set $req_line stream.req_line; # 请求行缓存
server {
listen 12345; # ② Echo 服务
js_preread stream.preread; # 读取首行
return $req_line; # 回射
}
server {
listen 12346; # ③ 透明代理
js_access stream.access; # ACL
proxy_pass 127.0.0.1:8000;
js_filter stream.header_inject; # 报文注入
}
}
// stream.js(核心逻辑)
let line = '';
export function bar(s) {
const v = s.variables;
s.log('hello from bar()');
return `bar-${v.remote_port}; pid=${v.pid}`;
}
export function preread(s) {
s.on('upload', (data) => {
const n = data.indexOf('\n');
if (n !== -1) {
line = data.substr(0, n);
s.done(); // 切换到下一阶段
}
});
}
export const req_line = () => line;
export function header_inject(s) {
const hdr = 'Foo: foo';
let buf = '';
s.on('upload', (data, flg) => {
buf += data;
const n = buf.search('\n');
if (n !== -1) {
const rest = buf.substr(n + 1);
buf = buf.substr(0, n + 1);
s.send(buf + hdr + '\r\n' + rest, flg);
s.off('upload'); // 注入一次即停
}
});
}
export function access(s) {
if (/^192\./.test(s.remoteAddress)) {
s.deny();
return;
}
s.allow();
}
运行效果
| 流程 | 钩子 | 关键行为 |
|---|---|---|
| 客户端连 12345 | js_preread |
捕获首行并回射 |
| 客户端连 12346 | js_access |
私网 192.* 拒绝 |
| - | js_filter |
将 Foo: foo 注入转发流量 |
三、核心指令拆解
| 类别 | 指令 | 作用与要点 | |
|---|---|---|---|
| 生命周期钩子 | js_preread / js_access / js_filter |
分别对应握手前、鉴权、内容阶段;s.on() 可多次读写,s.done()/s.allow()/s.deny() 控制流转 |
|
| 变量 | js_set $var func [nocache]js_var $writable |
首次引用触发函数;nocache 每次执行 |
|
| 脚本加载 | js_import / js_path |
支持多文件、命名空间;js_include 已废弃 |
|
| Fetch 支持 | js_fetch_* 系列(缓冲、TLS、超时…) |
在脚本内 await ngx.fetch() 调用外部 HTTP/HTTPS |
|
| 定时任务 | js_periodic func interval=60s worker_affinity=all |
Stream 层 “cron” 功能,可拉黑名单、做健康检查 | |
| 共享字典 | js_shared_dict_zone zone=foo:1M timeout=60s |
跨进程 KV 存储,支持 get/set/incr/delete |
|
| 引擎 & 内存 | `js_engine njs | qjs<br>js_context_reuse 512` |
QuickJS 兼容 ES2020 更多特性;合理复用上下文降低 GC |
四、典型落地场景
-
自定义协议网关
- 利用
js_preread解析自研二进制头,提取租户 ID 写入$tenant变量;后续upstream选路。
- 利用
-
实时 ACL / 令牌鉴权
js_access中await ngx.fetch('https://auth.local/verify', {headers}),校验失败直接s.deny()。
-
灰度发布 / A/B Test
js_var $bucket hash.bucket; map $bucket转发至新旧集群,灰度比例写死或由远程配置中心下发。
-
报文动态改写
- MQTT CONNECT 报文中插入追踪 ID;Redis 协议重写数据库序号,解决多租户隔离。
-
长连接健康探测
js_periodic每分钟拉取后端状态,结果写入共享字典;js_access读字典实现熔断。
五、进阶实践
1. 使用 QuickJS 加速复杂脚本
js_engine qjs; # ES2020、async/await 原生支持
js_context_reuse 256; # 复用上下文,减少 Per-Session 开销
2. 预加载配置对象
js_preload_object region_map.json; # 变为全局常量 region_map
在脚本中:
if (region_map[s.remoteAddress]) { ... }
3. 跨进程共享 KV
js_shared_dict_zone zone=limiter:2M timeout=30s evict;
# limiter.js
export function incr(s) {
const cnt = ngx.shared.limiter.incr(s.remoteAddress, 1, 0, 10);
if (cnt > 1000) s.deny();
}
六、性能调优与踩坑
| 现象 | 原因 | 解决方案 |
|---|---|---|
| QPS 高时 CPU 飙升 | 上下文频繁创建 | 提高 js_context_reuse 或改用 njs |
| Fetch 卡顿 | 默认缓冲 16 kB,TLS 验证全面 | 调整 js_fetch_buffer_size 32k; js_fetch_timeout 5s; |
| 变量值错乱 | js_set 缓存机制误用 |
需要实时计算时加 nocache |
| Filter 注入丢包 | 忘记 s.off('upload') |
注入完成后及时解绑事件 |
| QuickJS 占用内存上涨 | 未复用/GC 周期长 | 结合 worker_processes + context_reuse,或改回 njs |
七、总结
ngx_stream_js_module 把 “用 JS 写 L4 代理” 这件事从概念变成了生产可用的能力:
- 开发效率:告别 C/Lua,热更新脚本即可上线。
- 生态一致:与 HTTP 层 njs/Lua 配合,统一监控 & 日志格式。
- 性能友好:解释器极小、上下文可复用,足以支撑 10⁵ QPS 以上场景。
倘若你正为私有协议网关、流量灰度、动态鉴权或报文改写犯愁,不妨给 njs/QuickJS 一个试水机会——在 Nginx Stream 层加一点点 JavaScript,就能让网关瞬间“活”起来。
更多推荐



所有评论(0)