OpenResty是一个基于Nginx的可伸缩的web应用服务器,由国人章亦春发起,提供了很多高质量的第三方模块。web开发人员可以使用Lua脚本调用Nginx支持的各种C和Lua模块。其最突出的特点是能够快速构造出承受10K以上的并发连接响应的超高性能web应用系统,目前,360、阿里云、新浪、腾讯都是OpenResty的深度用户。
OpenResty应用场景:
- 高访问下的应用及官网的主页
如商城,咨询类的应用首页,会存在大量的请求,由于涉及到的内容比较多;所以可是私用预载入的形式,将主页的数据放置在redis中;使用OpenResty+redis实现首页,官网主页的高并发加载 - 商城类的秒杀功能
秒杀功能会存在短时间的请求洪峰,如果处理不当可能会造成down机的风险,可以结合OpenResty+redis实现秒杀的功能 - ip限流
互联网系统可能存在非法用户恶意暴力请求,导致正常的用户无法使用,可以通过OpenResty+redis实现ip的白名单机制,去拦截非法的用户ip - APP灰度升级发布
可以根据系统的数据及条件实现APP的灰度升级测试
OpenResty可以简单地理解成封装了Nginx,并且集成Lua脚本,开发人员只需要简单地使用已经提供好的Lua脚本,而不需要自己编写复杂的Lua脚本调用Nginx。本文所使用的Linux环境为Centos7。
1. 安装OpenResty
# 添加仓库地址
yum install yum-utils
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
# 执行安装
yum install openresty
# 默认已经安装好了Nginx,进入默认目录如下,可以看到Nginx的文件夹
cd /usr/local/openresty
2. 安装Lua
进入Lua官网地址可以看到具体的安装指南。
curl -R -O http://www.lua.org/ftp/lua-5.4.0.tar.gz
tar zxf lua-5.4.0.tar.gz
cd lua-5.4.0
make all test
# 出错时运行下面的指令安装Lua需要的依赖
yum install libtermcap-devel ncurses-devel libevent-devel readline-devel
3. 配置Nginx
# 默认已经安装好了Nginx,进入默认目录如下,可以看到Nginx的文件夹
cd /usr/local/openresty/nginx/conf
vi nginx.conf
可以看到Nginx虚拟机的端口为80,我们可以更改配置文件,修改Nginx虚拟机的访问端口。接着,使用/bin/systemctl start openresty.service
命令启动OpenResty服务。
4. 测试访问
在浏览器中访问:http://192.168.137.118:80看到下图内容,说明配置成功。注意,这里是Linux虚拟机的IP地址和Nginx服务器的端口号。
5. 访问Redis
lua-resty-redis 模块:文档
首先,打开OpenResty下的Nginx配置文件:
# 默认已经安装好了Nginx,进入默认目录如下,可以看到Nginx的文件夹
cd /usr/local/openresty/nginx/conf
vi nginx.conf
在nginx.conf中加入:
location /redis_test{
default_type text/html;
content_by_lua_file /usr/local/openresty/lualib/redis_test.lua;
}
这里,需要注意的是,笔者试过新建/root/lua
目录,并将脚本放在该目录下,发现访问之后是404,所以这里建议放在/usr/local/openresty/lualib/
这个目录下。在该目录下创建redis_test.lua
脚本的内容如下:
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("failed to connect: ", err)
return
end
ngx.say("set result: ", ok)
local res, err = red:get("dog")
if not res then
ngx.say("failed to get doy: ", err)
return
end
if res == ngx.null then
ngx.say("dog not found.")
return
end
ngx.say("dog: ", res)
使用/bin/systemctl restart openresty.service
指令重启OpenResty服务,并访问http://192.168.137.118/redis_test:
接着,我们进入redis容器中,并设置dog的值。
docker exec -it redis-test /bin/sh
redis-cli -p 6379
set dog my_friend
执行成功后,再次访问,可以看到已经可以成功访问到redis数据库中的数据。
6. 访问mysql
openresty的mysql模块:文档
在nginx.conf加入如下配置:
location /mysql_test {
default_type text/html;
content_by_lua_file /usr/local/openresty/lualib/mysql_test.lua;
}
/usr/local/openresty/lualib/mysql_test.lua
脚本内容如下,注意修改您mysql数据库宿主机的IP地址、数据库名、用户名和密码等信息:
local mysql = require "resty.mysql"
local db, err = mysql:new()
if not db then
ngx.say("failed to instantiate mysql: ", err)
return
end
db:set_timeout(1000)
local ok, err, errno, sqlstate = db:connect{
host = "127.0.0.1",
port = 3306,
database = "changgou_poster",
user = "root",
password="Jack@13163",
max_packet_size = 1024 * 1024
}
if not ok then
ngx.say("failed to connect: ", err, ": ", errno, " ", sqlstate)
return
end
ngx.say("connected to mysql.")
local res, err, errno, sqlstate = db:query("drop table if exists cats")
if not res then
ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
return
end
res, err, errno, sqlstate = db:query("create table cats " .. "(id int not null primary key auto_increment, "
.. "name varchar(30))")
if not res then
ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
return
end
ngx.say("table cats created.")
res, err, errno, sqlstate = db:query("insert into cats(name) " .. "values (\'Bob\'),(\'\'),(null)")
if not res then
ngx.say("bad request: ", err, ": ", errno, ": ", sqlstate, ".")
return
end
ngx.say(res.affected_rows, " rows inserted into table cats ", "(last insert id: ", res.insert_id, ")")
res, err, errno, sqlstate = db:query("select * from cats order by id asc", 10)
if not res then
ngx.say("bad result ", err, ": ", errno, ": ", sqlstate, ".")
return
end
local cjson = require "cjson"
ngx.say("result: ", cjson.encode(res))
local ok, err = db:set_keepalive(1000, 100)
if not ok then
ngx.say("failed to set keepalive: ", err)
return
end
本文中mysql数据库是直接安装在虚拟机里面的,读者也可以在虚拟机的docker容器中安装mysql容器。需要注意的是,由于mysql的密码安全校验机制的存在,MySQL的用户名和密码必须是即有大小写字母,又有数字和符号,因此,笔者为了不那么麻烦,就直接遵循它的规则,设置密码为Jack@13163
,大家可以自己定义一个好记的密码。关于mysql在centos7下的安装,请移步至CentOS7安装mysql8.0.12。
# 拉取mysql最新镜像
docker pull mysql
# 后台运行容器,并设置root账户密码为123456
docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql
# 设置mysql容器随虚拟机自动启动
docker update --restart=always mysql
# 进入容器,并创建changgou_poster数据库
docker exec -it mysql /bin/bash
# 登录mysql数据库
mysql -u root -p
# 创建数据库
create database changgou_poster;
使用/bin/systemctl restart openresty.service
指令重启OpenResty服务,并访问:http://192.168.137.118/mysql_test
lua 连接MySQL mysql.cant_connect: ***consider upgrading MySQL client, errno:1251, sql_state:08004,解决方案:地址。
接着,使用systemctl restart mysqld
命令重启mysql服务,可以看到此时已经可以正常创建数据表,并向其中插入了三条记录。
7. 广告信息缓存测试
假设现在有一个场景:
电商项目中的网站首页需要展示大量的广告,这些广告通常是变化频率较低的,我们可以通过OpenResty服务器的两级缓存来提高系统的并发能力,同时真的做到像外挂一样,不影响系统的原有的架构。
具体的流程如下:
这里我们首先在数据库中创建两个表,建表语句如下:
drop database if exists changgou_poster;
create database changgou_poster;
use changgou_poster;
-- 广告分类表
drop table if exists tb_content_category;
create table tb_content_category
(
id bigint(20) auto_increment primary key comment '类目ID',
name varchar(50) not null comment '分类名称'
);
insert into tb_content_category(name) values('轮播广告');
insert into tb_content_category(name) values('活动广告');
-- 广告表
drop table if exists tb_content;
create table tb_content
(
id int auto_increment primary key comment '广告编号',
category_id varchar(50) not null comment '内容类别编号',
title varchar(500) comment '内容标题',
url varchar(1000) comment '链接',
pic varchar(500) comment '图片路径',
status varchar(1) comment '状态:0无效,1有效',
sort_order int(11) comment '排序'
);
insert into tb_content(category_id, title, url, pic, status, sort_order) values(1, '肉松饼', 'http://www.baidu.com', 'http://192.168.137.118:8080/group1/M00/00/00/wKiJdl8rYp-Ab9ydAAA-Nvu01EY467.png', '1', 1);
insert into tb_content(category_id, title, url, pic, status, sort_order) values(1, '华为P30', 'http://www.baidu.com', 'http://192.168.137.118:8080/group1/M00/00/00/wKiJdl8rYp-Ab9ydAAA-Nvu01EY467.png', '1', 1);
-- 查询某一分类的广告
select url,pic from tb_content where status = '1' and category_id = 1 order by sort_order;
具体的poster_test.lua
脚本如下:
ngx.header.content_type="application/json;charset=utf-8"
local cjson = require("cjson")
local mysql = require("resty.mysql")
local uri_args = ngx.req.get_uri_args()
local id = uri_args["id"]
local db = mysql:new()
db:set_timeout(1000)
local props = {
host = "127.0.0.1",
port = 3306,
database = "changgou_poster",
user = "root",
password = "Jack@13163"
}
local res = db:connect(props)
local select_sql = "select url,pic from tb_content where status = '1' and category_id = "..id.." order by sort_order"
res = db:query(select_sql)
db:close()
local redis = require("resty.redis")
local red = redis:new()
red:set_timeout(2000)
local ip = "127.0.0.1"
local port = 6379
red:connect(ip,port)
red:set("content_"..id,cjson.encode(res))
red:close()
ngx.say("{flag:true}")
配置Nginx拦截,即在/usr/local/openresty/nginx/conf/nginx.conf
加入如下配置:
location /update_content {
default_type text/html;
content_by_lua_file /usr/local/openresty/lualib/poster_test.lua;
}
使用/bin/systemctl restart openresty.service
重新启动OpenResty服务器,或者进入到/usr/local/openresty/nginx/sbin
下,执行./nginx -s reload
。最后,访问http://192.168.137.118/update_content?id=1
。
同时,可以看到Mysql数据库中的数据已经保存到redis中。
上述过程实现了将数据库中的数据读取到redis中,下面我们进一步完善poster_test.lua
脚本,完成两级缓存。
ngx.header.content_type="application/json;charset=utf-8"
local uri_args = ngx.req.get_uri_args();
local id = uri_args["id"];
--先查询Nginx本地缓存模块:【dis_cache,需要添加配置信息:lua_shared_dict dis_cache 128m;】
local cache_ngx = ngx.shared.dis_cache;
--根据Id获取缓存数据
local contentCache = cache_ngx:get('content_cache_'..id);
--在Nginx本地缓存查询不到
if contentCache == "" or contentCache == nil then
local redis = require("resty.redis")
local red = redis:new()
red:set_timeout(2000)
red:connect("127.0.0.1",6379)
local redisContent = red:get("content_"..id)
--从Redis中查询是否存在
if ngx.null == redisContent then
local cjson = require("cjson")
local mysql = require("resty.mysql")
local db = mysql:new()
db:set_timeout(2000)
local props = {
host = "127.0.0.1",
port = 3306,
database = "changgou_poster",
user = "root",
password = "Jack@13163"
}
local res = db:connect(props);
local select_sql = "select url,pic from tb_content where status = '1' and category_id = "..id.." order by sort_order"
res = db:query(select_sql)
--数据序列化为json字符串
local jsonResult = cjson.encode(res)
--插入数据到Redis缓存
red:set("content_"..id,jsonResult)
ngx.say("MySQL: "..jsonResult)
db:close()
else
--插入数据到Nginx本地缓存,缓存过期时间为2分钟,可以根据实际情况调整
cache_ngx:set('content_cache_'..id,redisContent, 2*60)
ngx.say("Redis: "..redisContent)
end
red:close()
else
--从Nginx本地缓存找到数据后直接输出
ngx.say("Nginx: "..contentCache)
end
添加Nginx缓存模块
清空Redis中之前的缓存,三次访问http://192.168.137.118/update_content?id=1
依次输入如下结果:
- MySQL: [{“url”:“http://www.baidu.com”,“pic”:“http://192.168.137.118:8080/group1/M00/00/00/wKiJdl8rYp-Ab9ydAAA-Nvu01EY467.png”},{“url”:“http://www.baidu.com”,“pic”:“http://192.168.137.118:8080/group1/M00/00/00/wKiJdl8rYp-Ab9ydAAA-Nvu01EY467.png”}]
- Redis: [{“url”:“http://www.baidu.com”,“pic”:“http://192.168.137.118:8080/group1/M00/00/00/wKiJdl8rYp-Ab9ydAAA-Nvu01EY467.png”},{“url”:“http://www.baidu.com”,“pic”:“http://192.168.137.118:8080/group1/M00/00/00/wKiJdl8rYp-Ab9ydAAA-Nvu01EY467.png”}]
- Nginx: [{“url”:“http://www.baidu.com”,“pic”:“http://192.168.137.118:8080/group1/M00/00/00/wKiJdl8rYp-Ab9ydAAA-Nvu01EY467.png”},{“url”:“http://www.baidu.com”,“pic”:“http://192.168.137.118:8080/group1/M00/00/00/wKiJdl8rYp-Ab9ydAAA-Nvu01EY467.png”}]