如果你熟习 Node 或者其他后端措辞,下面代码要做的事情,就一览无余了:首先定义了一个名为 simple 的函数 ,接着定义了我们要展示的接口数据,然后设置 Nginx 相应内容类型为 UTF8 编码的 JSON,以及接口 HTTP Code 为 200,末了声明模块中的 simple 是可被公开调用的。
function simple(req) { var result = { code: 200, desc: "这是描述内容" }; req.headersOut["Content-Type"] = "application/json;charset=UTF-8"; req.return(200, JSON.stringify(result));}export default { simple };
将上面的内容保存为 app.js,并放置于一个名为 script 目录中,我们稍后利用。接着我们声明一份可以让 Nginx 调用 NJS 的配置文件:
load_module modules/ngx_http_js_module.so;user nginx;worker_processes auto;error_log /var/log/nginx/error.log warn;pid /var/run/nginx.pid;events { worker_connections 1024;}http { include /etc/nginx/mime.types; default_type application/octet-stream; js_import app from script/app.js; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; server { listen 80; server_name localhost; charset utf-8; gzip on; location / { js_content app.simple; } }}
将上述内容保存为 nginx.conf,我们同样稍后利用。

可以看到这份配置文件和以往的配置文件看起来差别不大,但是确实又有一些“不同”,将所有和 NJS 无关的内容去掉,就可以清晰的看到 NJS 是如何和 Nginx 联动的。
load_module modules/ngx_http_js_module.so;...http {... js_import app from script/app.js; server {... location / { js_content app.simple; } }}
首先是全局显式声明加载 ngx_http_js_module.so 模块,然后是将我们编写的脚本引入 Nginx HTTP 块浸染域内,末了则是调用脚本详细的方法供应做事。
为了方便的验证做事,我们还须要编写一个大略的 compose 编排文件:
version: '3'services: nginx-api-demo: image: nginx:1.19.8-alpine restart: always ports: - 8080:80 volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./script:/etc/nginx/script
上一篇文章提过,目前 NJS 已经是 Nginx 官方模块,并默认附带在官方 Docker 镜像中,以是我们这里直策应用最新的官方镜像 nginx:1.19.8-alpine 就可以了。
将上面的文件保存为 docker-compose.yml ,适当调度下上面文件的目录构造,并利用 docker-compose up 启动做事,访问 localhost:8080,可以看到我们得到了我们想要的结果,浏览器中涌现了接口内容。
浏览器中展示接口结果
和我们利用 Nginx 调用 CGI 程序不同,可以看到接口处理韶光只花费了 1ms ,虽然这和我们实现的代码繁芜度非常低有关系,但是常日网络开销导致我们得到的结果会远大于这个数值。从某个角度解释不须要“外部程序”打算参与时, Nginx 直接参与结果打算在性能方面是有潜力的。
考试测验编写获取远端数据的接口接着我们来编写一个能够获取远端数据的接口,和之前编写的办法类似,只须要将我们定义的接口返回数据更换为利用 subrequest 方法要求的数据接口结果即可。
function fetchRemote(req) { req.subrequest("https://www.mysql.com/common/chat/chat-translation-data.json").then((response) => { req.headersOut["Content-Type"] = "application/json;charset=UTF-8"; req.return(200, JSON.stringify(response)); })}export default { fetchRemote };
为了便于区分,我们这里将函数名改为更贴切的“fetchRemote”,接着将 nginx.conf 文件中的调用方法也进行更新:
...location / { js_content app.fetchRemote;}...
随后利用 docker-compose up 重新启动做事,再次访问 localhost:8080 来验证程序的结果是否符合预期。
然而页面返回了类似下面的结果:
{"status":404,"args":{},"httpVersion":"1.1","remoteAddress":"172.21.0.1","headersOut":{"Content-Type":"text/html","Content-Length":"555"},"method":"GET","uri":"https://www.mysql.com/common/chat/chat-translation-data.json","responseText":"<html>\r\n<head><title>404 Not Found</title></head>\r\n<body>\r\n<center><h1>404 Not Found</h1></center>\r\n<hr><center>nginx/1.19.8</center>\r\n</body>\r\n</html>\r\n<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n","headersIn":{"Host":"localhost:8080","Connection":"keep-alive","Cache-Control":"max-age=0","sec-ch-ua":"\"Google Chrome\";v=\"89\", \"Chromium\";v=\"89\", \";Not A Brand\";v=\"99\"","sec-ch-ua-mobile":"?0","DNT":"1","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9","Sec-Fetch-Site":"none","Sec-Fetch-Mode":"navigate","Sec-Fetch-User":"?1","Sec-Fetch-Dest":"document","Accept-Encoding":"gzip, deflate, br","Accept-Language":"zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7"}}
页面虽然返回了数据,但是显然不是我们想要的结果。
检讨 Nginx 日志,可以进一步理解这个缺点发生的缘故原由。
[error] 33#33: 1 open() "/etc/nginx/htmlhttps://www.mysql.com/common/chat/chat-translation-data.json" failed (2: No such file or directory), client: 172.21.0.1, server: localhost, request: "GET / HTTP/1.1", subrequest: "https://www.mysql.com/common/chat/chat-translation-data.json", host: "localhost:8080"...
不卖关子了,来聊聊“精确答案”。
精确的获取远程数据这里会发生缺点由于 NJS 的 subrequest 方法仅支持将要求利用异步办法发送给反向代理。
将要要求地址改为由 Nginx 反向代理,这里由于这个接口我们仅用作 NJS 调用,不须要供应开放访问,以是可以添加 internal 指令,来进行外部访问限定处理,避免 NJS 之外调用过程访问我们的远端接口:
location /proxy/api-mysql { internal; proxy_pass https://www.mysql.com/; proxy_set_header Host www.mysql.com;}
接着修正之前代码中的要求地址:
function fetchRemote(req) { req.subrequest("/proxy/api-mysql/common/chat/chat-translation-data.json").then((response) => { req.headersOut["Content-Type"] = "application/json;charset=UTF-8"; req.return(200, JSON.stringify(response)); })}export default { fetchRemote };
再次启动做事,可以看到我们已经能够获取远端数据,但是结果看起来有一些问题:
{"status":200,"args":{},"httpVersion":"1.1","remoteAddress":"172.27.0.1","headersOut":{"Content-Type":"application/json","Content-Length":"1863","X-Frame-Options":"SAMEORIGIN","Strict-Transport-Security":"max-age=15768000","Last-Modified":"Tue, 27 Nov 2018 20:34:52 GMT","Accept-Ranges":"bytes","Vary":"Accept-Encoding","Content-Encoding":"gzip","X-XSS-Protection":"1; mode=block","X-Content-Type-Options":"nosniff"},"method":"GET","uri":"/proxy/api-mysql/common/chat/chat-translation-data.json","responseText":"\u001f�\b\u0000\u0000\u0000\u0000\u0000\u0000\u0003�Z[o\u0013G\u0014~G�?��W(\u0002�J�R�\u0014���Bk�JT}\u0018{��$�]3��4��|!j�i�4��&$��P(��;qA��}�\u001b\u0016\u0007'1�_�\u0019�\u001d��c�(�M\"9^9����sf��\u0006\u0019+!\u0003���p\u0016}�\b����\u0017B\rD���?ᄆ�e�98�B�D�\u0010�o�q\u0003���c[lh@U\u00022�xk��\u0004
涌现这个问题的缘故原由是由于远端做事器给我们返回了 GZip 后的数据,以是这里我们有两个选择,见告做事器我们不支持 GZip,或者让 Nginx 对取回的数据进行解压缩。
由于存在即是我们见告远程做事器,我们不支持 GZip,远程做事器还是会发送压缩后的数据(常见于CDN),以是这里建议利用方案二,再次修正 Nginx 配置,让 Nginx 能够自动解压缩远端数据。
location /proxy/api-mysql { internal; gunzip on; proxy_pass https://www.mysql.com/; proxy_set_header Host www.mysql.com;}
但是当我们重新启动做事进行测试的时候会发生其余一个问题:
间隔成功很近的时的缺点
[error] 33#33: 4 pending events while closing request, client: 172.28.0.1, server: 0.0.0.0:80[error] 33#33: 8 too big subrequest response while sending to client, client: 172.28.0.1, server: localhost, request: "GET / HTTP/1.1", subrequest: "/proxy/api-mysql/common/chat/chat-translation-data.json", upstream: "https://137.254.60.6:443//common/chat/chat-translation-data.json", host: "localhost:8080"
检讨日志可以看到上面的缺点提示,这是由于 GZip 解压缩之后,数据量远大于 Nginx 默认处理临时数据的 Buffer 容量,以是我们要进一步对此进行调度:
subrequest_output_buffer_size 200k;location /proxy/api-mysql { internal; gunzip on; proxy_pass https://www.mysql.com/; proxy_set_header Host www.mysql.com;}
这里的subrequest_output_buffer_size 配置数值根据自己的场景需求进行调度即可。再次重启做事,会看到我们已经能够获取精确的远程接口数据内容了。
从远端获取的数据内容
编写具备聚合功能的程序由于我们要聚合多个接口,以是我们将 NJS 代码和 Nginx 配置同时进行一些调度。
我在这里就不演示很挫的顺序实行模式了,由于对付这些无高下文依赖的接口,利用异步并发获取的办法可以花费尽可能少的韶光来供应结果。当然,串行要求也是有场景的,我会在后面的文章中提到如何灵巧利用 NJS 掌握要求流程。
// https://github.com/nginx/njs/issues/352#issuecomment-721126632function resolveAll(promises) { return new Promise((resolve, reject) => { var n = promises.length; var rs = Array(n); var done = () => { if (--n === 0) { resolve(rs); } }; promises.forEach((p, i) => { p.then((x) => { rs[i] = x; }, reject).then(done); }); });}function aggregation(req) { var apis = ["/proxy/api-mysql/common/chat/chat-translation-data.json", "/proxy/api-redis/wp-content/themes/wpx/proxy/signup_proxy.php"]; resolveAll(apis.map((api) => req.subrequest(api))) .then((responses) => { var result = responses.reduce((prev, response) => { var uri = response.uri; var prop = uri.split("/proxy/api-")[1].split("/")[0]; try { var parsed = JSON.parse(response.responseText); if (response.status === 200) { prev[prop] = parsed; } } catch (err) { req.error(`Parse ${uri} failed.`); } return prev; }, {}); req.headersOut["Content-Type"] = "application/json;charset=UTF-8"; req.return(200, JSON.stringify(result)); }) .catch((e) => req.return(501, e.message));}export default { aggregation };
接着对 Nginx 配置文件中的部分进行调度:
...location / { js_content app.aggregation;}subrequest_output_buffer_size 200k;location /proxy/api-mysql { internal; gunzip on; proxy_pass https://www.mysql.com/; proxy_set_header Host www.mysql.com;}location /proxy/api-redis { internal; gunzip on; proxy_pass https://redislabs.com/; proxy_set_header Host redislabs.com;}...
末了再次启动做事,来验证我们能否拿到精确的远程数据,并将数据们进行聚合。
It works
看样子,我们已经拿到了我们想要的结果,接着来大略聊聊容器封装。
利用容器对 NJS 运用进行封装前文提到,NJS 模块由 Nginx 官方镜像默认支持,我们可以直策应用 nginx:1.19.8-alpine 为根本来进行镜像构建。
镜像文件非常大略,只须要三行:
FROM nginx:1.19.8-alpineCOPY nginx.conf /etc/nginx/nginx.confCOPY app.js /etc/nginx/script/app.js
将上面的内容保存为 Dockerfile,然后利用 docker build -t njs-api . 构建出我们的镜像。
如果你选择利用 docker images 查看镜像,你会创造我们构建的镜像非常小巧,险些能够和 Nginx 官方镜像尺寸保持同等,以是在公网分发的时候,会有非常大的上风,根据 docker 增量分发的特性,我们实在只会分发上面那三行配置中的后两行构建结果(layers),差不多几 KB。
njs-api latest f4b6de5dacb8 3 minutes ago 22.6MBnginx 1.19.8-alpine 5fd75c905b52 7 days ago 22.6MB
在构建镜像之后,利用 docker run --rm -it -p 8090:80 njs-api 可以进一步验证做事是否能够正常运行,不出意外,会得到上一小节图片中的结果。
末了好了,来总结一下。
本篇文章中,由于我们没有利用任何非 Nginx 镜像外的 Runtime ,以是得到的镜像结果非常小巧,十分利于进行网络分发。
同时由于 NJS 和 Nginx 大略清晰的设计理念,NJS 程序伴随要求生命周期结束而开释,NJS 引擎实行效率比较高,以及NJS 引擎本身只是实现了 ECMA 的一个子集(整体繁芜度低),加之子要求的生命周期非常短暂,以是我们的做事可以利用非常低的资源(靠近于 Nginx 原生资源占用)供应一个靠近 Nginx 原生做事的性能。
如果你常常写业务代码,你会创造本文留下了一些明显可以改进性能的话题没有诉诸笔墨:如何提聚合接口的性能,如何在定制过的 Nginx 镜像、环境中和三方模块一起事情,以及 NJS 到底能够干哪些更繁芜的活?