使用 Nginx 和 Lua 擴充套件 API

類別: IT

Nginx作為API代理

3scale我們是nginx的狂熱粉,幾周前,我們釋出了一個開源的API代理,它主要是使用一系列lua指令碼允許使用者進行簡單配置API代理的定製版nginx。有多簡單?檢視下指南,你會發現比想象的簡單多了。

有很多原因說明你為什使用nginx作為API代理。首先因為他是開源的;其次,Nginx有大量的安裝基礎,他背後有一個強大的社群支援,在效能方面也表現的非常出色。對於我們來說,這是顯而易見的,如果開源軟體有相同的解決方案我們為啥還要用那些私有的軟體。

另外一個極大的優勢就是nginx對lua的支援,nginx+lua是一個非常好的組合,它允許使用一個高效能的指令碼語言擴充套件nginx。nginx有很多方法是自帶的,但是使用lua沒有限制的。

原理很簡單。有沒有這樣的情況你更喜歡使用基於nginx的API代理而不是它自帶的方法呢?呵呵,你可以非常簡單的新增。

擴充套件目標: Sentiment API (可以是任何API)

為了展示nginx和lua的強大之處,我們將使用一個簡單的REST API呼叫Sentiment,不使用任何一行API原始碼(可以直接使用github上的)。

Sentiment API 是一個非常基礎的API,它返回一個有情感價值分析的單詞或者句子。比如,下面的請求(你可以自己試試)

curl http://api-sentiment.3scale.net/v1/word/fantastic.json
上面的請求將返回包含fantastic情感分析單詞的json串。
{"sentiment":4,"word":"fantastic"}

我們有了擴充套件物件,接下來繼續吧…

分析部分: 擴充套件Sentiment API

有很多方式你可以擴充套件Sentiment API(或者你自己的API)。為了符合這篇文章的主題,我們限定了三種場景來展示nginx+lua的強大之處和可擴充套件性。

1) 想做資料轉換?

想把輸出資料從json轉換為xml格式?或者更好一些,把xml轉換為json。

2) 想更換你的API方法的簽名?

你想把漂亮的 REST形式的url path/v1/word/WORD.json替換為貌似更加 “漂亮的” 簽名方式 parameters/sentiment?action=word&word=WORD&version=v1.

我們不能容忍變成那樣的路徑方式:-)這應該是個反面例子,自從Sentiment API改為RESTful方式,這個例子應該反過來了。

3) 想建立一個新的API方法?

沒問題,你可以自己建立個新的API方法得到你想要的,或者有可能,你可以不接觸任何API原始碼來擴充套件你的API方法。

我們將展示下建立一個Sentiment API的新方法:用來查詢在一個句子中最有情感分析價值的單詞。這個方法在Sentiment API沒有提供,但是我們可以通過nginx和lua建立它。

這個案例無論對於使用者還是對於API開發者多有很大的潛能。基本上可以允許你自己在不修改原始碼的基礎上定製API,或者,這還有酷斃了的一部分,允許你定製你不能控制的API。想在包含一系列方法的Twitter API上建立自己的方法?當然可以,結果可能是你的應用程式程式碼更簡潔了。

這只是使用nginx+lua擴充套件的三個簡單例子。還有一些其他例子我們只是為了突出使用nginx+lua擴充套件你的API有多麼簡單和強大。

讓我們開始做點實際的東西吧…

使用lua擴充套件nginx

我們假設你應經對nginx基礎概念有了瞭解(servers, locations, 等…)

擴充套件nginx我們必須先提供lua的支援,它不是ngnix的一部分。我們無需擔心因為已經有很多元件編譯進了lua,像:

  • openresty (在3scale)
  • tengine

如果你堅持自己安裝 :-) 你可以自己安裝下面的元件:

  • Lua nginx module
  • HttpProxy module

事實上,如果你不想用lua而是更喜歡perl,檢視下這個頁面look at the CPAN page,這裡提供了全部文件。

基礎部分

整個處理過程是代理請求到真實的API,主要通過下面過程:1)捕獲請求傳遞給API 2)響應請求,接著 3)處理響應。

下面展示了nginx配置檔案中的相關配置:

upstream backend {  # service name: API ;  server api-sentiment.3scale.net:80 max_fails=5 fail_timeout=30;}server {  listen 8181;  location ~ /v1/word/(.*)\.json$ {    proxy_pass http://backend/v1/word/$1.json ;  }}

這裡我們只配置了一個路由地址:/v1/word/your-word-goes-here.json。這個路由在Sentiment API上返回一個結果. Nginx 只是負責做一個簡單的傳遞。

你可以啟動你的nginx (監聽本地埠 8181) ,用下面的方式傳送一個請求

curl http://localhost:8181/v1/word/fantastic.json

它將返回一個同樣的json

{"sentiment":4,"word":"fantastic"}

我們只是給真實的Sentiment API做了箇中轉。讓我們帶著興趣繼續吧…

1) 資料轉換

JSON 到 XML

在nginx配置檔案新增新的路由,如下:

upstream backend {  # service name: API ;  server api-sentiment.3scale.net:80 max_fails=5 fail_timeout=30;}server {  listen 8181;  location ~ /v1/word/(.*)\.json$ {    proxy_pass http://backend/v1/word/$1.json ;  }  location ~ /v1/word/(.*)\.xml$ {    content_by_lua_file /PATH_TO/json_to_xml.lua;  }}

我們僅添加了一個新路由:/v1/word/your-word-goes-here.xml。這個路由將把 Sentiment API輸出的json轉換為xml格式。我們沒有做一個傳遞,而是通過呼叫一個lua檔案實現邏輯的(不要擔心,很簡單)。

現在你可以做下面的工作了,

curl http://localhost:8181/v1/word/fantastic.xml

你將獲取到下面資訊:

<?xml version="1.0" encoding="UTF-8"?><response>  <sentiment>4</sentiment>  <word>fantastic</word></response>

這裡發生了什麼?好吧,我們基本上把Sentiment API輸出的json資料轉換成了xml格式!

lua的魔法

轉化json為xml需要一系列的lua libs:

  • cjson :通過luarocks安裝或者在專案主頁上下載手動安裝。

  • luaXml :  我們將使用一個補丁版本來使他在nginx下工作,你可以在這裡下載補丁版本here

如果你在安裝luaxml時遇到問題,那麼可以直接安裝luarocks作為替代方案,把luaxml檔案放到openresty裡面的lua lib目錄下,查詢lua libs預設目錄就是openresty。

當我們訪問xml路由時,nginx將呼叫lua檔案

local xml = require("LuaXml")require("os")local cjson = require "cjson" local path = ngx.var.request:split(" ")[2]local m = ngx.re.match(path,[=[/([^/]+)\.(json|xml)$]=]) -- match last wordlocal res = ngx.location.capture("/v1/word/".. m[1] .. ".json" )local value=cjson.new().decode(res.body) local response = xml.new("response") response.word= xml.new("word")response.sentiment = xml.new("sentiment")response.timestamp = xml.new("timestamp")table.insert(response.word, value.word)table.insert(response.sentiment, value.sentiment)table.insert(response.timestamp, os.date()) ngx.say('<?xml version="1.0" encoding="UTF-8"?>', xml.str(response,0))

這個lua檔案做了一個本地json請求,使用下面的配置

local res = ngx.location.capture("/v1/word/".. m[1] .. ".json" )

它直接請求的真實的Sentiment API,一旦你有了json物件,我們就可以按照規則轉化為xml格式,從

{"sentiment":4,"word":"fantastic"}
<?xml version="1.0" encoding="UTF-8"?><response>  <sentiment>4</sentiment>  <word>fantastic</word></response>

注意split函式在lua中不存在,但是你可以參照這裡 but you can use this one.

現在,這個轉換是個手動過程,我們需要知道json的欄位名稱,但是我們也可以採用自動的方式分配json物件名稱為指定的xml標籤。

既然我們已經轉化為xml了,我們想要給輸出的xml新增額外的欄位,比如時間戳怎麼處理呢?

新增一個時間戳

在lua程式碼塊中,你有整個的lua環境變數可以自由使用,因此我們使用os模組來獲取當前時間。

我們僅需在ngx.say行之前新增下面幾行。

require("os")response.timestamp = xml.new("timestamp")table.insert(bar.timestamp, os.date())

當我們呼叫/xml時將從api輸出下面結果

<?xml version="1.0" encoding="UTF-8"?><response>  <sentiment>3</sentiment>  <word>hello</word>  <timestamp>Wed Jan  9 15:34:56 2013</timestamp></response>

酷斃了吧?怎麼樣,不難吧騷年 :)

XML 到 JSON

為了演示例子我們做一個從xml到json的轉換. 讓我們在nginx配置檔案中新增一個新的配置:

location ~ ^/round-trip/v1/word/(.*).json$ {  content_by_lua_file /PATH_TO/xml_to_json.lua;}

xml_to_json.lua如下所示:

local xml = require("LuaXml")local cjson = require "cjson" local path = ngx.var.request:split(" ")[2]local m = ngx.re.match(path,[=[/([^/]+)\.json]=])local res = ngx.location.capture("/v1/word/".. m[1] .. ".xml") local my_xml = xml.eval(res.body) local sent_val = my_xml:find("sentiment")[1]local word_val = my_xml:find("word")[1]local t = {sentiment = sent_val, word = word_val}local value=cjson.encode(t)ngx.say(value)

正如你所看到的,我們點選我們剛剛建立的xml端點路由時,那麼,我們將使用LuaXml解析xml並且使用cjson生成合理的json。

注意我們在這裡沒有遵循任何規範,轉換xml到json一般情況下是存在一些問題的,因為xml可讀性比json好。一般做轉換時你需要遵循一定的規範,比如BadgerFish或者 Parker,或者你自己建立的規範。

2) 重寫API方法

使用nginx重寫你的api方法是件微不足道的事,這樣對於開發者開說就更容易使用他們的api了。典型的例子就是舊的扭曲的API,我們想對其美化使得它對REST更加友好。

解決這個問題的一個方式是修改API原始碼的路由。然而,很多時候你不想改變原始碼,雖然改變原始碼也能實現,但是是一種落後的方法。克服那些接觸原始碼的擔憂,可以在nginx上新增一層,這樣就不用接觸和重新部署那些奇怪的程式碼了 :-)

為了用例子來說明,我們將轉換一個類似於REST的API方法

/v1/word/WORD.json

對於一些使用查詢引數的更“漂亮的”方式,如下:

/sentiment?action=word&version=v1&word=WORD

這種“升級”可以有多種方式實現。 對於熟悉nginx的可以簡單的通過重寫規則來解決這個問題。如果你更喜歡使用“sysadmin”方式,你可以按如下方式:

location ~ /sentiment$ {    content_by_lua '      local params = ngx.req.get_query_args()      if (params.action == "word" and params.version ~= nil) then        local res= ngx.location.capture("/".. params.version ..          "/word/" .. params.word .. ".json")        ngx.say(res.body)      end    ';}

正如上面這樣。現在sentiment API也接受如下舊的API方法:

curl http://localhost:8181/sentiment?action=word&word=fantastic&version=v1

這將返回預期的JSON物件。

3) 資料聚合

Nginx和lua可以幫助我們完成更為複雜的事情,像根據不同的方法組成一個全新的API方法。

在例子中,我們通過建立一個新的方法擴充套件了Sentiment API,這個方法返回一個句子中最有情感價值的單詞。

或許使用這樣的方法不值得大談特談:-D但是每次你都希望通過呼叫一個API完成4個方法呼叫,或者你可以通過單個任務呼叫其他3個不同的方法。你可以把你的方法聚集到一個API方法裡來供應用程式呼叫!

讓我們繼續看這個例子,首先我們需要新增一個新的配置,

location ~ ^/v1/max/(.*).json$ {  content_by_lua_file /PATH_TO/max.lua;}

接下來,我們只需要把聚集方法寫到lua腳本里:

local path = ngx.var.request:split(" ")[2] -- pathlocal t={}local cjson = require "cjson"ngx.log(0, path[2])local m = ngx.re.match(path,[=[^/v1/max/(.+).json]=])local words = m[1]:split("+") -- words in the sentence local max = nilfor i,k in pairs(words) dolocal res_word = ngx.location.capture("/v1/word/".. k .. ".json" )local value=cjson.new().decode(res_word.body)if max == nil or max.sentiment < value.sentiment thenmax = valueendendngx.say(cjson.new().encode(max))

如你所見,他不能再簡單了。首先,我們獲取到句子,切分單詞,然後對每個單詞呼叫API請求/v1/word。我們把情感分析價值較高的物件儲存起來。

最終結果很簡單,像下面的請求:

curl -g http://localhost:8181/v1/max/nginx+and+lua+are+amazing.json

我們獲取到積極情緒最高的單詞,

{"sentiment":4,"word":"amazing"}

max.lua聚合函式的邏輯可以按照你想要的更加複雜,也可以獲取到任何你的API方法,不管是不是你能控制的API。

能否外掛化? 完全可以。 你可以建立任意複雜的外掛,然後讓他們在應用程式中保持不可見。

結論

我們提到的這三個例子只是使用nginx和lua做的一個玩具性質的實驗。

在3scale上,我們已經把類似的架構應用於生產環境,像一些高負載環境,沒有什麼比這個結果更讓我們高興的了。

我們不斷地發現越來越多的地方可以使用這個特性,像netflix post一篇帖子最近提醒我們減少對APIs的呼叫次數可以在一些大業務量終端或者有缺陷的裝置上取得顯著的效能提升效果。

Nginx + lua 是一個改變常規的技術,雖然它不太常用,但是相信我們的話,一旦你嘗試下你就會被他的強大、靈活和簡單所吸引。

擴充套件一個API從來沒有這麼簡單過。愛過!


使用 Nginx 和 Lua 擴充套件 API原文請看這裡

推薦文章