Openresty的开发闭环初探

Web安全 1 14040
岂安科技
岂安科技 2018年01月05日

1. 为什么值得入手?

Nginx 作为现在使用最广泛的高性能后端服务器,Openresty 为之提供了动态预言的灵活,当性能与灵活走在了一起,无疑对于被之前陷于臃肿架构,苦于提升性能的工程师来说是重大的利好消息,本文就是在这种背景下,将初入这一未知的领域之后的一些经验与大家分享一下,若有失言之处,欢迎指教。


2. 安装

现在除了能在 [Download](http://openresty.org/en/download.html )里面下载源码来自己编译安装,现在连预编译好的[](http://openresty.org/en/linux-packages.html)都有了, 安装也就分分钟的事了。


3.  hello world

/path/to/nginx.conf`, `conftent_by_lua_file 里面的路径请根据 lua_package_path 调整一下。


    ```

    location / {

        content_by_lua_file ../luablib/hello_world.lua;

    }

    ```


`/path/to/openresty/lualib/hello_world.lua`

    ```

    ngx.say("Hello World")

    ```

访问一下, Hello World~.

    ```

    HTTP/1.1 200 OK

    Connection: keep-alive

    Content-Type: application/octet-stream

    Date: Wed, 11 Jan 2017 07:52:15 GMT

    Server: openresty/1.11.2.2

    Transfer-Encoding: chunked

    Hello World

    ```


基本上早期的 Openresty 相关的开发的路数也就大抵如此了, 将 lua 库发布到 lualib 之下,将对应的 nginx 的配置文件发布到 nginx/conf 底下,然后 reload 已有的 Openresty 进程(少数需要清空 Openresty shared_dict 数据的情况需要重启 )。


如果是测试环境的话,那更是简单了,在 http 段将 lua_code_cache 设为 off , Openresty 不会缓存 lua 脚本,每次执行都会去磁盘上读取 lua 脚本文件的内容,发布之后就可以直接看效果了(当然如果配置文件修改了,reload 是免不了了)。是不是找到一点当初 apache 写 php 的感觉呢:)


4. 开发语言 Lua 的大致介绍

环境搭建完毕之后,接下来就是各种试错了。关于 Lua 的介绍,网上的资料比如:Openresty 最佳实践(版本比较多,这里就不放了)写的都会比较详细,本文就不在这里过多解释了,只展示部分基础的Lua的模样。


下面对 lua 一些个性有趣的地方做一下分享,可能不会涉及到 lua 语言比较全面或者细节的一些部分,作为补充,读者可以翻阅官方的<<Programing in Lua>>。


    ```lua

    -- 单行注释以两个连字符开头

    --[[

多行注释

    --]]

    -- 变量赋值

    num = 13  -- 所有的数字都是双精度浮点型。

    s = '单引号字符串'

    t = "也可以用双引号"

    u = [[ 多行的字符串

           ]]

    -- 控制流程,和python最明显的差别可能就是冒号变成了do, 最后还得数end的对应

    -- while

    while n < 10 do

      n = n + 1  -- 不支持 ++ 或 += 运算符。

    end

    -- for

    for i = 0, 9 do

      print(i)

    end

    -- if语句:

    f n == 0 then

      print("no hits")

    elseif n == 1 then

      print("one hit")

    else

      print(n .. " hits")

    end

    --只有nil和false为假; 0和 ''均为真!

    if not aBoolValue then print('false') end

    -- 循环的另一种结构:

    repeat

      print('the way of the future')

      num = num - 1

    until num == 0

    -- 函数定义:

    function add(x, y)

      return x + y

    end

    -- table 用作键值对

    t = {key1 = 'value1', key2 = false}

    print(t.key1)  -- 打印 'value1'.

    -- 使用任何非nil的值作为key:

    u = {['@!#'] = 'qbert', [{}] = 1729, [6.28] = 'tau'}

    print(u[6.28])  -- 打印 "tau"

    -- table用作列表、数组

    v = {'value1', 'value2', 1.21, 'gigawatts'}

    for i = 1, #v do  -- #v 是列表的大小

      print(v[i])

    end

    -- 元表

    f1 = {a = 1, b = 2}  -- 表示一个分数 a/b.

    f2 = {a = 2, b = 3}

    -- 这会失败:

    -- s = f1 + f2

    metafraction = {}

    function metafraction.__add(f1, f2)

      local sum = {}

      sum.b = f1.b * f2.b

      sum.a = f1.a * f2.b + f2.a * f1.b

      return sum

    end

    setmetatable(f1, metafraction)

    setmetatable(f2, metafraction)

    s = f1 + f2  -- 调用在f1的元表上的__add(f1, f2) 方法

    -- __index、__add等的值,被称为元方法。

    -- 这里是一个table元方法的清单:

    -- __add(a, b)                     for a + b

    -- __sub(a, b)                     for a - b

    -- __mul(a, b)                     for a * b

    -- __div(a, b)                     for a / b

    -- __mod(a, b)                     for a % b

    -- __pow(a, b)                     for a ^ b

    -- __unm(a)                        for -a

    -- __concat(a, b)                  for a .. b

    -- __len(a)                        for #a

    -- __eq(a, b)                      for a == b

    -- __lt(a, b)                      for a < b

    -- __le(a, b)                      for a <= b

    -- __index(a, b)  <fn or a table>  for a.b

    -- __newindex(a, b, c)             for a.b = c

    -- __call(a, ...)                  for a(...)

    ```


以上参考了

[learn lua in y minute](https://learnxinyminutes.com/docs/zh-cn/lua-cn/ ) ,做了适当的裁剪来做说明。


4.1  Lua 语言个性的一面

4.1.1  第一道墙: 打印 table

作为 lua 里面唯一标准的数据结构, 直接打印居然只有一个 id 状的东西,这里说这一点没有抱怨的意思,只是让读者做好倒腾的心理准备,毕竟倒腾一个简洁语言终归是有代价的。

了解决定背后的原因,有时候比现成的一步到位的现成方案这也是倒腾的另一面好处吧,这里给出社区里面的[讨论](http://luausers.org/wiki/TableSerialization)


举个例子:

lua 里面一般使用 #table 来获取 table 的长度,究其原因,lua 对于未定义的变量、table 的键,总是返回 nil,而不像 python 里面肯定是抛出异常, 所以 来计算 table 长度的时候只会遍历到第一个值为 nil 的地方,毕竟他不能一直尝试下去,这时候就需要使用 table.maxn 的方式来获取了。


4.1.2  Good or Bad ? 自动类型转换


如果你在 python 里面去把一个字符串和数字相加,python 必定以异常回应。


    ```python

    >>> "a" + 1

    Traceback (most recent call last):

      File "<stdin>", line 1, in <module>

    TypeError: cannot concatenate 'str' and 'int' objects

    ```


但是 Lua 觉得他能搞定。


    ```lua

    > = "20" + 10

    30

    ```


如果你觉得 Lua 选择转换加号操作符的操作数成数字类型去进行求值显得不可思议的,下面这种情况下,这种转换又貌似是可以有点用的了 print("hello" .. 123) ,这时你不用手动去将所有参数手工转换成字符串类型。


尚没有定论说这项特性就是一无是处,但是这种依赖语言本身不明显的特性的代码笔者是不希望在项目里面去踩雷的。



4.1.3  多返回值

Lua 开始变得越来越与众不同了:允许函数返回多个结果。


    ```

    function foo0() end --无返回值

    function foo1() return 'a' end -- 返回一个结果

    function foo2() return 'a','b' end -- 返回两个结果

    -- 多重赋值时, 函数调用是最后一个表达式时

    -- 保留尽可能多的返回值

    x, y = foo2()     -- x='a', y='b'

    x = foo2()        -- x='a', 'b'被丢弃

    x,y,z = 10,foo2()    -- x=10, y='a', z='b'

    -- 如果多重赋值时,函数调用不是最后一个表达式时

    -- 只产生一个值

    x, y = foo2(),20   -- x='a', y=20   

    x,y = foo0(), 20, 30 -- x=nil, y= 20,30被丢弃,这种情况当函数没有返回值时,会用nil来补充。

    x,y,z = foo2() -- x='a', y='b', z=nil, 这种情况函数没有足够的返回值时也会用nil来补充。

    -- 同样在函数调用、table声明中 函数调用作为最后的表达式,都会竟可能多的填充返回值,如果不是最后,则只返回一个

    print(foo2(), 1)    --> a  1

    print(1, foo2())    --> 1  a  b

    t = {foo2(), 1}     --> {'a', 1}

    t = {1, foo2()}     --> {1, 'a', 'b'}

    -- 阻止这种参数顺序搞事:

    print(1, (foo2())) -- 1 a 加一层括号,强制只返回一个值

    ```


4.1.4  真个性: 模式匹配


简洁的 Lua 容不下行数比自己实现语言行数还多的正则表达式实现(无论是 POSIX ,还是 Perl 正则表达式),于是乎有了独树一帜的模式与匹配,下面只用模式匹配来做 URL 解码、编码功能实现的演示。


    ```

    -- 解码

    function unescape(s)

      s = string.gsub(s, "+", " ")

      s = string.gsub(s, "%%(%x%x)", function (h)

            return string.char(tonumber(h, 16))

          end)

      return s  

    end

    print(unescape("a%2Bb+%3D+c")) ---> a+b =c

    cgi = {}

    function decode(s)

      for name,value in string.gmatch(s, "([^&=]+)=([^&=]+)") do

        name = unescape(name)

        value = unescape(value)

        cgi[name] = value

      end

    end

    -- 编码

    function escape(s)

      s = string.gsub(s, "[&=+%%%c]", function(c)

          return string.format("%%%02X", string.byte(c))

        end)

      s = string.gsub(s, " ", "+")

      return s

    end  

    function encode(t)

      local b = {}

      for k,v in pairs(t) do

        b[#b+1] = (escape(k) .. "=" .. escape(v))

      end

      return table.concat(b,'&')

    end

    ```

模式匹配实现的功能是足够强大,但是工程上是否值得投入还值得商榷,没有通用性,只此 lua 一家用。


虽然正则表达式也是不好调试,但是至少知道了解的人多,可能到最后笔者也不会多深入 lua 的模式匹配,但是如此单纯为了减少代码而放弃正则表达式现成的库,自己又玩了一套,这也是没谁了。


4.1.5  与 c 的天然亲密


一言不合,就拿 c 写一个库给 lua 用,由此可见两门语言是多么哥俩好了。如果举个例子的话就是 lua5.1 里面的位操作符,luajit 就是这样提供的解决方案  [Lua Bit Operations Module](http://bitop.luajit.org ),有兴趣的读者可以下载源码看一下,完全就是用 lua 的 c api 包装了 c 里面的位操作符出来用。


除了加了些限制的话(ex. 位移出来的必然是 32 位有符)。lua 除了数据结构上面的过于简洁外,其他的控制结构、操作符这些语言特性基本该有的都有了,唯独缺了位操作符。


5.1 为什么当时选择了不实现位操作符呢?有知道出处或者原因的读者欢迎留言告知。(顺带一提,lua5.2 里面有官方的 bit32 库可以用,lua5.3 已经补上了位操作符,另外 Openresty 在 lua 版本的选择上是选择停留在 5.1,这点在 github 的 Issue 里面有回答过,且没有升级的打算)。


4.1.6  杂


  • 只有 nil false 为布尔假。

  • lua 中的索引习惯以 1 开始。

  • 没有整型,所有数字都是浮点数。

  • 当函数的参数是单引号或者双引号的字符串或者 table 定义的时候,可以省略外面的 () , 所以 require "cookie" 并不是代表 require 是个关键字。

  •  table[index] 等价于 table  [index] 



5. 构建公司层面完整的 Openresty 生态


5.1  开发助手:成长中的 resty 命令


习惯了动态语言的解释器的立即反馈,哪怕是熟悉 lua 的同学,初入 Openresty 的时候似乎又想起了编译->执行->修改的无限循环的记忆,因为每次都需要修改配置文件、reload 、测试再如此重复个几次才能写对一段函数,resty 命令无疑期待,笔者也希望 resty 命令能够更加完善、易用。


另外提一个小遗憾,现在 resty 命令不能玩 Openresty 里面的 shared_dict 共享内存, 这可能跟目前 resty 使用的 nginx 配置的模板是固定有关吧。


5.2  环境:可能不再需要重新编译 Nginx


有过 Nginx 维护开发经验的同学可能都熟悉这么一个过程,因为多半会做业务的拆分,除了小公司外,基本都不会把一个 Nginx 的所有可选模块都编译进去,每次有新的 Nginx 相关的功能的增减,都免不了重新编译,重新部署上线。


Openresty 是基于 Nginx 的,如果是新增 Nginx 本身的功能,重新编译增加功能没什么好说的,如何优雅的更新 Nginx 服务进程,Nginx 有提供方案、各家也有各家的服务可靠性要求,具体怎么办这里就不赘述了。


5.3  发布部署

Openresty 本身的发布部署跟 Nginx 本身没有太大的不同,Openresty 本身的发布部署官方也推出了 linux 平台的预编译好的包,在这样的基础上构建环境就更加便捷。


环境之上,首先是 lua 脚本和 nginx 配置文件的发布,在版本管理之下,加上自动构建的发布平台,Openresty 的应用分分钟就可以上线了:)


这个流程本身无关 Openresty ,但是简而言之一句话,当重复性的东西自动化之后,我们才有精力去解决更有趣的问题,不是么?


5.4  第三方库的安装、管理


  • 以前:自己找个第三方库编译之后扔到 Openresty 的 lualib 目录,luajit 是否兼容、是否 lua5.1 兼容都得自己来测试一遍。

  • 之前:对于解决前一个问题,Openresty 是通过给出 lua 里面 Luarocks 的安装使用来解决的,但是这种方式不能解决上面所说的第二个问题,所以现在这种方式已经不推荐使用了,下面贴一下官网的说明,只做内容收集、展示用, 最新的具体说明参见[using luarocks](http://openresty.org/en/using-luarocks.html)。


```

wget http://luarocks.org/releases/luarocks-2.0.13.tar.gz

tar -xzvf luarocks-2.0.13.tar.gz

cd luarocks-2.0.13/

./configure --prefix=/usr/local/openresty/luajit \

    --with-lua=/usr/local/openresty/luajit/ \

    --lua-suffix=jit \

    --with-lua-include=/usr/local/openresty/luajit/include/luajit-2.1

make

sudo make install

```


安装第三方库示例:

sudo /usr/local/openresty/luajit/luarocks install md5。


  • 现在:Openresty 提供了解决这两个问题的完整方案,自己的包管理的规范和仓库[opm](https://opm.openresty.org/)。


详细的标准说明, 请移步:https://github.com/openresty/opm#readme, 这里就不多做介绍了。


关于第三方库的质量,Openresty 官网上也有了专门的[QA页面](http://qa.openresty.org/),至少保证了第三方库一些实现的下限,不像 python 里面安装某些第三方包,比如 aerospike 的,里面安装 python 客户端,每次要去网上拉个 c 的客户端下来之类的稀奇古怪的玩法,期待 opm 未来更加完善。



5.5  关于单元测试


关于自动化测试的话,就笔者的试用经验而言,感觉还不是特别的顺手,官方提供的 Test:Nginx 工具已经提供简洁强大的功能,但是如果作为 TDD 开发中的测试驱动的工具而言,笔者觉得报错信息的有效性上面可能是唯一让人有点觉得有点捉鸡的地方,尚不清楚是否是笔者用的有误——


一般 Test:Nginx 的报错多半无法帮助调试代码,还是要走调试、修改的老路子。但是 Test:Nginx 的真正价值笔者觉得是讲实例代码和测试完美的结合,由此养成了看每个 Openresty 相关的项目代码都必先阅读里面的 Test:Nginx 的测试,当然最多最丰富的还是 Openresty 本身的测试。


举个实际的例子:


在使用 Test:Nginx 之前,之前对于 Nginx 的日志输出,一切的测试依据,对于外面的运行环境跑的测试只能通过 http 的请求和返回来做测试的判断条件,这时候对于一些情况就束手无策了, 比如处理某种错误情况可能需要在 log 里面记录一下,这种测试就无法保证。


另外也有类似[lua-resty-test](https://github.com/membphis/lua-resty-test)这样通过提供组件的方式来进行,但是我们一旦接触的了 Test:Nginx 的测试方法之后,这些就显得相形见绌了,我们举个实际的例子。


    ```

    # vim:set ft= ts=4 sw=4 et fdm=marker:

    use Test::Nginx::Socket::Lua;

    #worker_connections(1014);

    #master_process_enabled(1);

    #log_level('warn');

    #repeat_each(2);

    plan tests => repeat_each() * (blocks() * 3 + 0);

    #no_diff();

    no_long_string();

    #master_on();

    #workers(2);

    run_tests();

    __DATA__

    === TEST 1: lpush & lpop

    --- http_config

        lua_shared_dict dogs 1m;

    --- config

        location = /test {

            content_by_lua_block {

                local dogs = ngx.shared.dogs

                local len, err = dogs:lpush("foo", "bar")

                if len then

                    ngx.say("push success")

                else

                    ngx.say("push err: ", err)

                end

                local val, err = dogs:llen("foo")

                ngx.say(val, " ", err)

                local val, err = dogs:lpop("foo")

                ngx.say(val, " ", err)

                local val, err = dogs:llen("foo")

                ngx.say(val, " ", err)

                local val, err = dogs:lpop("foo")

                ngx.say(val, " ", err)

            }

        }

    --- request

    GET /test

    --- response_body

    push success

    1 nil

    bar nil

    0 nil

    nil nil

    --- no_error_log

    [error]

    ```

以上是随便选取的lua-nginx-module的测试文件145-shdict-list.t中的一段做说明,测试文件分为3个部分:


  • __DATA__以上的部分编排测试如何运行,

  • __DATA__作为分隔符,

  • __DATA__以下的是各个测试的说明部分。


测试部分如果具体细分的话,


  • 一般由 ====TEST 1: name 开始到下一个测试的声明;

  • 然后是配置 nginx 配置的 http_config、config、... 的部分;

  • 接着是模拟请求的部分,基本就是 http 请求报文的设定,功能不限于这里的 request 部分;

  • 最后是输出部分,这时候不仅是 http 报文的 body 部分之类的 http 响应、还有 nginx 的日志的输出这样的测试条件。


对于这样清晰可读、还能顺带把使用例子写的清楚的单元测试的框架, pythoner 真的难道不羡慕么?



5.6  关于调试、性能调优


这一块笔者还没有深入研究过,所以这里就不多说了,这里就做一下相关知识的链接归纳,方便大家整理资料吧。lua 语言本身提供的调试就比较简洁、加上 Openresty 是嵌入 Nginx 内部的,这就更给排查工作带来了困难。


  • [官方的调试页面](http://openresty.org/en/debugging.html )

  • [官方的性能调优页面](http://openresty.org/en/profiling.html )

  • [通过systemtap探查在线的Nginx work进程](https://github.com/agentzh/nginx-systemtap-toolkit)

  • [额外的工具库stap++](https://github.com/agentzh/stapxx )

  • [工具火焰图Flame Graphs的介绍](http://dtrace.org/blogs/brendan/2011/12/16/flame-graphs/ )

  • [Linux Kernel Performance: Flame Graphs](http://dtrace.org/blogs/brendan/2012/03/17/linux-kernel-performance-flame-graphs/ )




作者toyld 岂安科技搬运代码负责人

主导岂安各处的挖坑工作,擅长挖坑于悄然不息,负责生命不息,挖坑不止。 



文章标签