nginScript 系列:nginScript 简介

阅读数:2563 2017 年 4 月 12 日 17:11

这篇文章是 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 来访问的处理阶段,以及相应的配置指令。

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


感谢郭蕾对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

收藏

评论

微博

用户头像
发表评论

注册/登录 InfoQ 发表评论