nginx_lua(3) - openapi项目实战

nginx和lua的配合让我们眼前一亮,高性能的带逻辑和数据库访问的nginx诞生了,感觉它天生就是为了 http 接口或者代理而生的。现在有这样一些需求:

1、将公司所有的api放在后方,让ngx_lua顶在最前面,作为 api 服务的前置入口,安全性要好

2、要能抗住压力,毕竟公司所有的 api 接口都将从ngx_lua这条总线走,不能有太长的响应

3、要可以扩展,如果一台服务器扛不住压力,可以很方便的进行水平扩展

4、要稳定,不能隔三差五的挂一次,不能影响公司的 api 服务

5、要好维护,出现问题能及时解决

对于以上5点,我们使用ngx_lua完全满足,其中可维护和稳定性是我们不使用node.js的主要原因。我们主要使用openresty来作为我们openapi项目的架构。
下面使用一些代码片段来简单介绍一下ngx_lua在实际使用中的特性:

1、ngx配置:
worker_processes 1;
error_log logs/error.log debug;
pid /usr/local/openresty/nginx/logs/nginx.pid;

#events配置
events {
use epoll;
worker_connections 1024;
}
http {

    #设置dns服务器
    resolver 10.1.1.1;

#设置lua缓存off,product应该开启
lua_code_cache off;

# 设置lua模块的require路径
lua_package_path 'lib/?.lua;lua/class/?.lua;lua/db/?.lua;;';

# 设置lua的c模块的require路径
lua_package_cpath 'lib/?.so;;';

#执行lua初始化
init_by_lua_file 'lua/init/init.lua';

#加载server配置
include vhost/server_debug.conf;

}
vhost.conf
server {

#侦听80端口
listen       80;
#定义使用www.xx.com访问
server_name  test.api6998.com;    
#反向代理的配置           
location /favicon.ico{
    echo 'favicon.ico';
}
location / {
    content_by_lua_file 'lua/access/access.lua';
    }    
location /lua_test {
    content_by_lua_file 'test/test.lua';
    }    

}
注意点:
1、dns服务器如果在内网的话需要制定,否则resty.http会无法发起请求
2、设定pid保存路径可以支持 nginx -s reload
3、lua_code_cache off; 是便于开发的,将lua缓存关闭,避免每次都重启ngx
4、require路径是指lua的require包,会依次去这些路径获取lua包,注意相对目录是当前ngx启动目录
5、特别注意,init_by_lua_file 是在nginx启动,让lua做一些初始化动作使用的,只可以使用一次,不能有多个init_by_lua_file。
6、这里我们设置了2个路径/作为api的通用路径,让lua去处理一些路由匹配的事情,/lua_test是用来执行整个openapi的测试代码的

2、启动nginx
nginx -p pwd/ -c conf/nginx_main_debug.conf;
在跟了参数-p之后,我们上面的ngx配置就可以使用相对路径了

3、lua连接和使用mysql
lua连接mysql也相对的简单,openresty封装了mysql的链接库供lua使用,使用C编写的,相当搞笑。
Mysql_CLass = {
host = “192.168.28.4”,
port = 3306,
database = “openapi”,
user = “root”,
password = “123456”,
max_packet_size = 1024 * 1024
}

function Mysql_CLass:connect()

local db, err = mysql:new()
if not db then
ngx.log(ngx.ERR, “mysql library error “ .. err) – 如果mysql模块出错,则记录错误日志
return ngx.HTTP_INTERNAL_SERVER_ERROR, nil, ERR_MYSQL_LIB
end

db:set_timeout(1000) – 1 sec

local ok, err, errno, sqlstate = db:connect{
host = self.host,
port = self.port,
database = self.database,
user = self.user,
password = self.password,
max_packet_size = self.max_packet_size
}

if not ok then
ngx.log(ngx.ERR, “mysql not connect: “ .. err .. “: “ .. errno .. “: “.. sqlstate .. “.”) –如果数据库连接错误则记录日志
return ngx.HTTP_INTERNAL_SERVER_ERROR, nil, ERR_MYSQL_DB
end

return ngx.HTTP_OK, db, nil

end

如果一切顺利,我们就可以使用返回的 db 类来操作mysql数据库了,一个简单的联表查询:
local res2, err, errno, sqlstate =
db:query(“SELECT name,apikey,ApiSecret from ApiUserRoleTag as a JOIN ApiUser as b ON a.ApiUserId = b.id where a.ServiceRoleId = “ .. api_service_table.serviceroleid )
if not res2 then
ngx.log(ngx.ERR, “bad result: “ .. err .. “: “ .. errno .. “: “.. sqlstate .. “.”) –出错记录错误日志
return ngx.HTTP_INTERNAL_SERVER_ERROR, nil, ERR_MYSQL_ERROR
end
直接在db:query()里写sql语句即可,当然我们也可以封装成ORM,不过既然ngx_lua被定义为接口,自然对数据库访问不会向做后台系统那么频繁,所以只需要简单的写一些sql语句就可以了

4、一些逻辑判断
local code, service_table, error_code = Mysql_CLass:query_api_service(db, ngx.var.uri)
if(code ~= ngx.HTTP_OK) then
return res:send({status=code, error_code = error_code})
end
local req = Filter:new(service_table)
local code, error_code = req:check_all()
if(code ~= ngx.HTTP_OK) then
return res:send({status=code, error_code = error_code})
end
比如上述代码就是根据mysql返回值和用户请求req的参数是否合法进行的不同的响应,代码不用细看,这样我们就可以让nginx拥有更加强大的逻辑判断,在openapi项目中,nginx更是获得了验证用户公钥和签名验证的能力,直接在nginx中判断比之前proxy到后端验证再响应高效很多。

5、根据需要进行http request
我们使用ngx_lua不仅要解决参数错误,签名有误和一些访问日志以及访问频率的限制,更需要将合法的请求转发到后端的应用服务器中,应用服务器可能在各个地方,拥有各种域名或ip,所以我们需要让lua具有发送http请求的能力,同时就算我们后端应用服务器更换ip或者down机了,也不用更改nginx配置和重启,只需要在mysql数据库或者redis数据库将相应的domain或ip变更即可,灵活度大大提高。
我们看一个利用resty.http模块简单的发送一个请求例子:
function Http_Class:send_request()
self.data_array = {}
local ok, code, headers, status, body = self.http_client:proxy_pass {
url = self.url, –例如 http://www.baidu.com
headers = self.header, –各种http请求头部,table格式
method = self.method, –POST,GET,PUT,DELETE
body = self.body, –KEY,VALUE
body_callback = function(data, …)
table.insert(self.data_array, data)
end
}
if(ngx.header[“Transfer-Encoding”]) then
ngx.header[“Transfer-Encoding”] = nil
end
if(ngx.header[“Connection”]) then
ngx.header[“Connection”] = nil
end
self.data = table.concat(self.data_array, “”)
return ok, code, headers, status, body
end
注意点:
1、为什么将 Transfer-Encoding 设置为 nil,这里我发现一个小坑,可能是我不会用,如果不设置他为nil,则在响应给客户端时会出现2个Transfer-Encoding和Connection
2、body_callback 这个回调函数肯能会被执行多次,这个回调表示当ngx接受到数据就会执行这个回调,所以如果不定义这个回调,resty.http库会自动调用ngx.print(data)直接响应给客户端了,为什么不用 str = str + data,虽然不清楚resty.http是否支持2进制的返回,这样str = str + data对2进制返回必然不友好。
即使返回 string 类型,使用table将其保存,并且最终利用 table.concat 将他们连接起来要比之前字符串直接拼接高效
3、另外真正的返回 http.status 是在 code 中