可以在 Nginx 中運(yùn)行 JavaScript,厲害了!
引言
Nginx 作為市場占有率最高的Web服務(wù)器,主打高性能、可擴(kuò)展。自帶了很多核心功能模塊,并且也有大量的第三方模塊。
Web 服務(wù)中灰度方案的實(shí)現(xiàn),很多會(huì)采用 Nginx + Lua + Redis 方案。Lua 是一個(gè)輕量級(jí)的腳本語言,體積小、啟動(dòng)速度快、性能高。通過 lua-nginx-module 模塊將 Lua 語言嵌入到 Nginx 中,可以使用 Lua 腳本擴(kuò)展 Nginx 功能,并可以訪問 MySQL、Redis 等數(shù)據(jù)庫。
圖片
Lua 雖然是個(gè)強(qiáng)大的腳本語言,但過于小眾。Nginx 團(tuán)隊(duì)選擇非常流行的 JavaScript 研發(fā) NGINX JavaScript 模塊 (njs),讓更多工程師可以使用 JavaScript 來擴(kuò)展 Nginx 功能,從而更好的發(fā)展 Nginx 社區(qū)生態(tài)。
圖片
NGINX JavaScript 簡介
NGINX JavaScript 簡稱 njs,是 JavaScript 語言的子集,實(shí)現(xiàn)了部分 ECMAScript 5.1(strict mode)規(guī)范和 ECMAScript 6 規(guī)范,可以使用 njs 來擴(kuò)展 Nginx 功能。
njs 與 Node.js、JavaScript 的區(qū)別
一、運(yùn)行時(shí)不同
Node.js 使用 V8 引擎,njs 是專門為 Nginx 定制設(shè)計(jì)的運(yùn)行時(shí)。Node.js 使用 V8 引擎在內(nèi)存中有一個(gè)持久化的 JavaScript 虛擬機(jī) (VM) 并執(zhí)行垃圾收集以進(jìn)行內(nèi)存管理;而 njs 是專門為 Nginx 設(shè)計(jì),非常輕量,會(huì)為每個(gè)請(qǐng)求初始化一個(gè)新的 JavaScript VM 和必要的內(nèi)存,并在請(qǐng)求完成時(shí)釋放內(nèi)存。
二、語言規(guī)范差異
JavaScript 的規(guī)范是由 ECMAScript 標(biāo)準(zhǔn)定義,隨著標(biāo)準(zhǔn)版本的更新迭代,會(huì)支持更多的語言功能;njs 自研的服務(wù)端運(yùn)行時(shí),更多的優(yōu)先支撐服務(wù)于 Nginx,只實(shí)現(xiàn)了 ECMAScript 5.1 和部分 ECMAScript 6,實(shí)現(xiàn)更多標(biāo)準(zhǔn)規(guī)范的同時(shí),更多會(huì)考慮是否是 Nginx 所需要的。
njs 安裝&配置
安裝 nginx-module-njs 動(dòng)態(tài)模塊,需要 Nginx 版本為 1.9.11 之后支持動(dòng)態(tài)模塊的載入。
- yum install nginx-module-njs
安裝后,在配置文件 nginx.conf 中需要使用 load_module 指令加載 njs 動(dòng)態(tài)模塊。
- load_module modules/ngx_http_js_module.so;
njs 基本使用
Hello World
nginx.conf:
- http {
- js_import http.js;
- # or js_import http from http.js;
- server {
- listen 8000;
- location / {
- js_content http.hello;
- }
- }
- }
http.js:
- function hello(r) {
- r.return(200, "Hello world!");
- }
- export default { hello };
js_import : 導(dǎo)入一個(gè) njs 模塊,沒有指定模塊名稱則默認(rèn)為文件名稱。
js_content : 使用 njs 模塊里導(dǎo)出的方法處理這個(gè)請(qǐng)求。
HTTP Proxying
使用 njs 模塊處理 HTTP 請(qǐng)求,并使用 subrequest 發(fā)起子請(qǐng)求。
nginx.conf:
- js_import http.js;
- location /start {
- js_content http.content;
- }
- location /foo {
- proxy_pass <http://backend1>;
- }
- location /bar {
- proxy_pass <http://backend2>;
- }
http.js:
- function content(r) {
- r.subrequest('/api/5/foo', {
- method: 'POST',
- body: JSON.stringify({ foo: 'foo', bar: "bar" })
- }, function(res) {
- if (res.status != 200) {
- r.return(res.status, res.responseBody);
- return;
- }
- var json = JSON.parse(res.responseBody);
- r.return(200, json.content);
- });
- }
- export default { content };
r.subrequest : 可以去請(qǐng)求內(nèi)部的其他 API ,headers 和該請(qǐng)求相同,并且可以在 location 塊里使用 proxy_set_header 來設(shè)置或覆蓋原來的 header。
自定義日志輸出格式
使用 njs 定制 Nginx 日志的輸出格式。
nginx.js:
- js_import logging.js;
- js_set $access_log_headers logging.kvAccess;
- log_format kvpairs $access_log_headers;
- server {
- listen 80;
- root /usr/share/nginx/html;
- access_log /var/log/nginx/access.log kvpairs;
- }
logging.js:
- function kvAccess(r) {
- var log = `${r.variables.time_iso8601} client=${r.remoteAddress} method=${r.method} uri=${r.uri} status=${r.status}`;
- r.rawHeadersIn.forEach(h => log += ` in.${h[0]}=${h[1]}`);
- r.rawHeadersOut.forEach(h => log += ` out.${h[0]}=${h[1]}`);
- return log;
- }
- export default { kvAccess }
js_set : 將 njs 模塊里的 kvAccess 方法執(zhí)行后,執(zhí)行結(jié)果放到 $access_log_headers 變量中。但如果只被引用在 log_format 中,則只會(huì)在日志記錄階段被執(zhí)行。
r : HTTP request 對(duì)象。屬性列表:http://nginx.org/en/docs/njs/reference.html#http
訪問數(shù)據(jù)庫
一、訪問 Redis
使用 redis2-nginx-module 動(dòng)態(tài)模塊,結(jié)合 subrequest 來訪問 Redis 數(shù)據(jù)。
nginx.conf:
- js_import http.js;# GET /redis_get?key=some_keylocation = /redis_get { # 解碼 uri 中的參數(shù) key,賦值到變量 $key set_unescape_uri $key $arg_key; redis2_query get $key; redis2_pass 127.0.0.1:6379;}# GET /redis_set?key=one&val=first%20valuelocation = /redis_set { set_unescape_uri $key $arg_key; set_unescape_uri $val $arg_val; redis2_query set $key $val; redis2_pass 127.0.0.1:6379;}# GET /get_redis_data?key=some_keylocation /get_redis_data { js_content http.get_redis_data;}
http.js:
- function serialize(obj) { var str = []; for (var p in obj) { if (obj.hasOwnProperty(p)) { str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); } } return str.join("&");};function get_redis_data(r) { r.subrequest('/redis_get', { args: serialize(r.args), method: 'GET' }, function(res) { if (res.status != 200) { r.return(res.status, res.responseBody); return; } r.return(200, res.responseBody); }); return log;}export default { get_redis_data }
set_unescape_uri :解碼 uri 中參數(shù)的 %XX 編碼。
redis2_query : 執(zhí)行的 Redis 命令。
redis2_pass : Redis 后端服務(wù)。
redis2_pass 返回值為類似 redis-cli 執(zhí)行后的返回值,需要有一個(gè) parser 來解析是否執(zhí)行成。
二、訪問 MySQL
使用 drizzle-nginx-module 動(dòng)態(tài)模塊,結(jié)合 subrequest 來訪問 MySQL 數(shù)據(jù)。
nginx.conf:
- upstream backend {
- drizzle_server 127.0.0.1:3306 dbname=test
- password=some_pass user=monty protocol=mysql;
- }
- server {
- js_import http.js;
- location /mysql {
- set_unescape_uri $name $arg_name;
- # 為防止 SQL 注入攻擊,使用 set_quote_sql_str 來設(shè)置 sql 語句中的變量
- set_quote_sql_str $quoted_name $name;
- drizzle_query "select * from cats where name = $quoted_name";
- drizzle_pass backend;
- drizzle_connect_timeout 500ms; # default 60s
- drizzle_send_query_timeout 2s; # default 60s
- drizzle_recv_cols_timeout 1s; # default 60s
- drizzle_recv_rows_timeout 1s; # default 60s
- }
- # GET /get_mysql_data?name=cat_name
- location /get_mysql_data {
- js_content http.get_mysql_data;
- }
- }
http.js:
- function serialize(obj) {
- var str = [];
- for (var p in obj) {
- if (obj.hasOwnProperty(p)) {
- str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
- }
- }
- return str.join("&");
- };
- function get_mysql_data(r) {
- r.subrequest('/mysql', {
- args: serialize(r.args),
- method: 'GET'
- }, function(res) {
- if (res.status != 200) {
- r.return(res.status, res.responseBody);
- return;
- }
- r.return(200, res.responseBody);
- });
- return log;
- }
- export default { get_mysql_data }
set_quote_sql_str : 為防止 SQL 注入攻擊,來設(shè)置 sql 語句中的變量。
drizzle_query : 執(zhí)行的 SQL 語句。
drizzle_pass : Drizzle 或 MySQL 服務(wù)的 upstream。
結(jié)語
在 njs 之前,Nginx+Lua 生態(tài)雖然已日趨成熟,但 Nginx 畢竟是一個(gè) Web 服務(wù)器,JavaScript 作為 Web 開發(fā)的最流行的語言,可以使用 JavaScript 生態(tài)來擴(kuò)展 Nginx 的功能,可能會(huì)更加的有一些想象力做更多的事情。