用OpenResty搭建高性能服务端_openresty

相关链接:https://github.com/openresty/lua-nginx-module

OpenResty 简介

​OpenResty®​​ 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

OpenResty 基于 ​​Nginx​​​开发,可以简单认为是 ​​Nginx​​​ + ​​lua-nginx-module​​的组合版。

官网:https://openresty.org/cn/ 官方文档:https://github.com/openresty/lua-nginx-module#version

高性能服务端两个重要要素:需要支持缓存,语言层面要支持异步非堵塞。

缓存速度上,内存 > SSD > 机械磁盘;本机 > 网络 ; 进程内 > 进程间 。异步非阻塞指的是事件驱动方式(事件完成后再通知)。

OpenResty 包含的技术:

  • Nginx:不仅仅是负载均衡+反向代理等功能,Nginx c module开发成本高。
  • LuaJIT:OpenResty用的是 LuaJIT,LuaJIT 是主打性能的Lua。

​OpenResty​​​ 本质上是将 ​​LuaJIT​​​ 的虚拟机嵌入到 Nginx的worker中,所以效率特别高,在性能上, ​​OpenResty​​ 接近或超过 Nginx c module:

 

OpenResty已经颠覆了高性能服务端的开发模式。

​OpenResty​​与市面上其他语言对比:

  • node.js:第一门将异步非阻塞特性放入自己语言中的,前端同学可以快速切入。但是 node.js 用回调(callback)实现异步非阻塞,代码写起来比较麻烦。
  • Python:3.4之后加入了异步的支持,比如异步io和aiohttp;3.5引入了协程。缺点是版本跨度大,因为很多人还是使用2.7。
  • Golang:最近几年非常火。缺点:代码写法上需要使用go关键字;线上热调试不方便(​​SystemTap​​ 提供了有限的支持)。

Hello World

OpenResty安装

以 CentOS 为例:

  

mkdir /opt && cd /opt


# download openresty

wget https://openresty.org/download/openresty-1.13.6.2.tar.gz


tar zxvf openresty-1.13.6.2.tar.gz

cd openresty-1.13.6.2


# configure

./configure --prefix=/usr/local/openresty -j4


make -j4 && make install

其中 源码包可以到 https://openresty.org/cn/download.html 该页面获取。 ​​-j4​​​表示使用4核。 ​​configure​​那一步还可以指定各种参数:

./configure --prefix=/usr/local/openresty \
--with-luajit \
--without-http_redis2_module \
--with-http_iconv_module \
--with-http_postgres_module

使用 ​​./configure--help​​ 查看更多的选项。

其它系统环境上安装可以参考 https://openresty.org/cn/installation.html 。

其实安装 OpenResty 和安装 Nginx 是类似的,因为 OpenResty 是基于 Nginx 开发的。

如果已经安装了 Nginx,又想使用 OpenResty 的功能,可以参考 《Nginx编译安装Lua》

 

第一个程序

修改 ​​/usr/local/openresty/nginx/conf/nginx.conf​​:

worker_processes  1;

error_log logs/error.log;

events {
worker_connections 1024;
}

http {
server {
listen 8080;

location /hello {

default_type text/html;

content_by_lua '
ngx.say("<p>hello, world</p>")
';
}
}
}

把默认的 ​​80​​​端口改为 ​​8080​​​,新增 ​​/hello​​部分。

其中 ​​content_by_lua​​​便是 OpenResty 提供的指令,在官方文档可以搜索到​​https://github.com/openresty/lua-nginx-module​

现在我们启动OpenResty:

/usr/local/openresty/nginx/sbin/nginx

启动成功后,查看效果:

知识点: 1、 ​​content_by_lua​​​:返回的内容使用 lua 代码。 2、 ​​content_by_lua_file​​​:读取lua文件里的 lua 代码。 3、默认情况下,修改Lua代码,需要 reload OpenResty服务才会生效。可以修改 ​​lua_code_cache​​​为 ​​off​​,作用域: http, server, location, location if。请勿在生产环境里开启。

测试1:使用 ​​content_by_lua_file​

cd /usr/local/openresty

mkdir nginx/conf/lua

vim nginx/conf/lua/hello.lua

内容为:

ngx.say("<p>hello, lua world</p>")

然后修改 nginx.conf:

location /hello {
default_type text/html;

content_by_lua_file conf/lua/hello.lua;
}

重启 OpenResty:

./nginx/sbin/nginx -s reload

查看效果:

curl http://127.0.0.1:8080/hello

 

测试2:关闭 ​​lua_code_cache​​​: 根据 ​​lua_code_cache​​作用域,我们可以在server块加上:

 

lua_code_cache off;
location /hello {
default_type text/html;

content_by_lua_file conf/lua/hello.lua;
}

重启:

./nginx/sbin/nginx -s reload

提示说 ​​lua_code_cache​​​关闭后影响性能。我们再次修改 ​​nginx/conf/lua/hello.lua​​的代码,保存后就会生效,无需 reload server。

 

OpenResty 入门

这节使用 ngx_lua api完成一个小功能。

lua代码:
nginx/conf/lua/getrandomstring.lua

-- 实现随机字符串

local args = ngx.req.get_uri_args()
local salt = args.salt

if not salt then
ngx.exit(ngx.HTTP_BAD_REQUEST)
end

local str = ngx.md5(ngx.time() .. salt)
ngx.say(str)

修改 nginx.conf ,新增:

location /get_random_string {
content_by_lua_file conf/lua/getrandomstring.lua;
}

由于修改了 nginx.conf ,需要reload OpenResty 服务。然后,我们访问服务:

curl http://127.0.0.1:8080/get_random_string?salt=2

说明:

1、 ​​ngx.req.get_uri_args()​​用于获取URI请求参数。

2、 ​​ngx.HTTP_BAD_REQUEST​​为ngx常量,指的是400。代码里尽量使用常量。

3、 ​​ngx.time()​​用于获取时间戳,是带有缓存的。与Lua的日期库不同,不涉及系统调用。尽量使用Ngx给出的方法,以免发生性能问题。

4、 ​​ngx.md5()​​用于生成md5值。

5、如果代码里有语法错误,我们可以通过nginx 的 error.log里看到,默认文件是 ​​nginx/logs/error.log​​。

 

再次提醒大家,做 OpenResty 开发,lua-nginx-module 的文档是你的首选,Lua 语言的库都是同步阻塞的,用的时候要三思。也就是说,尽量使用 ngx_lua提供的api,而不是使用 Lua 本身的。例如 ​​ngx.sleep()​​与 lua提供的sleep,前者不会造成阻塞,后者是会阻塞的,详见:sleep · OpenResty最佳实践 。

 

ngx_lua API介绍

本节主要是带着大家简单的过一下常用的ngx_lua API。

ngx_lua 有60多个指令(Directive),140多个 API(截止到2019-3-26)。

指令 是 ngx_lua 提供给Nginx调用的方法,与 Nginx自带的 ​​location​​​、 ​​rewrite​​​等是一个级别的。指令有自己的作用域,例如: ​​content_by_lua_file​​​只能作用于 ​​location​​​和 ​​locationif​​里面:

API 是指ngx_lua基于lua代码实现的一系列方法或常量,遵循 lua的语法规则。只能在lua代码块或者lua文件里使用。

例如:

ngx.say("<p>hello, lua world</p>")

其中 ​​content_by_lua​​​是指令,作用于 ​​location​​​块; ​​ngx.say()​​是 ngx_lua 提供的API。

 

下面,我们使用 ngx_lua完成另外一个小功能:实现base64的解码并重新json编码输出。代码里会用到一些指令和API。

lua代码:
nginx/conf/lua/decode_info.lua

 

 

用OpenResty搭建高性能服务端_openresty_02

 

 

修改 nginx.conf ,新增:

location /decode_info {
content_by_lua_file conf/lua/decode_info.lua;
}

由于修改了 nginx.conf ,需要 reload OpenResty 服务。然后,我们访问服务

 

$ php -r "echo base64_encode('test');"

dGVzdA==

$ curl -XPOST -d "info=dGVzdA==" http://127.0.0.1:8080/decode_info

{
"user_agnet":"curl\/7.19.7",
"client_ip":"127.0.0.1",
"info":"test"
}

 说明:

1、 ​​require​​是 lua 里面引入其他库的关键字。这里引入的 cjson。

2、当我们要读取 http里的post数据的时候,就需要使用 ​​ngx.req.read_body()​​。该API同步读取客户端请求主体而不阻塞Nginx事件循环。

3、 ​​ngx.req.get_post_args()​​ 用于获取post请求数据。

4、 ​​ngx.var.remote_var​​​实际是获取的nginx里的变量 ​​remote_var​​。

也就是说, ​​ngx.var.xxx​​​实际是获取的nginx里的变量 ​​xxx​​。例如:

nginx变量详见:[Alphabetical index of variables}(http://nginx.org/en/docs/varindex.html)。 ngx_lua ​​ngx.var​​ API详见:ngx.var.VARIABLE。

5、 ​​ngx.req.get_headers()​​ 用于读取nginx的header参数。返回的是lua table。

6、 ​​ngx.decode_base64()​​​用于 base64字符串解码。对应的编码API是 ​​ngx.encode_base64()​​。

 

连接数据库

连接数据库我们需要使用到ngx_lua的第三方库:

  • lua-resty-redis library based on ngx_lua cosocket.
  • lua-resty-mysql library based on ngx_lua cosocket.

这两个库都是基于cosocket实现的,特点是异步非阻塞。代码风格是同步的写法。更多第三方库详见:See Also 。

连接 MySQL

lua代码:
nginx/conf/lua/test_mysql.lua

local mysql = require "resty.mysql"

local db, err = mysql:new()
if not db then
ngx.say("failed to instantiate mysq:", err)
return
end

db:set_timeout(1000) -- 1s

local ok, err, errcode, sqlstate = db:connect{
host = "127.0.0.1",
port = 3306,
database = "ngx_test",
user = "ngx_test",
password = "ngx_test",
charset = "utf8",
max_packet_size = 1024 * 1024
}

if not ok then
ngx.say("failed to connect:", err, ": ", errcode, " ", sqlstate)
return
end

-- insert
res, err, errcode, sqlstate = db:query("insert into cats(name)".." values ('lanlang'),('')")

if not res then
ngx.say("failed to insert data: ", err, ": ", errcode, " ", sqlstate)
return
end

ngx.say(res.affected_rows, " rows inserted into table cat", "last insert id: ", res.insert_id)
res, err, errcode, sqlstate = db:query("select * from cats ", 10)

if not res then
ngx.say("failed to select data: ", err, ": ", errcode, " ", sqlstate)
return
end

local cjson = require "cjson"
ngx.say("result: ", cjson.encode(res))

-- close connection
local ok, err = db:close()
if not ok then
ngx.say("failed to close: ", err)
return
end

配置访问路径:

test_mysql {
content_by_lua_file conf/lua/test_mysql.lua;
}

访问测试:

curl http://127.0.0.1:80/test_mysql

 

连接Redis

添加lua文件test_redis.lua

local redis = require "resty.redis"

local rds = redis.new()
rds:set_timeout(1000) -- 1s

local ok, err = rds:connect("127.0.0.1", 6379)if not ok then
ngx.say("failed to connect:", err) return
end
local res, err = rds:auth('Zxing.112233')
if not res then ngx.say("failed to auth: ", err)
returnend

ok, err = rds:set("name", "lanlang")
if not ok then
ngx.say("failed to set name:", err)
returnend

ngx.say("set result: ", ok)

res, err = rds:get("name")
if not res then
ngx.say("failed to get name: ", err)
return
end

if res == ngx.null then
ngx.say("name not found.")
return
end

ngx.say("name: ", res)

-- close the connection right away
local ok, err = rds:close()
if not ok then
ngx.say("failed to close: ", err)
return
end

添加配置:

test_redis {
content_by_lua_file conf/lua/test_redis.lua;
}

测试:

curl http://127.0.0.1/test_redis

 

 

 

 

 参考文档:

 

 

 

常见错误

1. lua entry thread aborted: runtime error: /opt/apps/openresty/lualib/resty/redis.lua:238: bad argument #1 to 'rawget' (table expected, got string)

解决方案:

ok,error = redis:set("fruit","apple") 此处应该为 : 号,若写成 . 则会报上述错误。若为逗号,set的参数应该为 set(self,"fruit","apple")