OpenResty + Lua访问Redis,实现高并发访问时的毫秒级响应

2021-01-12 17:12:00 1176 技术小虫有点萌

什么是OpenResty

天下武功,为快不破。Nginx 的看家本领就是速度,Lua 的拿手好戏亦是速度,这两者的结合在速度上无疑有基因上的优势。最先将 Nginx,Lua 组合到一起的是 OpenResty,它有一个 ngx_lua 模块,将 Lua 嵌入到了 Nginx 里面。本教程从环境搭建到实战讲解,逐步向读者展示如何使用 Nginx+Lua 框架进行开发。

使用场景

先看一下官网的解释

在 Lua 中混合处理不同 Nginx 模块输出(proxy, drizzle, postgres, Redis, memcached 等)。 在请求真正到达上游服务之前,Lua 中处理复杂的准入控制和安全检查。 比较随意的控制应答头(通过 Lua)。 从外部存储中获取后端信息,并用这些信息来实时选择哪一个后端来完成业务访问。 在内容 handler 中随意编写复杂的 web 应用,同步编写异步访问后端数据库和其他存储。 在 rewrite 阶段,通过 Lua 完成非常复杂的处理。 在 Nginx 子查询、location 调用中,通过 Lua 实现高级缓存机制。 对外暴露强劲的 Lua 语言,允许使用各种 Nginx 模块,自由拼合没有任何限制。该模块的脚本有充分的灵活性,同时提供的性能水平与本地 C 语言程序无论是在 CPU 时间方面以及内存占用差距非常小。所有这些都要求 LuaJIT 2.x 是启用的。其他脚本语言实现通常很难满足这一性能水平。

不擅长的场景

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

我工作中遇到的使用场景

  • 场景一 接触lua,是因为我们的网站是一个内容类型的网站,使用了很多的缓存,比如cdn,nginx,redis用来缓解服务器的压力,但是与此同时也会有一些问题,比如需要动态展示的数据,不能实时展示了,比如点赞量,浏览量。只要不主动刷新缓存(而且有这么多层缓存~~),这些本来应该是动态的数据就不会变。那这么动态修改这些数据呢?第一个想到的肯定是通过ajax异步调取,但是从哪获取数据?你会用php、java等语言写一个接口给前端调用吗?如果这样的话就会造成资源浪费,这些非关键数据却需要消耗大量的服务器资源,高并发下更可能会拖累整个网站。这是第一个问题。
  • 第二个使用场景,还是以点赞和浏览量为栗子,用户每点一次赞或者 每浏览一次,或者每评论一次,都要发一次nginx请求去记录数据,如果我们也通过传统的方式,比如nginx通过9000端口调用php处理信息吗?有没有更好的方式呢?
  • 当然有,本文基于上述场景,使用 nginx+lua+redis 实现高性能的接口(前端页面效果略了,只对接口演示)

安装OpenResty

  • docker 拉取镜像 docker pull openresty/openresty
  • 创建容器,暴露8000端口(注意路径改成自己的)
docker run -d --name openresty -p 8000:80 -v /Users/zhangguofu/app/docker/openresty/conf.d:/etc/nginx/conf.d:Z -v /Users/zhangguofu/app/docker/openresty/data:/data openresty/openresty
  • 这里再重复一次用到的docker命令
  • -d 后台运行
  • --name xxx 容器的名字
  • -p 宿主机端口:容器暴露端口
  • -v 挂载宿主机目录:容器的目录
  • -z选项指明bind mount的内容在多个容器间是共享的
  • -Z选项指明bind mount的内容是私有不共享的

文件配置

  • 在/etc/nginx/conf.d 目录创建 nginx.conf文件,写入以下内容
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
# 包含lua配置文件
include /etc/nginx/conf.d/lua.conf;
  • 继续编写/etc/nginx/conf.d/lua.conf
server {
    listen       80;
    server_name  _;
 # 代理到lua脚本
    location /lua {
     default_type 'text/html';
     content_by_lua_file /etc/nginx/conf.d/conf/lua/test.lua;
 }
}
  • 编写/etc/nginx/conf.d/conf/lua/test.lua脚本测试一下吧
# 在文件中写入迁入的代码
ngx.say("hello world");

重启容器

# 重启 docker
docker restart openresty

# 查看 日志
docker logs -f openresty

在宿主机访问8000端口

image.png

修改业务逻辑

  • 修改 test.lua
ngx.header['Content-Type']="text/html; charset=utf-8";
local info=ngx.req.get_uri_args()["id"];
ngx.say(info);
return;
# 重启容器
zhangguofu@zhangguofudeMBP lua $ docker restart openresty
openresty
#查看输出日志
zhangguofu@zhangguofudeMBP lua $ docker logs -f openresty
2021/01/12 06:46:01 [warn] 1#1: conflicting server name "_" on 0.0.0.0:80, ignored
nginx: [warn] conflicting server name "_" on 0.0.0.0:80, ignored
172.17.0.1 - - [12/Jan/2021:06:46:18 +0000] "GET / HTTP/1.1" 200 1097 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36"
2021/01/12 06:46:18 [error] 6#6: *2 open() "/usr/local/openresty/nginx/html/favicon.ico" failed (2: No such file or directory), client: 172.17.0.1, server: _, request: "GET /favicon.ico HTTP/1.1", host: "127.0.0.1:8000", referrer: "http://127.0.0.1:8000/"
172.17.0.1 - - [12/Jan/2021:06:46:18 +0000] "GET /favicon.ico HTTP/1.1" 404 561 "http://127.0.0.1:8000/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36"
172.17.0.1 - - [12/Jan/2021:06:46:25 +0000] "GET /lua HTTP/1.1" 200 22 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36"
2021/01/12 06:51:41 [warn] 1#1: conflicting server name "_" on 0.0.0.0:80, ignored
  • 访问8000端口

lua访问redis

  • 此刻,我已经有一个redis服务在运行了
zhangguofu@localhost bin $  ./redis-cli  -h 192.168.9.195 -p 16379 --raw
192.168.9.195:16379>
192.168.9.195:16379>
192.168.9.195:16379> keys *
name
192.168.9.195:16379>

192.168.9.195:16379> set auds 200
OK
192.168.9.195:16379> get auds
200
  • 编写test.lua脚本访问redis
ngx.header['Content-Type']="text/html; charset=utf-8";
local info=ngx.req.get_uri_args()["id"];
local redis = require "resty.redis"
local red = redis:new()

local ok, err = red:connect("192.168.9.195", 16379)

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

ok, err = red:incr(info)
if not ok then
    ngx.say("failed to set dog: ", err)
    return
end

local views=red:get(info)

ngx.say("get result: ", views)
  • 访问8000端口
192.168.9.195:16379> get auds
205
  • 当然,也可以组装成json 返回
ngx.header['Content-Type']="text/html; charset=utf-8";
local info=ngx.req.get_uri_args()["id"];
local redis = require "resty.redis"
local red = redis:new()
-- 引入json 包
local cjson = require "cjson.safe";

local ok, err = red:connect("192.168.9.195", 16379)

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

ok, err = red:incr(info)
if not ok then
    ngx.say("failed to set dog: ", err)
    return
end

local views=red:get(info)
data_end1={}
data_end1['code']=200;
data_end1['views']=views;
ngx.say(cjson.encode(data_end1))
image.png

总结

这里面用到了lua语言,其实语言对于程序员来说,语言就是一条路而已,通过这条路到达我们的目的地,即实现我们的功能,但是条条大路通罗马,语言很多,能够精通一两个语言,甚至为某个语言开发几个包文件,我觉得就很了不起了。但是我们要不断拓宽自己的知识面,为什么?一个功能,实习生能实现,你也能实现,,甚至一个小白谷歌一下也能写出来,但是差距在哪里?你用了更高效的方式,更贴合需求的方案。

  • 此处小虫同学再叨叨几句,对于一个新的东西,不管是事物、技术、社会现象,如果你想了解它,那么最好是这样做
    • 它是一个什么
    • 为什么会出现
    • 出现带来了哪些影响(好的,坏的)
    • 怎么处理和使用
    • 同质化产品类比
    • 总结经验

顺带附赠历任领导教导我的话

  • 凡事清楚为什么这么做。任何事都要做到有理有据。
  • 你不用什么都精通,但是你要知道。知道很重要!
  • 做选择时,你不要选择目标太高的,那是好高骛远。也不要选择目标太低的,容易让人不思进取,你要选择虽然超出你现在的能力范围,但是通过努力你可以获得的!