可以在 Nginx 中運行 JavaScript,厲害了!
引言
Nginx 作為市場占有率最高的Web服務器,主打高性能、可擴展。自帶了很多核心功能模塊,并且也有大量的第三方模塊。
Web 服務中灰度方案的實現,很多會采用 Nginx + Lua + Redis 方案。Lua 是一個輕量級的腳本語言,體積小、啟動速度快、性能高。通過 lua-nginx-module 模塊將 Lua 語言嵌入到 Nginx 中,可以使用 Lua 腳本擴展 Nginx 功能,并可以訪問 MySQL、Redis 等數據庫。
圖片
Lua 雖然是個強大的腳本語言,但過于小眾。Nginx 團隊選擇非常流行的 JavaScript 研發 NGINX JavaScript 模塊 (njs),讓更多工程師可以使用 JavaScript 來擴展 Nginx 功能,從而更好的發展 Nginx 社區生態。
圖片
NGINX JavaScript 簡介
NGINX JavaScript 簡稱 njs,是 JavaScript 語言的子集,實現了部分 ECMAScript 5.1(strict mode)規范和 ECMAScript 6 規范,可以使用 njs 來擴展 Nginx 功能。
njs 與 Node.js、JavaScript 的區別
一、運行時不同
Node.js 使用 V8 引擎,njs 是專門為 Nginx 定制設計的運行時。Node.js 使用 V8 引擎在內存中有一個持久化的 JavaScript 虛擬機 (VM) 并執行垃圾收集以進行內存管理;而 njs 是專門為 Nginx 設計,非常輕量,會為每個請求初始化一個新的 JavaScript VM 和必要的內存,并在請求完成時釋放內存。
二、語言規范差異
JavaScript 的規范是由 ECMAScript 標準定義,隨著標準版本的更新迭代,會支持更多的語言功能;njs 自研的服務端運行時,更多的優先支撐服務于 Nginx,只實現了 ECMAScript 5.1 和部分 ECMAScript 6,實現更多標準規范的同時,更多會考慮是否是 Nginx 所需要的。
njs 安裝&配置
安裝 nginx-module-njs 動態模塊,需要 Nginx 版本為 1.9.11 之后支持動態模塊的載入。
- yum install nginx-module-njs
安裝后,在配置文件 nginx.conf 中需要使用 load_module 指令加載 njs 動態模塊。
- 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 : 導入一個 njs 模塊,沒有指定模塊名稱則默認為文件名稱。
js_content : 使用 njs 模塊里導出的方法處理這個請求。
HTTP Proxying
使用 njs 模塊處理 HTTP 請求,并使用 subrequest 發起子請求。
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 : 可以去請求內部的其他 API ,headers 和該請求相同,并且可以在 location 塊里使用 proxy_set_header 來設置或覆蓋原來的 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 方法執行后,執行結果放到 $access_log_headers 變量中。但如果只被引用在 log_format 中,則只會在日志記錄階段被執行。
r : HTTP request 對象。屬性列表:http://nginx.org/en/docs/njs/reference.html#http
訪問數據庫
一、訪問 Redis
使用 redis2-nginx-module 動態模塊,結合 subrequest 來訪問 Redis 數據。
nginx.conf:
- js_import http.js;# GET /redis_get?key=some_keylocation = /redis_get { # 解碼 uri 中的參數 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 中參數的 %XX 編碼。
redis2_query : 執行的 Redis 命令。
redis2_pass : Redis 后端服務。
redis2_pass 返回值為類似 redis-cli 執行后的返回值,需要有一個 parser 來解析是否執行成。
二、訪問 MySQL
使用 drizzle-nginx-module 動態模塊,結合 subrequest 來訪問 MySQL 數據。
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 來設置 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 注入攻擊,來設置 sql 語句中的變量。
drizzle_query : 執行的 SQL 語句。
drizzle_pass : Drizzle 或 MySQL 服務的 upstream。
結語
在 njs 之前,Nginx+Lua 生態雖然已日趨成熟,但 Nginx 畢竟是一個 Web 服務器,JavaScript 作為 Web 開發的最流行的語言,可以使用 JavaScript 生態來擴展 Nginx 的功能,可能會更加的有一些想象力做更多的事情。