nginScript系列:nginScript简介
这篇文章是nginScript系列文章的第一篇,介绍了Nginx公司为什么要开发自己的JavaScript实现,并提供了一个nginScript入门的示例。
nginScript项目自2015年9月启动以来,一直处于实验性阶段,不过有很多特性和核心语言支持不断被添加进来。随着NGINX Plus R12的发布,我们很高兴地宣布,nginScript现在正式成为NGINX和NGINX Plus的一个稳定模块。
nginScript是为了NGINX和NGINX Plus而开发的JavaScript实现,它被设计用于在服务器端处理请求。它通过融入JavaScript代码对NGINX的配置语法进行扩展,以便实现复杂的配置。
nginScript同时支持HTTP和TCP/UDP两种协议,所以它的应用场景很广。
- 生成自定义的日志格式, 日志里可以包含普通NGINX变量无法表示的值。
- 实现新的负载均衡算法。
- 通过解析TCP/UDP协议,实现应用层的粘性会话。
除此之外,nginScript还有很多其他特性,不过还有很多特性还没有实现。虽然我们发布了具有一般可用性的nginScript版本,并且可被用于生产环境,不过,我们仍然计划了一个演化线路图,包含了更多的应用场景。
- 检查和修改HTTP请求消息和响应消息的body(已经支持TCP/UDP)。
- 从nginScript代码里发起HTTP子请求。
- 编写HTTP认证处理器(已经支持TCP/UDP)。
- 读写文件。
在更详细地讨论nginScript之前,我们先来澄清两个概念。
nginScript不是Lua
多年来,NGINX社区创建了很多编程式扩展,Lua是其中最受欢迎的一个。Lua被作为NGINX的一个模块,也是通过NGINX Plus认证的第三方模块。Lua模块和它的扩展插件与NGINX内核深度集成,提供了非常丰富的功能,其中就包括Redis的驱动程序。
Lua是一个强大的脚本语言,不过,在市场采用率方面仍然不是很理想,而且对于一线开发人员和DevOps工程师来说,它也不在他们的“必备技能”之列。
nginScript并不是要取代Lua,nginScript的目标是要通过使用一门流行的编程语言为广大的社区提供一种编程式的配置方案。
nginScript不是Node.js
nginScript的目标并不在于要把NGINX或者NGINX Plus升格成为应用服务器。简单地说,nginScript的应用场景有点类似于中间件,因为nginScript的代码运行在客户端和服务器内容之间。从技术角度来看,nginScript和NGINX(或NGINX Plus)组合在一起之后有点类似Node.js,不过它们与Node.js的相似之处也仅限于两点,即采用了基于事件驱动的架构以及使用JavaScript作为编程语言。
Node.js使用的是Google的V8 JavaScript引擎,而nginScript实现了ECMAScript标准,是为了NGINX和NGINX Plus而特别设计的。Node.js在内存里运行稳定的JavaScript虚拟机,并通过垃圾回收来管理内存,而nginScript会为每个请求启动一个JavaScript虚拟机,并为其分配必要的内存,在请求处理完毕之后清理内存。
JavaScript作为服务器端的编程语言
上面已经提到过,nginScript是JavaScript的一个定制实现。其他大部分JavaScript运行时引擎都是为浏览器而设计的。客户端的代码执行属性在很多方面与服务器端的不一样,这些不同点表现在系统资源的可用性和可创建的并发运行时数量等方面。
我们之所以要实现自己的JavaScript运行时,是为了满足服务器端代码的执行需求,并与NGINX的请求处理架构保持兼容。nginScript的设计遵循了如下原则。
根据请求来创建和销毁运行时环境
nginScript使用单线程执行字节码,可以实现快速的初始化和销毁。系统为每个请求创建一个新的运行时环境,因为不需要初始化复杂的状态和辅助组件,所以启动速度非常快。在处理请求期间分配一个内存池,请求处理完毕之后销毁内存池。这种内存使用模式避免了对象的跟踪和释放工作,也不需要使用垃圾回收器。
非阻塞的代码执行
nginScript的运行时环境是通过NGINX和NGINX Plus的事件驱动模型来执行的。当某个nginScript规则在执行阻塞操作时(比如读取网络数据或向外部发起子请求),NGINX和NGINX Plus会将该虚拟机挂起,直到阻塞操作事件结束。也就是说,规则的编写会变得很简单,NGINX和NGINX Plus会在内部把它们变成非阻塞操作。
只实现必要的语言特性
ECMAScript定义了JavaScript的规范。nginScript实现了ECMAScript 5.1,以及ECMAScript 6中与数学运算相关的规范。实现自己的JavaScript运行时给了我们充分的自由,我们可以优先实现服务器端需要的特性,忽略掉我们不需要的部分。我们给出了一个列表,包括已经支持的语言特性和尚未支持的部分。
与请求处理阶段的深度集成
NGINX和NGINX Plus把请求的处理分为不同的阶段。配置指令一般会在一个特定的阶段执行,本地NGINX模块经常利用这个特性检查和修改请求消息的内容。nginScript通过配置指令把某些处理阶段暴露出来,从而能够控制JavaScript代码的执行。这种方式保证了本地NGINX模块的强大和灵活,同时保持JavaScript代码的简单性。
下面的表格列出了可以通过nginScript来访问的处理阶段,以及相应的配置指令。
处理阶段 | HTTP模块 | 流(TCP/UDP)模块 |
访问阶段——网络连接访问控制 | X | [js_access](http://nginx.org/en/docs/stream/ngx_stream_js_module.html?&_ga=1.73259859.1538733534.1490062010#js_access) |
预读取阶段——读取/写入body | X | [js_preread](http://nginx.org/en/docs/stream/ngx_stream_js_module.html?&_ga=1.73259859.1538733534.1490062010#js_preread) |
过滤阶段——在代理期间读取/写入body | X | [js_filter](http://nginx.org/en/docs/stream/ngx_stream_js_module.html?&_ga=1.73259859.1538733534.1490062010#js_filter) |
返回内容阶段——发送响应给客户端 | [js_content](http://nginx.org/en/docs/http/ngx_http_js_module.html?&_ga=1.14475127.1538733534.1490062010#js_content) | X |
日志/变量——按需计算 | [js_set](http://nginx.org/en/docs/http/ngx_http_js_module.html?&_ga=1.14475127.1538733534.1490062010#js_set) | [js_set](http://nginx.org/en/docs/stream/ngx_stream_js_module.html?&_ga=1.14475127.1538733534.1490062010#js_set) |
nginScript入门——一个真实的例子
我们可以将nginScript作为模块编译到NGINX里,也可以动态地将其加载到NGINX或NGINX Plus里。这篇文章的末尾将介绍如何在NGINX和NGINX Plus里启用nginScript。
下面的例子使用NGINX或NGINX Plus作为简单的反向代理,并用nginScript构造具有特定格式的访问日志,每个日志里包含了如下几项内容。
- 由客户端发送的请求消息头。
- 由服务器端返回给客户端的响应消息头。
- 使用键值对的格式,方便日志处理工具(ELK、Graylog、Splunk)处理这些日志。
NGINX的配置非常简单:
js_include conf.d/header_logging.js; # Load JavaScript code from here
js_set $access_log_with_headers kvAccessLog; # Fill variable from JS function
log_format kvpairs $access_log_with_headers; # Define special log format
server {
listen 80;
access_log /var/log/nginx/access_headers.log kvpairs;
location / {
proxy_pass http://www.example.com;
}
}
从上面可以看出,配置里并没有直接包含nginScript代码。我们使用js_include指令指定包含了JavaScript代码的文件。js_set指令定义了一个NGINX变量\$access_log_with_headers,以及处理这个变量的JavaScript函数。log_format指令定义了新的格式kvpairs,日志里的每一行将包含\$access_log_with_headers的值。server代码块定义了一个简单的HTTP反向代理,它将所有的请求转向到http://www.example.com。access_log指令表示所有的请求消息将会以kvpairs的格式被记录下来。
现在来看一下格式化日志的JavaScript代码。我们有两个函数:
- kvHeaders——这个函数把headers对象转成一系列键值对。函数必须在被调用前声明。
- kvAccessLog——这个函数是在js_set指令里定义的,它接收两个参数,一个是客户端的请求(req),一个是服务器端的响应(res)。这类内建对象可以被传给任何一个nginScript函数。
function kvHeaders(headers, parent) {
var kvpairs = "";
for (var h in headers) {
kvpairs += " " + parent + "." + h + "=";
if ( headers[h].indexOf(" ") == -1 ) {
kvpairs += headers[h];
} else {
kvpairs += "'" + headers[h] + "'";
}
}
return kvpairs;
}
function kvAccessLog(req, res) {
var log = req.variables.time_iso8601; // nginScript可以访问所有变量
log += " client=" + req.remoteAddress; // request对象的属性
log += " method=" + req.method; // "
log += " uri=" + req.uri; // "
log += " status=" + res.status; // response对象的属性
log += kvHeaders(req.headers, "req"); // 把request header对象传给函数
log += kvHeaders(res.headers, "res"); // 把response header对象传给函数
return log;
}
kvAccessLog函数的返回值被传给了js_set配置指令。NGINX变量是按需进行计算的,也就是说,只有当变量需要被用到的时候,js_set定义的函数才会被执行。在这个例子里,log_format指令使用了变量$access_log_with_headers,所以kvAccessLog()函数会在记录日志时执行。
map和rewrite指令所使用的变量会在早期处理阶段触发执行相应的JavaScript代码。
我们可以向反向代理发送一个请求,并观察日志文件,以便验证这个方案。
$ curl http://127.0.0.1/
$ tail --lines=1 /var/log/nginx/access_headers.log
2017-03-14T14:36:53+00:00 client=127.0.0.1 method=GET uri=/
status=200 req.Host=127.0.0.1 req.User-Agent=curl/7.47.0 req.Accept=
*/* res.Cache-Control=max-age=604800 res.Etag=\x22359670651+
ident\x22 res.Expires='
Tue, 21 Mar 2017 14:36:53 GMT'
res.Last-Modified='Fri, 09 Aug 2013 23:54:35 GMT'
res.Vary=Accept-Encoding res.X-Cache=HIT
大多数情况下,我们使用nginScript来访问NGINX的内部结构。上面的例子还利用了request和response对象的一些属性,TCP/UDP的Stream nginScript模块则利用了session对象的一些属性。
查看英文原文:Introduction to nginScript