当前位置:首页 >> 脚本专栏

Lua和Nginx结合使用的超级指南

 Nginx作为API代理

有很多原因说明你为什使用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) 想做数据转换"_blank" href="http://openresty.org/">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

    你将获取到下面信息:

    复制代码 代码如下:<"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 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('<"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"}

    复制代码 代码如下:<"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添加额外的字段,比如时间戳怎么处理呢"codetitle">复制代码 代码如下:require("os")
    response.timestamp = xml.new("timestamp")
    table.insert(bar.timestamp, os.date())

    当我们调用/xml时将从api输出下面结果

    复制代码 代码如下:<"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"codetitle">复制代码 代码如下: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"codetitle">复制代码 代码如下:location ~ ^/v1/max/(.*).json$ {
      content_by_lua_file /PATH_TO/max.lua;
    }

    接下来,我们只需要把聚集方法写到lua脚本里:

    复制代码 代码如下: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))

    如你所见,他不能再简单了。首先,我们获取到句子,切分单词,然后对每个单词调用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从来没有这么简单过。爱过!