当前位置:   article > 正文

Openresty+Lua+Redis实现高性能缓存_openresty lua redis

openresty lua redis

一、背景

当我们的程序需要提供较高的并发访问时,往往需要在程序中引入缓存技术,通常都是使用Redis作为缓存,但是要再更进一步提升性能的话,就需要尽可能的减少请求的链路长度,比如可以将访问Redis缓存从Tomcat服务器提前Nginx

原本访问缓存逻辑

User---> Nginx -> Tomcat -> Redis 

User---> Nginx -> Redis 

二、介绍

1 OpenResty 介绍

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

官网: OpenResty® - 开源官方站

2 Lua 介绍

Lua 是一个小巧的脚本语言。它是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo三人所组成的研究小组于1993年开发的。 其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。所以Lua不适合作为开发独立应用程序的语言。Lua 有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。

推荐教程: Lua 教程 | 菜鸟教程

三、软件安装

1 OpenResty 安装

下载最新版本

上传到虚拟机的/usr/local 目录下,之后解压

这里前提是需要安装c语言编译器和Nginx依赖包(如已经安装过了跳过下面3个命令),否则下面的安装会报错的

yum install -y gcc

yum install -y pcre pcre-devel

yum install -y zlib zlib-devel

yum install -y openssl openssl-devel

进入到解压后的文件夹  openresty-1.25.3.1 中执行

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

正常的话,出现下面的画面说明执行成功了

然后执行make && make install 

make 

make install 

执行完成后,可以看到在/usr/local 目录下多了一个openresty 目录

2 目录介绍

  1. bin目录:执行文件目录。
  2. lualib目录:这个目录存放的是OpenResty中使用的Lua库,主要分为ngx和resty两个子目录。
  3. nginx目录:这个目录存放的是OpenResty的nginx配置和可执行文件。
  4. luajit目录:luajit目录是LuaJIT的安装根目录,用于提供LuaJIT的运行环境和相关资源。

3 启动Nginx

 nginx/sbin/nginx -c /usr/local/openresty/nginx/conf/nginx.conf

4 访问Nginx

在浏览器中输入虚拟机的地址http://192.168.31.115/

四、Openresty中初试Lua

1 编辑nginx.conf

在server{}中插入下面代码

  1. location /lua {
  2. default_type text/html;
  3. content_by_lua '
  4. ngx.say("<p>hello,world</p>")
  5. ';
  6. }

2 重启一下Nginx

nginx/sbin/nginx -s stop

nginx/sbin/nginx -c /usr/local/openresty/nginx/conf/nginx.conf

3 访问浏览器

在浏览器中输入虚拟机的IP地址+lua

http://192.168.31.115/lua

正常的话应该可以看到下面的画面

 4 通过lua文件的方式

进入到Nginx目录,创建lua文件夹,并新建一个hello.lua文件

cd nginx

mkdir lua

vim lua/hello.lua

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

修改nginx.conf 文件

  1. location /lua {
  2. default_type text/html;
  3. content_by_lua_file lua/hello.lua;
  4. }

重启Nginx,再次刷新网站

5 Openresty连接Redis

参考官网文档:GitHub - openresty/lua-resty-redis: Lua redis client driver for the ngx_lua based on the cosocket API

在/usr/local/openresty/nginx/lua目录下,编辑一个redis.lua 文件,内容如下:

  1. local redis = require "resty.redis"
  2. local red = redis:new()
  3. red:set_timeouts(1000, 1000, 1000) -- 1 sec
  4. local ok, err = red:connect("192.168.31.114", 6579)
  5. if not ok then
  6. ngx.say("failed to connect: ", err)
  7. return
  8. end
  9. local res, err = red:auth("123456")
  10. if not res then
  11. ngx.say("failed to authenticate: ", err)
  12. return
  13. end
  14. ok, err = red:set("dog", "an animal")
  15. if not ok then
  16. ngx.say("failed to set dog: ", err)
  17. return
  18. end
  19. ngx.say("set result: ", ok)
  20. local res, err = red:get("dog")
  21. if not res then
  22. ngx.say("failed to get dog: ", err)
  23. return
  24. end
  25. if res == ngx.null then
  26. ngx.say("dog not found.")
  27. return
  28. end
  29. ngx.say("dog: ", res)

再修改nginx.conf 

  1. location /lua {
  2. default_type text/html;
  3. content_by_lua_file lua/redis.lua;
  4. }

访问浏览器

到这里,我们已经成功使用Nginx通过lua脚本访问到了Redis,但是这种写法仍然有一个巨大的问题,就是每次请求都会重新连接Redis,性能非常低下,我们测试一下这样写的接口性能

6 解决Redis重复连接问题

再修改nginx.conf 

require("my/cache").go()

五、真实案例

1 案例背景

应用程序中有一个接口/goods-center/getGoodsDetails 希望通过nginx先查询Redis缓存,缓存中没有就去应用服务中查询,然后把查询到结果缓存到Redis中

2 Nginx获取请求的参数

编辑conf/nginx.conf 中的server,添加下面配置

  1. location /goods-center/getGoodsDetails {
  2. default_type application/json;
  3. content_by_lua_file lua/item.lua;
  4. }

然后在/usr/local/openresty/nginx/lua 目录中创建item.lua,添加下面lua代码

  1. local args = ngx.req.get_uri_args()
  2. ngx.say(args["goodsId"])

重新加载nginx配置

sbin/nginx -s reload

浏览器演示

3 转发请求到后端服务

定义一些工具类,方便后续写代码时调用,在/usr/local/openresty/lualib/mylua/common.lua

在common.lua中添加http get工具方法

  1. local function http_get(path,params)
  2. local resp = ngx.location.capture(path,{
  3. method = ngx.HTTP_GET,
  4. args = params
  5. })
  6. if not resp then
  7. ngx.log(ngx.ERR)
  8. ngx.exit(404)
  9. end
  10. return resp.body
  11. end
  12. local _M = {
  13. http_get = http_get
  14. }
  15. return _M

编辑/usr/local/openresty/nginx/lua/item.lua 文件

  1. -- 导入common包
  2. local common = require('mylua.common')
  3. local http_get = common.http_get
  4. local args = ngx.req.get_uri_args()
  5. -- 查询商品信息
  6. local itemJson = http_get("/goods-center/getGoodsDetails",args)
  7. ngx.say(itemJson)

修改/usr/local/openresty/nginx/conf/nginx.conf 添加下面代码

  1. location /nginx-cache/goods-center/getGoodsDetails {
  2. default_type application/json;
  3. content_by_lua_file lua/item.lua;
  4. }
  5. location ~ ^/goods-center/ {
  6. proxy_pass http://192.168.31.112:9527;
  7. }

解释一下上面代码,将/nginx-cache/goods-center/getGoodsDetails 请求通过lua脚本处理,转发到/goods-center/getGoodsDetails ,再通过~ ^/goods-center/  反向代理到应用服务器上http://192.168.31.112:9527;

演示:

4 优先查询Redis (官方并发会报错)

官网文档:GitHub - openresty/lua-resty-redis: Lua redis client driver for the ngx_lua based on the cosocket API

这应该是我到目前为止最想吐槽的开源软件了,按照官网的文档操作简直就是个玩具,无法商用

只要超过1个线程去压测就会报bad request 错误,这个在官网的局限性一栏中有提到,但是不明白为什么不解决,这个问题不解决,就无法商用,而且每次请求都会创建连接,性能巨差,关键这个问题在网上都很少有人提出过这个问题,包括一些教学视频,都是点到为止,根本没有测试过并发场景能不能用,我只要一并发测试就GG,怎么改都不行,翻了很多文档,都没有解决方案,如果有人有方案可以在评论区分享一下,互相学习

GitHub - openresty/lua-resty-redis: Lua redis client driver for the ngx_lua based on the cosocket API

先看代码

/usr/local/openresty/lualib/mylua/common.lua

  1. local redis = require('resty.redis')
  2. local red = redis:new()
  3. red:set_timeouts(1000,1000,1000)
  4. local function get_from_redis(key)
  5. ngx.log(ngx.INFO,"redis init start .............")
  6. local ok,err = red:connect("192.168.31.114",6579)
  7. -- 连接失败
  8. if not ok then
  9. ngx.log(ngx.ERR,"connect redis error",err)
  10. return nil
  11. end
  12. -- 认证失败
  13. local res, err = red:auth("123456")
  14. if not res then
  15. ngx.say(ngx.ERR,"failed to authenticate: ", err)
  16. return nil
  17. end
  18. local resp,err = red:get(key)
  19. if not resp then
  20. ngx.log(ngx.ERR,"get from redis error ",err," key: ",key)
  21. return nil
  22. end
  23. -- 数据为空
  24. if resp == ngx.null then
  25. ngx.log(ngx.ERR,"this key is nil, key: ",key)
  26. return nil
  27. end
  28. -- 设置连接超时时间和连接池大小
  29. red:set_keepalive(600000, 100)
  30. return resp
  31. end
  32. local function http_get(path,params)
  33. local resp = ngx.location.capture(path,{
  34. method = ngx.HTTP_GET,
  35. args = params
  36. })
  37. if not resp then
  38. ngx.log(ngx.ERR)
  39. ngx.exit(404)
  40. end
  41. return resp.body
  42. end
  43. local _M = {
  44. http_get = http_get,
  45. get_from_redis = get_from_redis
  46. }
  47. return _M

/usr/local/openresty/nginx/lua/item.lua

  1. -- 导入common包
  2. local _M = {}
  3. common = require('mylua.common')
  4. http_get = common.http_get
  5. get_from_redis = common.get_from_redis
  6. function _M.get_data()
  7. local args = ngx.req.get_uri_args()
  8. -- 先查询Redis
  9. local itemJson = get_from_redis("goods-center:goodsInfo:" .. args["goodsId"])
  10. ngx.log(ngx.INFO,"get from redis itemJson, ",itemJson)
  11. if itemJson == nil then
  12. -- redis 没有,则查询服务器信息
  13. itemJson = http_get("/goods-center/getGoodsDetails",args)
  14. end
  15. ngx.say(itemJson)
  16. end
  17. return _M

修改/usr/local/openresty/nginx/conf/nginx.conf 添加下面代码

  1. server {
  2. location /nginx-cache/goods-center/getGoodsDetails {
  3. default_type application/json;
  4. content_by_lua_block {
  5. require("lua/item").get_data()
  6. }
  7. }
  8. location ~ ^/goods-center/ {
  9. proxy_pass http://192.168.31.112:9527;
  10. }

单线程测试(没有报错,且有500多的吞吐量)

2个线程测试,有40%+ 的错误率,报错详情截图给了,线程越多报错越多

  1. 2024/02/04 21:56:06 [error] 21662#0: *390686 lua entry thread aborted: runtime error: /usr/local/openresty/lualib/resty/redis.lua:166: bad request
  2. stack traceback:
  3. coroutine 0:
  4. [C]: in function 'connect'
  5. /usr/local/openresty/lualib/resty/redis.lua:166: in function 'connect'
  6. /usr/local/openresty/lualib/mylua/common.lua:9: in function 'get_from_redis'
  7. ./lua/item.lua:12: in function 'get_data'
  8. content_by_lua(nginx.conf:49):2: in main chunk, client: 192.168.31.32, server: localhost, request: "GET /nginx-cache/goods-center/getGoodsDetails?goodsId=10000 HTTP/1.1", host: "192.168.31.115"

5 使用ngx.shared.redis_pool连接池(并发不会报错)

/usr/local/openresty/lualib/mylua/common.lua

  1. -- 引入 lua-resty-redis 模块
  2. local redis = require "resty.redis"
  3. -- 获取 OpenResty 全局字典对象(连接池)
  4. local redis_pool = ngx.shared.redis_pool
  5. -- Redis 连接池的最大连接数
  6. local max_connections = 100
  7. -- Redis 服务器地址和端口
  8. local redis_host = "192.168.31.114"
  9. local redis_port = 6579
  10. -- 获取 Redis 连接
  11. local function get_redis_connection()
  12. local red = redis_pool:get(redis_host)
  13. if not red then
  14. ngx.log(ngx.ERR, "create new : ", err)
  15. -- 创建一个新的 Redis 连接
  16. red = redis:new()
  17. -- 设置连接超时时间
  18. red:set_timeout(1000,1000,1000)
  19. -- 连接 Redis 服务器
  20. local ok, err = red:connect(redis_host, redis_port)
  21. if not ok then
  22. ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
  23. return nil, err
  24. end
  25. local res, err = red:auth("123456")
  26. if not res then
  27. ngx.say(ngx.ERR,"failed to authenticate: ", err)
  28. return nil
  29. end
  30. -- 将连接放入连接池
  31. redis_pool:set(redis_host, red, 600)
  32. end
  33. return red
  34. end
  35. local function get_from_redis(key)
  36. local redis_conn, err = get_redis_connection()
  37. if not redis_conn then
  38. ngx.log(ngx.ERR, "Failed to get Redis connection: ", err)
  39. ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
  40. end
  41. -- 获取失败
  42. local resp,err = redis_conn:get(key)
  43. if not resp then
  44. ngx.log(ngx.ERR,"get from redis error ",err," key: ",key)
  45. return nil
  46. end
  47. -- 数据为空
  48. if resp == ngx.null then
  49. ngx.log(ngx.ERR,"this key is nil, key: ",key)
  50. return nil
  51. end
  52. -- 设置连接超时时间
  53. redis_conn:set_keepalive(600000, max_connections)
  54. return resp
  55. end
  56. local function http_get(path,params)
  57. local resp = ngx.location.capture(path,{
  58. method = ngx.HTTP_GET,
  59. args = params
  60. })
  61. if not resp then
  62. ngx.log(ngx.ERR)
  63. ngx.exit(404)
  64. end
  65. return resp.body
  66. end
  67. local _M = {
  68. http_get = http_get,
  69. get_from_redis = get_from_redis
  70. }
  71. return _M

/usr/local/openresty/nginx/lua/item.lua

  1. -- 导入common包
  2. local _M = {}
  3. common = require('mylua.common')
  4. http_get = common.http_get
  5. get_from_redis = common.get_from_redis
  6. function _M.get_data()
  7. local args = ngx.req.get_uri_args()
  8. -- 先查询Redis
  9. local itemJson = get_from_redis("goods-center:goodsInfo:" .. args["goodsId"])
  10. ngx.log(ngx.INFO,"get from redis itemJson, ",itemJson)
  11. if itemJson == nil then
  12. -- redis 没有,则查询服务器信息
  13. itemJson = http_get("/goods-center/getGoodsDetails",args)
  14. end
  15. ngx.say(itemJson)
  16. end
  17. return _M

修改/usr/local/openresty/nginx/conf/nginx.conf 添加下面代码

  1. lua_shared_dict redis_pool 100m;
  2. server {
  3. location /nginx-cache/goods-center/getGoodsDetails {
  4. default_type application/json;
  5. content_by_lua_block {
  6. require("lua/item").get_data()
  7. }
  8. }
  9. location ~ ^/goods-center/ {
  10. proxy_pass http://192.168.31.112:9527;
  11. }

6 压测详情

1 命中缓存压测

命中缓存的情况吞吐量1400多,并且采用本章第5小结这种线程池的方式连接不会报错,基本上可以商用了,唯一缺点是并发量没有达到预期,通过排查原因发现,大量的连接池并未真正生效,仍然有大量的创建连接,可能这是影响性能的主要因素,如果有同学解决了这个问题可以在评论区分享一下

2 未命中缓存压测

未命中缓存,会请求后端Tomcat服务器,Tomcat服务器会查询MySQL,这边的吞吐量测试数据为313,也不怎么高,排查了一下原因,仍然是Redis一直在不停的创建连接,这个问题目前还没有找到解决方案

六、总结

通过Nginx+lua 的方式,在Nginx这层就去查询Redis缓存,看起来的确是个非常棒的方案,但是缺点是操作起来特别麻烦,需要开发人员了解Nginx + Lua  还要了解Openresty 如何集成Nginx + Lua  + Redis,还要掌握在这种方式下,能够使用好Redis的连接池。最关键的是目前这种技术的文档并不完善,代码在某些地方不是特别的成熟,网上能找到的资料都很少而且都比较皮毛,不够深入,然而开发人员却需要深入地了解他们,才能比较好的驾驭这种方式,这次探究仍然有一个遗留问题就是Openresty + Lua + Redis 连接池的方式,连接池看起来有时候会不生效,也不是每次都不生效,有一定的概率,从而导致性能并不高,这个需要后面再研究解决它

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/代码探险家/article/detail/991796
推荐阅读
相关标签
  

闽ICP备14008679号