OpenResty
# 概述
OpenResty 是一个基于 Nginx 和 Lua 的 Web 应用服务器 , 集成了 LuaJIT 编程环境 , 可以通过 Lua 语言来扩展 Nginx 的功能 , 在Nginx中实现简单业务 , 从而提高 性能 和 并发抗压能力 !!
了解Lua语法 : 传送门跳转
# 应用
# 安装
安装开发者库
yum install -y pcre-devel openssl-devel gcc --skip-broken
安装OpenResty仓库
在Linux系统中 添加 openresty
仓库
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
如果以上命令不存在 , 则运行以下命令 : (运行完后 , 重复上面命令)
yum install -y yum-utils
安装OpenResty
yum install -y openresty
安装opm工具 opm是OpenResty的一个管理工具 , 可以帮助我们安装一个第三方的Lua模块
yum install -y openresty-opm
# 配置
配置环境变量
进入 vim /etc/profile
文件 , 在最后行添加以下两行语句
export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH
NGINX_HOME:后面是OpenResty安装目录下的nginx的目录
生效配置
source /etc/profile
Nginx配置
进入 vim /usr/local/openresty/nginx/conf/nginx.conf
文件 , 更改配置 (官方注释比较多 , 可直接覆盖以下代码)
#user nobody;
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
server {
listen 8081;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
提示
开放进入的端口是 8081
模块代码 , 有些代码需要依赖 .
# lua模块 lua_package_path "/usr/local/openresty/lualib/?.lua;;"; # c模块 lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
# 启用
# 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop
访问 http://<Ip>:<端口>
即可
提示
防火墙/安全组 端口保持开放!
# 简单应用
在 openresty 根目录中的 nginx里配置 nginx.conf
文件 , 以下是简单实例 模拟 lua脚本响应 :
location /example {
# 以下是 lua代码 注释可能不一样
content_by_lua_block {
-- 异常打印 , ngx.var.args变量 包含请求URI中的查询参数
ngx.log(ngx.ERR, "query parameters: ", ngx.var.args)
-- 响应客户端
ngx.header.content_type = "application/json"
ngx.say('{"msg": "Hello, OpenResty!"}')
-- 检查异常并处理
local status = ngx.status
-- ngx.HTTP_BAD_REQUEST 400 以上的为错误码
if status >= ngx.HTTP_BAD_REQUEST then
ngx.exit(status)
end
}
}
访问 http://<Ip>:<端口>/example
即可
# 请求参数处理
OpenResty提供了各种API进行获取不同类型的请求参数 :
参数形式 | 参数示例 |
---|---|
路径占位符 | localhst/item/1001 |
请求头 | 请求头 Authorization : token |
Get请求参数 | localhst?id=1001 |
Post请求参数 | 表单 id = 1001 |
Json请求参数 | {"id":1001} |
路径占位符
# nginx正则配置 1. 正则表达式匹配获取
location ~ /item/(\d+) {
content_by_lua_file lua/item.lua
}
# lua脚本获取 2. 匹配参数存到 数组中
local id = ngx.var[1];
请求头
--- lua脚本 , 获取 请求头信息 , table类型
local headers = ngx.req.get_headers();
Get请求参数
-- lua脚本 , 获取 Get请求参数 , table类型
local getParams = ngx.req.get_uri_args();
Post请求参数
-- lua脚本 , 获取 Post请求参数 , table类型
local postParams = ngx.req.get_post_args();
Json请求参数
-- lua脚本 , 读取请求体
ngx.req.read_body();
-- lua脚本 , 获取 Jons请求参数 , string类型
local jsonBody = ngx.req.get_body_data();
# 请求发送
Ngxin提供的内部API进行发送HTTP请求
请求结构 : (捕获请求)
local resp = ngx.location.capture("/path",{
-- 请求方式
method = ngx.HTTP_GET,
-- get请求参数
args = {a=1,b=2},
-- post请求参数
body = "c=3&d=4"
})
--[[ resp 结构 , table类型 , 分别包含 状态码;响应头;响应体
resp : {
status,
header,
body
}
]]--
提示
/path 是不包含IP和端口 , 请求被内部nginx的server监听并处理 . 可通过反向代理实现即可!!!
# 反向代理
location /path{
proxy_pass http://<ip>:<port>;
}
封装发送请求
在 nginx 加载lua模块路径 , 创建 common.lua文件
/usr/local/openresty/lualib/common.lua
-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)
local resp = ngx.location.capture(path,{
method = ngx.HTTP_GET,
args = params,
})
-- 为空情况
if not resp then
-- 记录错误信息 , 返回404
ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)
ngx.exit(404)
end
return resp.body
end
-- 将方法导出
local _M = {
read_http = read_http
}
return _M
加载缘故
OpenResty的lua模块加载代码 (前面的nginx也配置有)
#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
应用示例 :
应用示例 点击展开
--[[
请求入口 : (反向代理)
/item/{id}
/item/stock/{id}
]]--
-- 导入 common函数库
local common = require('common');
local read_http = common.read_http;
-- 导入 cjson库
local cjson = require("cjson");
-- 获取参数
local id = ngx.var[1];
-- 查询缓存
local itemJSON = read_http("/item/" .. id, nil);
-- 查询库存
local stockJSON = read_http("/item/stock/" .. id, nil);
-- 反序列化 转table(map)
local stock = cjson.decode(stockJSON);
local item = cjson.decode(itemJSON);
-- 组合数据
item.stock = stock.stock;
item.sold = stock.sold;
-- 序列化 转json
ngx.say(cjson.encode(item));
反向代理配置
# 请求接受反向代理出口
location /item {
# 个人应用测试的 Mock远端测试
proxy_pass https://mock.apifox.cn/m1/2492136-0-default/item;
}
location ~ /api/item/(\d+) {
default_type application/json;
# 该响应结果由 lua/item.lua 文件决定
content_by_lua_file lua/item.lua;
# 捕获Lua错误并将其记录到OpenResty错误日志中
log_by_lua_block {
ngx.log(ngx.ERR, "Lua error: ", tostring(ngx.var.lua_error));
}
}
Nginx内部运作过程
外部请求(https:xxx.cn/api/item/{id}) -> lua脚本请求(/item) -> toncat接收
# JOSN转Table
OpenResty 提供 cjson的模块用来处理 JONS的序列化/反序列化 功能
官网 : https://github.com/openresty/lua-cjson/ (opens new window)
引入模块
local cjson = require("cjson");
序列化
local obj = {
name = "sans",
age = 20
}
-- 序列化 , string类型
local json = cjson.enchod(obj);
反序列化
local json = "{'name':'Sans', 'age':20}"
-- 反序列化 , table 类型
local obj = cjson.decode(json);
print(obj.name);
print(obj.age);
# Redis通信
OpenResty 提供 Redis模块 里面包含有很多API , 可直接使用
引入模块
-- 导入 redis模块
local reids = require("resty.redis");
--- 初始化Redis对象
local red = redis.new();
--- 超时时间
red:set_timeouts(1000, 1000, 1000);
封装 Redis
在 nginx 加载lua模块路径 , 创建 common.lua文件
/usr/local/openresty/lualib/common.lua
Redis封装 点击展开
-- 关闭redis连接的工具方法 , 其实是放入连接池
local function close_redis(red)
-- 连接空闲时间 , 单位ms
local pool_max_idle_time = 10000;
--连接池大小
local pool_size = 100;
-- 将red放入连接池中
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size);
if not ok then
ngx.log(ngx.ERR, "放入redis连接池失败: ", err);
end
end
-- 查询Redis (这三个参数都不陌生)
local function read_redis(ip, port, key)
-- 获取一个连接
local ok, err = red:connect(ip, port)
if not ok then
ngx.log(ngx.ERR, "连接redis失败 : ", err)
return nil
end
-- 认证 Redis 服务器密码
local authRes, err = red:auth("123123")
if not authRes then
ngx.say("认证失败 , 密码错误: ", err)
return
end
-- 选择第二个数据库
local selectRes, err = red:select(1)
if not selectRes then
ngx.say("无法选择该库连接 : ", err)
return
end
-- 查询redis
local resp, err = red:get(key)
-- 查询失败处理
if not resp then
ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
end
--得到的数据为空处理
if resp == ngx.null then
resp = nil
ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
end
close_redis(red)
return resp
end
Redis应用
Redis应用 点击展开
-- 导入 common模块
local common = require('common');
local read_http = common.read_http;
local read_redis = common.read_redis;
-- 导入 cjson模块
local cjson = require("cjson");
-- 封装查询参数
function read_data(key, path, params)
-- 查询redis
local resp = read_redis("127.0.0.1", 6379, key);
if not resp then
ngx.log(ngx.ERR, "redis未查到 , 尝试http请求");
resp = read_http(path, params);
end
return resp;
end
-- 获取参数
local id = ngx.var[1];
-- 查询缓存
local itemJSON = read_data("item:" .. id, "/item/" .. id, nil);
...
# Nginx本地缓存
OpenResty 为Nginx 提供了 共享词典(shard dict) 的功能 , 可在多个不同请求之间共享字典数据 , 从而实现 缓存/节流 等场景
启动共享字典
在 Nginx配置 中通过 lua_shared_dict
指令 创建共享字典
http {
# 创建 共享字典,也就是本地缓存 ,
# 字典名称 : item_cache , 大小150m(150MB)
lua_shared_dict item_cache 150m;
}
读写共享字典
在 Lua脚本 中通过 ngx.shared.<字典名称>
模块 读写共享字典
-- 获取本地 字典对象
local item_cache = ngx.shared.item_cache;
-- 存储 , 指定 key,value,过期时间(单位s)0代表永久
item_cache:set('key','value', 1000);
-- 读取
local val = item_cache.get('key');
提示
共享字典的缓存数据一定一定要设置过期时间 , 根据业务情况设置时长
共享内存池
在 Nginx配置 可将多个字典中的内存共享在一个字典中 , 通过 lua_shared_dict_zone
实现 将多个字典绑定在同一个共享内存池中
http {
lua_shared_dict dict1 10m;
lua_shared_dict dict2 20m;
lua_shared_dict my_pool 30m;
lua_shared_dict_zone my_pool {
dict1 10m;
dict2 20m;
}
}
计数器
在 Lua中 通过 incr
和decr
方法控制 增加/减少 指定键的值
# var +1
incr(var , 1);
# var -1
decr(var , 1);
直接上应用场景比较好理解 , 请求限流 , 指定限制流量1000次流量
http {
# 创建字典 req_count_dict
lua_shared_dict req_count_dict 10m;
server {
location /api {
# 设置该变量 $req_count 自增1
set $req_count $lua_shared_dict:req_count_dict:incr($uri, 1);
if ($req_count > 1000) {
# 返回 429状态码 表示限流
return 429;
}
...
}
}
}
关键代码
set $req_count $lua_shared_dict:req_count_dict:incr($uri, 1);
先定义了一个 $req_count
变量 , 然后使用 lua_shared_dict
指令 访问字典 req_count_dict
, 使用 incr
方法 对当前请求的$uri
对应的计数器加 1 , 并将结果赋值给 $req_count
变量
# Lua实现完整应用
通过 实现多级缓存 OpenResty
架构图 :
多层缓存 , 分别说明缓存层级 : (可以根据情况优化)
- OpenResty Nginx 字典 本地缓存
- Redis缓存
- JVM缓存 (集群需要依赖 负载均衡的 Hash分配策略)
笔记
以上实现目的是为了突破 Tomcat 接收压力瓶颈问题 , 从而选举优化方案!
Nginx配置
nginx.conf 点击展开
#user nobody;
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
# 共享字典,也就是本地缓存 , 名称叫做:item_cache , 大小150m
lua_shared_dict item_cache 150m;
server {
listen 8081;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
# 请求响应反向代理出口
# 以下我采用内网穿透测试 , 一般情况采用 集群形式
location /item {
proxy_pass http://bozhu.freehk.svipss.top;
}
location ~ /api/item/(\d+) {
default_type application/json;
# 该响应结果由 lua/item.lua 文件决定
content_by_lua_file lua/item.lua;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
Lua脚本
item.lua 点击展开
-- 导入 common模块
local common = require('common');
local read_http = common.read_http;
local read_redis = common.read_redis;
-- 导入 cjson模块
local cjson = require("cjson");
-- 导入 共享字典模块(本地缓存)
local item_cache = ngx.shared.item_cache;
-- 封装查询参数
function read_data(key, expire, path, params)
-- 先从字典查询本地缓存
local resp = item_cache:get(key);
if not resp then
ngx.log(ngx.ERR, "Nginx 缓存未查到 , 尝试 Redis");
-- redis查询
resp = read_redis("127.0.0.1", 6379, key);
if not resp then
ngx.log(ngx.ERR, "Redis 未查到 , 尝试 HTTP");
-- http请求
resp = read_http(path, params);
end
end
-- 写入本地缓存
item_cache:set(key, resp, expire);
return resp;
end
-- 获取参数
local id = ngx.var[1];
-- 查询缓存 (本地缓存30min)
local itemJSON = read_data("item:" .. id, 1800,"/item/" .. id, nil);
-- 查询库存 (本地缓存 1min)
local stockJSON = read_data("item:stock:" .. id, 60, "/item/stock/" .. id, nil);
-- 反序列化 转table(map)
local stock = cjson.decode(stockJSON);
local item = cjson.decode(itemJSON);
-- 组合数据
item.stock = stock.stock;
item.sold = stock.sold;
-- 序列化 转json
ngx.say(cjson.encode(item));
封装工具模块
common.lua 点击展开
-- 导入 redis模块
local redis = require("resty.redis");
--- 初始化Redis对象
local red = redis.new();
--- 超时时间
red:set_timeouts(1000, 1000, 1000);
-- 关闭redis连接的工具方法 , 其实是放入连接池
local function close_redis(red)
-- 连接空闲时间 , 单位ms
local pool_max_idle_time = 10000;
--连接池大小
local pool_size = 100;
-- 将red放入连接池中
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size);
if not ok then
ngx.log(ngx.ERR, "Redis 加入连接池失败: ", err);
end
end
-- 查询Redis (这三个参数都不陌生)
local function read_redis(ip, port, key)
-- 获取一个连接
local ok, err = red:connect(ip, port)
if not ok then
ngx.log(ngx.ERR, "Redis 连接失败 : ", err)
return nil
end
-- 认证 Redis 服务器密码
local authRes, err = red:auth("panzer")
if not authRes then
ngx.say("Redis 认证失败: ", err)
return
end
-- 选择第二个数据库
local selectRes, err = red:select(1)
if not selectRes then
ngx.say("Redis [1]库不存在", err)
return
end
-- 查询redis
local resp, err = red:get(key)
-- 查询失败处理
if not resp then
ngx.log(ngx.ERR, "Redis key不存在 , kye: ", key, err)
end
--得到的数据为空处理
if resp == ngx.null then
resp = nil
ngx.log(ngx.ERR, "Redis val为nil , key: ", key)
end
close_redis(red)
return resp
end
-- 封装函数 , 发送http请求 , 并解析响应
local function read_http(path, params)
local resp = ngx.location.capture(path,{
method = ngx.HTTP_GET,
args = params,
})
-- 为空情况
if not resp then
-- 记录错误信息 , 返回404
ngx.log(ngx.ERR, "HTTP 查不到 , path: ", path , " , args: ", args)
ngx.exit(404)
end
return resp.body
end
-- 将方法导出
local _M = {
read_http = read_http,
read_redis = read_redis
}
return _M