Nginx作為API代理
有很多原因說(shuō)明你為什使用nginx作為API代理。首先因?yàn)樗情_(kāi)源的;其次,Nginx有大量的安裝基礎(chǔ),他背后有一個(gè)強(qiáng)大的社區(qū)支持,在性能方面也表現(xiàn)的非常出色。對(duì)于我們來(lái)說(shuō),這是顯而易見(jiàn)的,如果開(kāi)源軟件有相同的解決方案我們?yōu)樯哆€要用那些私有的軟件。
另外一個(gè)極大的優(yōu)勢(shì)就是nginx對(duì)lua的支持,nginx+lua是一個(gè)非常好的組合,它允許使用一個(gè)高性能的腳本語(yǔ)言擴(kuò)展nginx。nginx有很多方法是自帶的,但是使用lua沒(méi)有限制的。
原理很簡(jiǎn)單。有沒(méi)有這樣的情況你更喜歡使用基于nginx的API代理而不是它自帶的方法呢?呵呵,你可以非常簡(jiǎn)單的添加。
擴(kuò)展目標(biāo): Sentiment API (可以是任何API)
為了展示nginx和lua的強(qiáng)大之處,我們將使用一個(gè)簡(jiǎn)單的REST API調(diào)用Sentiment,不使用任何一行API源碼(可以直接使用github上的)。
Sentiment API 是一個(gè)非?;A(chǔ)的API,它返回一個(gè)有情感價(jià)值分析的單詞或者句子。比如,下面的請(qǐng)求(你可以自己試試)
復(fù)制代碼 代碼如下:
curl http://api-sentiment.3scale.net/v1/word/fantastic.json
上面的請(qǐng)求將返回包含fantastic情感分析單詞的json串。
復(fù)制代碼 代碼如下:
{"sentiment":4,"word":"fantastic"}
我們有了擴(kuò)展對(duì)象,接下來(lái)繼續(xù)吧…
分析部分: 擴(kuò)展Sentiment API
有很多方式你可以擴(kuò)展Sentiment API(或者你自己的API)。為了符合這篇文章的主題,我們限定了三種場(chǎng)景來(lái)展示nginx+lua的強(qiáng)大之處和可擴(kuò)展性。
1) 想做數(shù)據(jù)轉(zhuǎn)換?
想把輸出數(shù)據(jù)從json轉(zhuǎn)換為xml格式?或者更好一些,把xml轉(zhuǎn)換為json。
2) 想更換你的API方法的簽名?
你想把漂亮的 REST形式的url path/v1/word/WORD.json替換為貌似更加 “漂亮的” 簽名方式 parameters/sentiment?action=wordword=WORDversion=v1.
我們不能容忍變成那樣的路徑方式:-)這應(yīng)該是個(gè)反面例子,自從Sentiment API改為RESTful方式,這個(gè)例子應(yīng)該反過(guò)來(lái)了。
3) 想創(chuàng)建一個(gè)新的API方法?
沒(méi)問(wèn)題,你可以自己創(chuàng)建個(gè)新的API方法得到你想要的,或者有可能,你可以不接觸任何API源碼來(lái)擴(kuò)展你的API方法。
我們將展示下創(chuàng)建一個(gè)Sentiment API的新方法:用來(lái)查找在一個(gè)句子中最有情感分析價(jià)值的單詞。這個(gè)方法在Sentiment API沒(méi)有提供,但是我們可以通過(guò)nginx和lua創(chuàng)建它。
這個(gè)案例無(wú)論對(duì)于用戶還是對(duì)于API開(kāi)發(fā)者多有很大的潛能。基本上可以允許你自己在不修改源碼的基礎(chǔ)上定制API,或者,這還有酷斃了的一部分,允許你定制你不能控制的API。想在包含一系列方法的Twitter API上創(chuàng)建自己的方法?當(dāng)然可以,結(jié)果可能是你的應(yīng)用程序代碼更簡(jiǎn)潔了。
這只是使用nginx+lua擴(kuò)展的三個(gè)簡(jiǎn)單例子。還有一些其他例子我們只是為了突出使用nginx+lua擴(kuò)展你的API有多么簡(jiǎn)單和強(qiáng)大。
讓我們開(kāi)始做點(diǎn)實(shí)際的東西吧…
使用lua擴(kuò)展nginx
我們假設(shè)你應(yīng)經(jīng)對(duì)nginx基礎(chǔ)概念有了了解(servers, locations, 等…)
擴(kuò)展nginx我們必須先提供lua的支持,它不是ngnix的一部分。我們無(wú)需擔(dān)心因?yàn)橐呀?jīng)有很多組件編譯進(jìn)了lua,像:
- openresty (在3scale)
- tengine
如果你堅(jiān)持自己安裝 :-) 你可以自己安裝下面的組件:
- Lua nginx module
- HttpProxy module
事實(shí)上,如果你不想用lua而是更喜歡perl,查看下這個(gè)頁(yè)面look at the CPAN page,這里提供了全部文檔。
基礎(chǔ)部分
整個(gè)處理過(guò)程是代理請(qǐng)求到真實(shí)的API,主要通過(guò)下面過(guò)程:1)捕獲請(qǐng)求傳遞給API 2)響應(yīng)請(qǐng)求,接著 3)處理響應(yīng)。
下面展示了nginx配置文件中的相關(guān)配置:
復(fù)制代碼 代碼如下:
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 ;
}
}
這里我們只配置了一個(gè)路由地址:/v1/word/your-word-goes-here.json。這個(gè)路由在Sentiment API上返回一個(gè)結(jié)果. Nginx 只是負(fù)責(zé)做一個(gè)簡(jiǎn)單的傳遞。
你可以啟動(dòng)你的nginx (監(jiān)聽(tīng)本地端口 8181) ,用下面的方式發(fā)送一個(gè)請(qǐng)求
復(fù)制代碼 代碼如下:
curl http://localhost:8181/v1/word/fantastic.json
它將返回一個(gè)同樣的json
復(fù)制代碼 代碼如下:
{"sentiment":4,"word":"fantastic"}
我們只是給真實(shí)的Sentiment API做了個(gè)中轉(zhuǎn)。讓我們帶著興趣繼續(xù)吧…
1) 數(shù)據(jù)轉(zhuǎn)換
JSON 到 XML
在nginx配置文件添加新的路由,如下:
復(fù)制代碼 代碼如下:
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;
}
}
我們僅添加了一個(gè)新路由:/v1/word/your-word-goes-here.xml。這個(gè)路由將把 Sentiment API輸出的json轉(zhuǎn)換為xml格式。我們沒(méi)有做一個(gè)傳遞,而是通過(guò)調(diào)用一個(gè)lua文件實(shí)現(xiàn)邏輯的(不要擔(dān)心,很簡(jiǎn)單)。
現(xiàn)在你可以做下面的工作了,
curl http://localhost:8181/v1/word/fantastic.xml
你將獲取到下面信息:
復(fù)制代碼 代碼如下:
?xml version="1.0" encoding="UTF-8"?>
response>
sentiment>4/sentiment>
word>fantastic/word>
/response>
這里發(fā)生了什么?好吧,我們基本上把Sentiment API輸出的json數(shù)據(jù)轉(zhuǎn)換成了xml格式!
lua的魔法
轉(zhuǎn)化json為xml需要一系列的lua libs:
- cjson :通過(guò)luarocks安裝或者在項(xiàng)目主頁(yè)上下載手動(dòng)安裝。
- luaXml : 我們將使用一個(gè)補(bǔ)丁版本來(lái)使他在nginx下工作,你可以在這里下載補(bǔ)丁版本here
如果你在安裝luaxml時(shí)遇到問(wèn)題,那么可以直接安裝luarocks作為替代方案,把luaxml文件放到openresty里面的lua lib目錄下,查找lua libs默認(rèn)目錄就是openresty。
當(dāng)我們?cè)L問(wèn)xml路由時(shí),nginx將調(diào)用lua文件
復(fù)制代碼 代碼如下:
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 word
local 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))
這個(gè)lua文件做了一個(gè)本地json請(qǐng)求,使用下面的配置
復(fù)制代碼 代碼如下:
local res = ngx.location.capture("/v1/word/".. m[1] .. ".json" )
它直接請(qǐng)求的真實(shí)的Sentiment API,一旦你有了json對(duì)象,我們就可以按照規(guī)則轉(zhuǎn)化為xml格式,從
復(fù)制代碼 代碼如下:
{"sentiment":4,"word":"fantastic"}
到
復(fù)制代碼 代碼如下:
?xml version="1.0" encoding="UTF-8"?>
response>
sentiment>4/sentiment>
word>fantastic/word>
/response>
注意split函數(shù)在lua中不存在,但是你可以參照這里 but you can use this one.
現(xiàn)在,這個(gè)轉(zhuǎn)換是個(gè)手動(dòng)過(guò)程,我們需要知道json的字段名稱,但是我們也可以采用自動(dòng)的方式分配json對(duì)象名稱為指定的xml標(biāo)簽。
既然我們已經(jīng)轉(zhuǎn)化為xml了,我們想要給輸出的xml添加額外的字段,比如時(shí)間戳怎么處理呢?
添加一個(gè)時(shí)間戳
在lua代碼塊中,你有整個(gè)的lua環(huán)境變量可以自由使用,因此我們使用os模塊來(lái)獲取當(dāng)前時(shí)間。
我們僅需在ngx.say行之前添加下面幾行。
復(fù)制代碼 代碼如下:
require("os")
response.timestamp = xml.new("timestamp")
table.insert(bar.timestamp, os.date())
當(dāng)我們調(diào)用/xml時(shí)將從api輸出下面結(jié)果
復(fù)制代碼 代碼如下:
?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
為了演示例子我們做一個(gè)從xml到j(luò)son的轉(zhuǎn)換. 讓我們?cè)趎ginx配置文件中添加一個(gè)新的配置:
復(fù)制代碼 代碼如下:
location ~ ^/round-trip/v1/word/(.*).json$ {
content_by_lua_file /PATH_TO/xml_to_json.lua;
}
xml_to_json.lua如下所示:
復(fù)制代碼 代碼如下:
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)
正如你所看到的,我們點(diǎn)擊我們剛剛創(chuàng)建的xml端點(diǎn)路由時(shí),那么,我們將使用LuaXml解析xml并且使用cjson生成合理的json。
注意我們?cè)谶@里沒(méi)有遵循任何規(guī)范,轉(zhuǎn)換xml到j(luò)son一般情況下是存在一些問(wèn)題的,因?yàn)閤ml可讀性比json好。一般做轉(zhuǎn)換時(shí)你需要遵循一定的規(guī)范,比如BadgerFish 或者 Parker,或者你自己創(chuàng)建的規(guī)范。
2) 重寫(xiě)API方法
使用nginx重寫(xiě)你的api方法是件微不足道的事,這樣對(duì)于開(kāi)發(fā)者開(kāi)說(shuō)就更容易使用他們的api了。典型的例子就是舊的扭曲的API,我們想對(duì)其美化使得它對(duì)REST更加友好。
解決這個(gè)問(wèn)題的一個(gè)方式是修改API源碼的路由。然而,很多時(shí)候你不想改變?cè)创a,雖然改變?cè)创a也能實(shí)現(xiàn),但是是一種落后的方法。克服那些接觸源碼的擔(dān)憂,可以在nginx上添加一層,這樣就不用接觸和重新部署那些奇怪的代碼了 :-)
為了用例子來(lái)說(shuō)明,我們將轉(zhuǎn)換一個(gè)類似于REST的API方法
復(fù)制代碼 代碼如下:
/v1/word/WORD.json
對(duì)于一些使用查詢參數(shù)的更“漂亮的”方式,如下:
復(fù)制代碼 代碼如下:
/sentiment?action=wordversion=v1word=WORD
這種“升級(jí)”可以有多種方式實(shí)現(xiàn)。 對(duì)于熟悉nginx的可以簡(jiǎn)單的通過(guò)重寫(xiě)規(guī)則來(lái)解決這個(gè)問(wèn)題。如果你更喜歡使用“sysadmin”方式,你可以按如下方式:
復(fù)制代碼 代碼如下:
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
';
}
正如上面這樣?,F(xiàn)在sentiment API也接受如下舊的API方法:
復(fù)制代碼 代碼如下:
curl http://localhost:8181/sentiment?action=wordword=fantasticversion=v1
這將返回預(yù)期的JSON對(duì)象。
3) 數(shù)據(jù)聚合
Nginx和lua可以幫助我們完成更為復(fù)雜的事情,像根據(jù)不同的方法組成一個(gè)全新的API方法。
在例子中,我們通過(guò)創(chuàng)建一個(gè)新的方法擴(kuò)展了Sentiment API,這個(gè)方法返回一個(gè)句子中最有情感價(jià)值的單詞。
或許使用這樣的方法不值得大談特談:-D但是每次你都希望通過(guò)調(diào)用一個(gè)API完成4個(gè)方法調(diào)用,或者你可以通過(guò)單個(gè)任務(wù)調(diào)用其他3個(gè)不同的方法。你可以把你的方法聚集到一個(gè)API方法里來(lái)供應(yīng)用程序調(diào)用!
讓我們繼續(xù)看這個(gè)例子,首先我們需要添加一個(gè)新的配置,
復(fù)制代碼 代碼如下:
location ~ ^/v1/max/(.*).json$ {
content_by_lua_file /PATH_TO/max.lua;
}
接下來(lái),我們只需要把聚集方法寫(xiě)到lua腳本里:
復(fù)制代碼 代碼如下:
local path = ngx.var.request:split(" ")[2] -- path
local 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 = nil
for i,k in pairs(words) do
local 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 then
max = value
end
end
ngx.say(cjson.new().encode(max))
如你所見(jiàn),他不能再簡(jiǎn)單了。首先,我們獲取到句子,切分單詞,然后對(duì)每個(gè)單詞調(diào)用API請(qǐng)求/v1/word。我們把情感分析價(jià)值較高的對(duì)象存儲(chǔ)起來(lái)。
最終結(jié)果很簡(jiǎn)單,像下面的請(qǐng)求:
復(fù)制代碼 代碼如下:
curl -g http://localhost:8181/v1/max/nginx+and+lua+are+amazing.json
我們獲取到積極情緒最高的單詞,
復(fù)制代碼 代碼如下:
{"sentiment":4,"word":"amazing"}
max.lua聚合函數(shù)的邏輯可以按照你想要的更加復(fù)雜,也可以獲取到任何你的API方法,不管是不是你能控制的API。
能否插件化? 完全可以。 你可以創(chuàng)建任意復(fù)雜的插件,然后讓他們?cè)趹?yīng)用程序中保持不可見(jiàn)。
結(jié)論
我們提到的這三個(gè)例子只是使用nginx和lua做的一個(gè)玩具性質(zhì)的實(shí)驗(yàn)。
在3scale上,我們已經(jīng)把類似的架構(gòu)應(yīng)用于生產(chǎn)環(huán)境,像一些高負(fù)載環(huán)境,沒(méi)有什么比這個(gè)結(jié)果更讓我們高興的了。
我們不斷地發(fā)現(xiàn)越來(lái)越多的地方可以使用這個(gè)特性,像netflix post一篇帖子最近提醒我們減少對(duì)APIs的調(diào)用次數(shù)可以在一些大業(yè)務(wù)量終端或者有缺陷的設(shè)備上取得顯著的性能提升效果。
Nginx + lua 是一個(gè)改變常規(guī)的技術(shù),雖然它不太常用,但是相信我們的話,一旦你嘗試下你就會(huì)被他的強(qiáng)大、靈活和簡(jiǎn)單所吸引。
擴(kuò)展一個(gè)API從來(lái)沒(méi)有這么簡(jiǎn)單過(guò)。愛(ài)過(guò)!
您可能感興趣的文章:- 安裝Nginx+Lua開(kāi)發(fā)環(huán)境
- nginx中使用lua腳本的方法
- openresty中使用lua-nginx創(chuàng)建socket實(shí)例
- Nginx+Lua+Redis構(gòu)建高并發(fā)Web應(yīng)用
- 使用nginx+lua實(shí)現(xiàn)信息訪問(wèn)量統(tǒng)計(jì)
- linux系統(tǒng)安裝Nginx Lua環(huán)境