openresty最佳实践笔记

1、免去写多个location

worker_processes 1; #nginx worker 数量
error_log logs/error.log; #指定错误日志文件路径
events {
worker_connections 1024;
}
http {
server {
listen 80;

    # 在代码路径中使用nginx变量
    # 注意: nginx var 的变量一定要谨慎,否则将会带来非常大的风险
    location ~ ^/api/([-_a-zA-Z0-9/]+) {
        access_by_lua_file  /path/to/lua/access_check.lua;
        content_by_lua_file /path/to/lua/$1.lua;
    }
}

}

2、ndk的api有点用处
ndk.set_var.DIRECTIVE

syntax: res = ndk.set_var.DIRECTIVE_NAME

context: init_worker_by_lua, set_by_lua, rewrite_by_lua, access_by_lua, content_by_lua, header_filter_by_lua, body_filter_by_lua, log_by_lua, ngx.timer., balancer_by_lua, ssl_certificate_by_lua, ssl_session_fetch_by_lua, ssl_session_store_by_lua*

This mechanism allows calling other nginx C modules’ directives that are implemented by Nginx Devel Kit (NDK)’s set_var submodule’s ndk_set_var_value.

For example, the following set-misc-nginx-module directives can be invoked this way:

set_quote_sql_str
set_quote_pgsql_str
set_quote_json_str
set_unescape_uri
set_escape_uri
set_encode_base32
set_decode_base32
set_encode_base64
set_decode_base64
set_encode_hex
set_decode_hex
set_sha1
set_md5

3、lua的cjson encode数组还是字典
– 内容节选lua-cjson-2.1.0.2/tests/agentzh.t
=== TEST 1: empty tables as objects
— lua
local cjson = require “cjson”
print(cjson.encode({}))
print(cjson.encode({dogs = {}}))
— out
{}
{“dogs”:{}}

=== TEST 2: empty tables as arrays
— lua
local cjson = require “cjson”
cjson.encode_empty_table_as_object(false)
print(cjson.encode({}))
print(cjson.encode({dogs = {}}))
— out
[]
{“dogs”:[]}

4、openresty的多个执行阶段
这样我们就可以根据我们的需要,在不同的阶段直接完成大部分典型处理了。

set_by_lua: 流程分支处理判断变量初始化
rewrite_by_lua: 转发、重定向、缓存等功能(例如特定请求代理到外网)
access_by_lua: IP准入、接口权限等情况集中处理(例如配合iptable完成简单防火墙)
content_by_lua: 内容生成
header_filter_by_lua: 应答HTTP过滤处理(例如添加头部信息)
body_filter_by_lua: 应答BODY过滤处理(例如完成应答内容统一成大写)
log_by_lua: 回话完成后本地异步完成日志记录(日志可以记录在本地,还可以同步到其他机器)

5、,require()、loadstring()、loadfile()、dofile()、io.、os. 等等 API 是一定不能暴露给不被信任的 Lua 脚本的。

6、lua中的同步函数
官方有明确说明,Openresty的官方API绝对100% noblock,所以我们只能在她的外面寻找了。我这里大致归纳总结了一下,包含下面几种情况:

高CPU的调用(压缩、解压缩、加解密等)
高磁盘的调用(所有文件操作)
非Openresty提供的网络操作(luasocket等)
系统命令行调用(os.execute等)
这些都应该是我们尽量要避免的。理想丰满,现实骨感,谁能保证我们的应用中不使用这些类型的API?没人保证,我们能做的就是把他们的调用数量、频率降低再降低,如果还是不能满足我们需要,那么就考虑把他们封装成独立服务,对外提供TCP/HTTP级别的接口调用,这样我们的Openresty就可以同时享受异步编程的好处又能达到我们的目的。

7、不擅长的应用场景

前面的章节,我们是从它适合的场景出发,OpenResty不适合的场景又有哪些?以及我们在使用中如何规避这些问题呢?

这里官网并没有给出答案,我根据我们的应用场景给大家列举,并简单描述一下原因:

有长时间阻塞调用的过程
例如通过 Lua 完成系统命令行调用
使用阻塞的Lua API完成相应操作
单个请求处理逻辑复杂,尤其是需要和请求方多次交互的长连接场景
Nginx的内存池 pool 是每次新申请内存存放数据
所有的内存释放都是在请求退出的时候统一释放
如果单个请求处理过于复杂,将会有过多内存无法及时释放
内存占用高的处理
受制于Lua VM的最大使用内存 1G 的限制
这个限制是单个Lua VM,也就是单个Nginx worker
两个请求之间有交流的场景
例如你做个在线聊天,要完成两个用户之间信息的传递
当前OpenResty还不具备这个通讯能力(后面可能会有所完善)
与行业专用的组件对接
最好是 TCP 协议对接,不要是 API 方式对接,防止里面有阻塞 TCP 处理
由于OpenResty必须要使用非阻塞 API ,所以传统的阻塞 API ,我们是没法直接使用的
获取 TCP 协议,使用 cosocket 重写(重写后的效率还是很赞的)
每请求开启的 light thread 过多的场景
虽然已经是light thread,但它对系统资源的占用相对是比较大的

8、cosocket
可以看到,cosocket 是依赖 Lua 协程 + nginx 事件通知两个重要特性拼的。

从 0.9.9 版本开始,cosocket 对象是全双工的,也就是说,一个专门读取的 “light thread”,一个专门写入的 “light thread”,它们可以同时对同一个 cosocket 对象进行操作(两个 “light threads” 必须运行在同一个 Lua 环境中,原因见上)。但是你不能让两个 “light threads” 对同一个 cosocket 对象都进行读(或者写入、或者连接)操作,否则当调用 cosocket 对象时,你将得到一个类似 “socket busy reading” 的错误。

location /test {
resolver 114.114.114.114;

content_by_lua_block {
    local sock = ngx.socket.tcp()
    local ok, err = sock:connect("www.baidu.com", 80)
    if not ok then
        ngx.say("failed to connect to baidu: ", err)
        return
    end

    local req_data = "GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n"
    local bytes, err = sock:send(req_data)
    if err then
        ngx.say("failed to send to baidu: ", err)
        return
    end

    local data, err, partial = sock:receive()
    if err then
        ngx.say("failed to recieve to baidu: ", err)
        return
    end

    sock:close()
    ngx.say("successfully talk to baidu! response first line: ", data)
}

}

9、http的dns实现
server {
location = /dns {
content_by_lua_block {
local resolver = require “resty.dns.resolver”
local r, err = resolver:new{
nameservers = {“8.8.8.8”, {“8.8.4.4”, 53} },
retrans = 5, – 5 retransmissions on receive timeout
timeout = 2000, – 2 sec
}

        if not r then
            ngx.say("failed to instantiate the resolver: ", err)
            return
        end

        local answers, err = r:query("www.google.com")
        if not answers then
            ngx.say("failed to query the DNS server: ", err)
            return
        end

        if answers.errcode then
            ngx.say("server returned error code: ", answers.errcode,
                    ": ", answers.errstr)
        end

        for i, ans in ipairs(answers) do
            ngx.say(ans.name, " ", ans.address or ans.cname,
                    " type:", ans.type, " class:", ans.class,
                    " ttl:", ans.ttl)
        end
    }
}

}

10、并发锁的实现
怎么解决?自然的想法是发现缓存失效后,加一把锁来控制数据库的请求。具体的细节,春哥在lua-resty-lock的文档里面做了详细的说明,我就不重复了,请看这里。多说一句,lua-resty-lock库本身已经替你完成了wait for lock的过程,看代码的时候需要注意下这个细节。

11、正则调优
– 使用 ngx.re.* 完成,使用调优参数 “jo”
function check_hex_jo( str )
if “string” ~= type(str) then
return false
end

return ngx.re.find(str, "([^0-9^a-f^A-F])", "jo")

end

12、检查table的类型
例如我们接受用户的注册请求,注册接口示例请求 body 如下:

{
“username”:”myname”,
“age”:8,
“tel”:88888888,
“mobile_no”:13888888888,
“email”:”*@.com”,
“love_things”:[“football”, “music”]
}
这时候可以用一个简单的字段描述格式来表达限制关系,如下:

{
“username”:””,
“age”:0,
“tel”:0,
“mobile_no”:0,
“email”:””,
“love_things”:[]
}
对于有效字段描述格式,数据值是不敏感的,但是数据类型是敏感的,只要数据类型能匹配,就可以让我们轻松不少。

来看下面的参数校验代码以及基本的测试用例:

function check_args_template(args, template)
if type(args) ~= type(template) then
return false
elseif “table” ~= type(args) then
return true
end

for k,v in pairs(template) do
  if type(v) ~= type(args[k]) then
    return false
  elseif "table" == type(v) then
    if not check_args_template(args[k], v) then
      return false
    end
  end
end

return true

end

local args = {name=”myname”, tel=888888, age=18,
mobile_no=13888888888, love_things = [“football”, “music”]}

print(“valid check: “, check_args_template(args, {name=””, tel=0, love_things=[]}))
print(“unvalid check: “, check_args_template(args, {name=””, tel=0, love_things=[], email=””}))

13、TIME_WAIT
请求包:
POST /api/heartbeat.json HTTP/1.1

Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT)
Accept-Encoding: gzip, deflate
Accept: /
Connection: Keep-Alive
Content-Length: 0
应答包:
HTTP/1.1 200 OK
Date: Mon, 06 Jul 2015 09:35:34 GMT
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: close
Server: 360 Web server
Content-Encoding: gzip

这个请求包是http1.1的协议,也声明了Connection: Keep-Alive,为什么还会被nginx主动关闭呢? 问题出在User-Agent,nginx认为终端的浏览器版本太低,不支持keep alive,所以直接close了。

在我们应用的场景下,终端不是通过浏览器而是后台请求的, 而我们也没法控制终端的User-Agent,那有什么方法不让nginx主动去关闭连接呢? 可以用keepalive_disable这个参数来解决。这个参数并不是字面的意思,用来关闭keepalive, 而是用来定义哪些古代的浏览器不支持keepalive的,默认值是MSIE6。

keepalive_disable none;
修改为none,就是认为不再通过User-Agent中的浏览器信息,来决定是否keepalive。