Openresty的开发闭环初探

图片描述

为何值得入手?

Nginx做为如今使用最普遍的高性能后端服务器,Openresty为之提供了动态预言的灵活,当性能与灵活走在了一块儿,无疑对于被以前陷于臃肿架构,苦于提高性能的工程师来讲是重大的利好消息,本文就是在这种背景下,将初入这一未知的领域以后的一些经验与你们分享一下,如有失言之处,欢迎指教。php

安装

如今除了能在 Download里面下载源码来本身编译安装,如今连预编译好的都有了, 安装也就分分钟的事了。html

hello world

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

location / {
    content_by_lua_file ../luablib/hello_world.lua;
}

/path/to/openresty/lualib/hello_world.lualinux

ngx.say("Hello World")

访问一下, Hello World~.nginx

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的感受呢:)git

开发语言Lua的大体介绍

环境搭建完毕以后,接下来就是各类试错了,关于Lua的介绍,网上的资料好比:Openresty最佳实践(版本比较多,这里就不放了)。 写的都会比较详细,本文就不在这里过多解释了,只展现部分基础的Lua的模样。下面对lua一些个性有趣的地方作一下分享,可能不会涉及到lua语言比较全面或者细节的一些部分,做为补充,读者能够翻阅官方的<<Programing in Lua>>。github

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

--[[ 
     多行注释
--]]

-- 变量赋值

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 ,作了适当的裁剪来作说明。正则表达式

Lua语言个性的一面

第一道墙: 打印table

做为lua里面惟一标准的数据结构, 直接打印竟然只有一个id状的东西,这里说这一点没有抱怨的意思,只是让读者作好倒腾的心理准备,毕竟倒腾一个简洁语言终归是有代价的,了解决定背后的缘由,有时候比现成的一步到位的现成方案这也是倒腾的另外一面好处吧,这里给出社区里面的讨论apache

举个例子: lua里面通常使用#table来获取table的长度,究其缘由,lua对于未定义的变量、table的键,老是返回nil,而不像python里面确定是抛出异常, 因此#来计算table长度的时候只会遍历到第一个值为nil的地方,毕竟他不能一直尝试下去,这时候就须要使用table.maxn的方式来获取了。vim

Good or Bad? 自动类型转换

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

>>> "a" + 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot concatenate 'str' and 'int' objects

可是Lua以为他能搞定。

> = "20" + 10
30

若是你以为Lua选择转换加号操做符的操做数成数字类型去进行求值显得难以想象的,下面这种状况下,这种转换又貌似是能够有点用的了,print("hello" .. 123),这时你不用手动去将全部参数手工转换成字符串类型。尚没有定论说这项特性就是一无可取,可是这种依赖语言自己不明显的特性的代码笔者是不但愿在项目里面去踩雷的。

多返回值

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 加一层括号,强制只返回一个值

真个性: 模式匹配

简洁的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的模式匹配,可是如此单纯为了减小代码而放弃正则表达式现成的库,本身又玩了一套,这也是没谁了。

与c的自然亲密

一言不合,就拿c写一个库给lua用,因而可知两门语言是多么哥两好了,若是举个例子的话就是lua5.1里面的位操做符,luajit就是这样提供的解决方案, Lua Bit Operations Module, 有兴趣的读者能够下载源码看一下,彻底就是用lua的c api包装了c里面的位操做符出来用,除了加了些限制的话(ex.位移出来的必然是32位有符)。lua除了数据结构上面的过于简洁外,其余的控制结构、操做符这些语言特性基本该有的都有了,惟独缺了位操做符,5.1为何当时选择了不实现位操做符呢?有知道出处或者缘由的读者欢迎留言告知。(顺带一提,lua5.2里面有官方的bit32库能够用,lua5.3已经补上了位操做符,另外Openresty在lua版本的选择上是选择停留在5.1,这点在github的Issue里面有回答过,且没有升级的打算)

  • 只有nilfalse为布尔假。

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

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

  • 当函数的参数是单引号或者双引号的字符串或者table定义的时候,能够省略外面的(), 因此require "cookie"并非表明require是个关键字。

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

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

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

习惯了动态语言的解释器的当即反馈,哪怕是熟悉lua的同窗,初入Openresty的时候彷佛又想起了编译->执行->修改的无限循环的记忆,由于每次都须要修改配置文件、reload、测试再如此重复个几回才能写对一段函数,resty命令无疑期待,笔者也但愿resty命令可以更加完善、易用。

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

环境:可能再也不须要从新编译Nginx

有过Nginx维护开发经验的同窗可能都熟悉这么一个过程,由于多半会作业务的拆分,除了小公司外,基本都不会把一个Nginx的全部可选模块都编译进去,每次有新的Nginx相关的功能的增减,都免不了从新编译,从新部署上线,Openresty是基于Nginx的,若是是新增Nginx自己的功能,从新编译增长功能没什么好说的,如何优雅的更新Nginx服务进程,Nginx有提供方案、各家也有各家的服务可靠性要求,具体怎么办这里就不赘述了。

发布部署

Openresty自己的发布部署跟Nginx自己没有太大的不一样,Openresty自己的发布部署官方也推出了linux平台的预编译好的包,在这样的基础上构建环境就更加便捷,环境之上,首先是lua脚本和nginx配置文件的发布,在版本管理之下,加上自动构建的发布平台,Openresty的应用分分钟就能够上线了:),这个流程自己无关Openresty,可是简而言之一句话,当重复性的东西自动化以后,咱们才有精力去解决更有趣的问题,不是么?

第三方库的安装、管理

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

  • 以前: 对于解决前一个问题,Openresty是经过给出lua里面Luarocks的安装使用来解决的,可是这种方式不能解决上面所说的第二个问题,因此如今这种方式已经不推荐使用了,下面贴一下官网的说明,只作内容收集、展现用, 最新的具体说明参见using luarocks

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://github.com/openresty/... 这里就很少作介绍了,关于第三方库的质量,Openresty官网上也有了专门的QA页面,至少保证了第三方库一些实现的下限,不像python里面安装某些第三方包,好比aerospike的, 里面安装python客户端,每次要去网上拉个c的客户端下来之类的稀奇古怪的玩法,期待opm将来更加完善。

关于单元测试

关于自动化测试的话,就笔者的试用经验而言,感受还不是特别的顺手,官方提供的Test:Nginx工具已经提供简洁强大的功能,可是若是做为TDD开发中的测试驱动的工具而言,笔者以为报错信息的有效性上面多是惟一让人有点以为有点捉鸡的地方,尚不清楚是不是笔者用的有误,通常Test:Nginx的报错多半没法帮助调试代码,仍是要走调试、修改的老路子。可是Test:Nginx的真正价值笔者以为是讲实例代码和测试完美的结合,由此养成了看每一个Openresty相关的项目代码都必先阅读里面的Test:Nginx的测试,固然最多最丰富的仍是Openresty自己的测试。

举个实际的例子,在使用Test:Nginx以前,以前对于Nginx的日志输出,一切的测试依据,对于外面的运行环境跑的测试只能经过http的请求和返回来作测试的判断条件,这时候对于一些状况就一筹莫展了, 好比处理某种错误状况可能须要在log里面记录一下,这种测试就没法保证,另外也有相似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真的难道不羡慕么?

关于调试、性能调优

这一块笔者尚未深刻研究过,因此,这里就很少说了,这里就作一下相关知识的连接概括,方便你们整理资料吧。

lua语言自己提供的调试就比较简洁、加上Openresty是嵌入Nginx内部的,这就更给排查工做带来了困难。

官方的调试页面

官方的性能调优页面

经过systemtap探查在线的Nginx work进程

额外的工具库stap++

工具火焰图Flame Graphs的介绍

Linux Kernel Performance: Flame Graphs

反爬虫

做者 toyld 岂安科技搬运代码负责人 主导各处的挖坑工做,擅长挖坑于悄然不息,负责生命不息,挖坑不止。

相关文章
相关标签/搜索