OpenResty+Redis:高性能缓存利器


OpenResty + redis

使用OpenResty中的Lua语言进行二次开发,实现Nginx接入层缓存的技术。

下图左边是常用的架构,http请求经过nginx负载均衡转发到tomcat,tomcat再从redis读取数据,整个链路过程是串行的,当tomcat挂掉或者tomcat线程数被消耗完,就无法正常返回数据。

下图右边是使用OpenResty的lua-resty-redis模块使nginx具备直接访问redis的能力,不占用tomcat线程,Tomcat暂时挂掉仍可正常处理请求,减少响应时长,提高系统并发能力。

图片

1.压缩功能

Redis的value值用json格式保存。

{
    length:xxx,
    content:yyy
}

content是压缩后的页面内容,length是content压缩前的大小,length字段是为了在读取redis时,根据length的大小来判断是否要解压缩content的数据。

使用lua-zlib库进行压缩。

图片

2.定时器定时请求功能

按下图第1和第2步定时执行,nginx lua定时器定时请求tomcat页面的url,返回的页面html保存在redis。

缓存有效期可设置长些,比如1个小时,可保证1个小时内tomcat挂掉,仍可使用缓存数据返回,缓存的定时更新时间可设置短些,比如1分钟,保证缓存快速更新

图片

浏览器打开页面:

  • nginx先从redis获取页面html
  • redis不存在数据时,从tomcat获取页面,同时更新redis
  • 返回页面HTML给浏览器

图片

Nginx的所有worker进程都可以处理前端请求转发到redis,只有nginx worker 0才运行定时任务定时更新redis,lua脚本中通过ngx.worker.id()获取worker进程编号。

图片

通过管理后台配置需要缓存的URL,可配置缓存URL、缓存有效期、定时更新时间,比如modify?url=index&&expire=3600000&&intervaltime=300000&sign=xxxx,sign的值是管理后台secretkey对modify?url=index&&expire=3600000&&intervaltime=300000签名运算得到的,nginx端用相同的secretkey对modify?url=index&&expire=3600000&&intervaltime=300000签名运算,得到的值与sign的值相同则鉴权通过,允许修改nginx的配置。

图片

实战

OpenResty 直连 Redis 实战

  • 创建 itemredis.lua 脚本,该脚本中实现 OpenResty 试图先去 Redis 中查询数据的逻辑:
mkdir /etc/lua/
vim  itemredis.lua
local args = ngx.req.get_uri_args()
local id = args["id"]
local redis = require "resty.redis"
local cache = redis:new()
local ok, err = cache:connect("127.0.0.1", 6379)
cache:select(10)
local item_model = cache:get("item_"..id)
if item_model == ngx.null or item_model == nil then
    local resp = ngx.location.capture("/item/get?id="..id)
    item_model = resp.body
end

ngx.say(item_model)
  • 修改 nginx.conf
location /luaitem/get {
    default_type "application/json";
    content_by_lua_file /etc/lua/itemredis.lua;
}
  • 重启 nginx
nginx -s reload

文件 /home/lixinlei/application/openresty/lualib/resty/redis.lua 实现了对 Redis 的一些通用操作,OpenResty 对 Redis 的操作就基于这个文件;

压测结果

  • TPS 达到 3300,比 Nginx 的 Shared Dic 方案要慢一点,多了个访问 Redis 服务器的网络时间;

设计考量

  • 热点数据当然是放的越前越好;
  • 但有些数据不是特别的热,还有些更新的操作,那么建议使用 OpenResty 直连 Redis 的方案,可以保证数据更新后,OpenResty 可以在 Redis 中命中数据的最新值;或者直接在 Tomcat 层面完成对 Redis 的操作;
  • 缓存推的越靠前,性能表现越好,占用分布式资源越多,更新越困难;
  • 根据跟新策略、数据热的程度、业务上对脏读的忍受程度;

Nginx 实现动态封禁IP

需求

为了封禁某些爬虫或者恶意用户对服务器的请求,我们需要建立一个动态的 IP 黑名单。对于黑名单中的 IP ,我们将拒绝提供服务。并且可以设置封禁失效时间

环境准备

  • linux version: centos7 / ubuntu 等
  • redis version: 6.2.14
  • nginx version: nginx-openresty

设计方案

实现 IP 黑名单的功能有很多途径:

1、在操作系统层面,配置 iptables,来拦截指定 IP 的网络请求。

  • 优点:简单直接,在服务器物理层面上进行拦截
  • 缺点:每次需要手动上服务器修改配置文件,操作繁琐且不灵活

2、在 Web 服务器层面,通过 Nginx 自身的 deny 选项或者 lua 插件配置 IP 黑名单。

  • 优点:可动态实现封禁 ip,通过设置封禁时间可以做到分布式封禁
  • 缺点:需要了解 Lua 脚本和 Nginx 配置,有一定的学习成本

3、在应用层面,在处理请求之前检查客户端的 IP 地址是否在黑名单中。

  • 优点:通过编写代码来实现,相对简单且易于维护。
  • 缺点:代码可能会变得冗长,而且在高并发情况下可能影响性能。

为了方便管理和共享黑名单,通过 nginx + lua + redis 的架构实现 IP 黑名单的功能

图片

配置 nginx.conf

在需要进行限制的 server 的 location 中添加如下配置:

location / {
    # 如果该location 下存在静态资源文件可以做一个判断      
    #if ($request_uri ~ .*\.(html|htm|jpg|js|css)) {
    # access_by_lua_file /usr/local/lua/access_limit.lua;   
    #}

    access_by_lua_file /usr/local/lua/access_limit.lua; # 加上了这条配置,则会根据 access_limit.lua 的规则进行限流
    alias /usr/local/web/;
    index  index.html index.htm;
}

配置 lua 脚本

/usr/local/lua/access_limit.lua
-- 可以实现自动将访问频次过高的IP地址加入黑名单封禁一段时间

--连接池超时回收毫秒
local pool_max_idle_time = 10000
--连接池大小
local pool_size = 100
--redis 连接超时时间
local redis_connection_timeout = 100
--redis host
local redis_host = "your redis host ip"
--redis port
local redis_port = "your redis port"
--redis auth
local redis_auth = "your redis authpassword";
--封禁IP时间(秒)
local ip_block_time= 120
--指定ip访问频率时间段(秒)
local ip_time_out = 1
--指定ip访问频率计数最大值(次)
local ip_max_count = 3


--  错误日志记录
local function errlog(msg, ex)
    ngx.log(ngx.ERR, msg, ex)
end

-- 释放连接池
local function close_redis(red)
    if not red then
        return
    end
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
    if not ok then
        ngx.say("redis connct err:",err)
        return red:close()
    end
end


--连接redis
local redis = require "resty.redis"
local client = redis:new()
local ok, err = client:connect(redis_host, redis_port)
-- 连接失败返回服务器错误
if not ok then
    close_redis(client)
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
--设置超时时间
client:set_timeout(redis_connection_timeout)

-- 优化验证密码操作 代表连接在连接池使用的次数,如果为0代表未使用,不为0代表复用 在只有为0时才进行密码校验
local connCount, err = client:get_reused_times()
-- 新建连接,需要认证密码
if  0 == connCount then
    local ok, err = client:auth(redis_auth)
    if not ok then
        errlog("failed to auth: ", err)
        return
    end
    --从连接池中获取连接,无需再次认证密码
elseif err then
    errlog("failed to get reused times: ", err)
    return
end

-- 获取请求ip
local function getIp()
    local clientIP = ngx.req.get_headers()["X-Real-IP"]
    if clientIP == nil then
        clientIP = ngx.req.get_headers()["x_forwarded_for"]
    end
    if clientIP == nil then
        clientIP = ngx.var.remote_addr
    end
    return clientIP
end

local cliendIp = getIp();

local incrKey = "limit:count:"..cliendIp
local blockKey = "limit:block:"..cliendIp

--查询ip是否被禁止访问,如果存在则返回403错误代码
local is_block,err = client:get(blockKey)
if tonumber(is_block) == 1 then
    ngx.exit(ngx.HTTP_FORBIDDEN)
    close_redis(client)
end

local ip_count, err = client:incr(incrKey)
if tonumber(ip_count) == 1 then
    client:expire(incrKey,ip_time_out)
end
--如果超过单位时间限制的访问次数,则添加限制访问标识,限制时间为ip_block_time
if tonumber(ip_count) > tonumber(ip_max_count) then
    client:set(blockKey,1)
    client:expire(blockKey,ip_block_time)
end

close_redis(client)

总结

以上,便是 Nginx+Lua+Redis 实现的 IP 黑名单功能,具有如下优点:

  • 配置简单轻量,对服务器性能影响小。
  • 多台服务器可以通过共享 Redis 实例共享黑名单。
  • 动态配置,可以手工或者通过某种自动化的方式设置 Redis 中的黑名单

扩展

1、IP 黑名单的应用场景

IP 黑名单在实际应用中具有广泛的应用场景,主要用于保护服务器和应用免受恶意攻击、爬虫或滥用行为的影响。下面列举几个常见的应用场景:

  • 防止恶意访问: 黑名单可以阻止那些试图通过暴力破解密码、SQL 注入、XSS 攻击等方式进行非法访问的 IP 地址。
  • 防止爬虫和数据滥用: 黑名单可以限制那些频繁访问网站并抓取大量数据的爬虫,以减轻服务器负载和保护数据安全。
  • 防止 DDOS 攻击: 黑名单可以封禁那些发起大规模DDoS攻击的IP地址,保护服务器的稳定性和安全性。
  • 限制访问频率: 黑名单可以限制某个IP在特定时间段内的访问次数,防止恶意用户进行暴力破解、刷票等行为。

2、高级功能和改进

除了基本的 IP 黑名单功能外,还可以进行一些高级功能和改进,以提升安全性和用户体验:

  • 异常检测和自动封禁: 通过分析访问日志和行为模式,可以实现异常检测功能,并自动将异常行为的 IP 地址封禁,提高安全性。
  • 白名单机制: 除了黑名单,还可以引入白名单机制,允许某些 IP 地址绕过黑名单限制,确保合法用户的正常访问。
  • 验证码验证: 对于频繁访问或异常行为的 IP,可以要求其进行验证码验证,以进一步防止恶意行为。
  • 数据统计和分析: 将黑名单相关的数据进行统计和分析,例如记录封禁 IP 的次数、持续时间等信息,以便后续优化和调整策略。

通过不断改进和优化 IP 黑名单功能,可以更好地保护服务器和应用的安全。

Nginx+Lua+Redis 实现灰度上线系统(了解)

一、灰度方案:

常见的灰度实现方案:

  1. 请求路由:通过请求中的标识(如用户ID、设备ID、请求头等)来决定是否将请求路由到灰度环境。可以使用反向代理(如Nginx、Envoy)或API网关(如Kong、Apigee)来实现路由规则。

  2. 权重控制:将流量按照一定的权重比例分配到不同的环境中。可以通过负载均衡器(如HAProxy、Kubernetes Ingress)或代理服务器(如Nginx、Envoy)来实现权重控制。

  3. 特性开关:通过在代码中嵌入特性开关(Feature Flag)来控制功能的开启与关闭。可以使用配置文件、数据库、键值存储或特性管理平台(如LaunchDarkly、Unleash)来管理特性开关。

  4. 分阶段发布:将功能的发布分为多个阶段,从内部测试到灰度环境再到全量发布。可以使用部署工具(如Jenkins、GitLab CI/CD)或云平台(如AWS、Azure)来支持分阶段发布。

  5. A/B测试:将流量分为多个不同版本的应用程序,比较它们的性能和用户反馈。可以使用A/B测试平台(如Optimizely、Google Optimize)来管理和监控A/B测试。

  6. 金丝雀发布:将新版本的应用程序逐步引入生产环境,仅将少量流量导向新版本,并根据其性能和稳定性逐步增加流量。可以使用部署工具、容器编排平台或云平台来实现金丝雀发布。

常用的灰度发布方案:

  1. 基于用户ID的灰度发布:基于用户ID来划分灰度用户或百分比灰度,例如根据用户ID的哈希值或随机数来决定用户是否路由到灰度环境。

  2. 基于IP地址的灰度发布:根据用户的IP地址来划分灰度用户,例如将某一范围的IP地址指定为灰度用户,将请求从这些IP地址路由到灰度环境。

  3. Cookie/Session的灰度发布:通过在用户的Cookie或会话中设置特定的标识来划分灰度用户。例如,将特定的Cookie或会话变量设置为灰度标识,将具有该标识的请求路由到灰度环境。

  4. 请求头的灰度发布:基于请求头中的特定标识来划分灰度用户。例如,根据请求头中的自定义标识或特定的HTTP Header来路由请求到灰度环境。

  5. 权重或百分比的灰度发布:将请求随机分配给不同的环境,可以通过给不同环境设置不同的权重或百分比来控制流量的分配。

  6. A/B测试:将流量分为多个不同版本的应用程序,在实验期间比较它们的性能和用户反馈,最终选择最佳版本进行全量发布。

二、nginx+lua+redis实现灰度

理论:

1、安装并配置 Nginx 和 Redis。确保 Nginx 启用 Lua 模块,并可以访问 Redis。

2、在 Nginx 配置中定义灰度规则。您可以使用 Lua 脚本来判断用户是否应该被路由到灰度环境。示例配置如下:

server {
    listen 80;
    server_name example.com;
    location / {
        access_by_lua_block {
            local redis = require "resty.redis"
            local red = redis:new()
            -- 连接到 Redis
            local ok, err = red:connect("redis_host", redis_port)
            if not ok then
                ngx.log(ngx.ERR, "failed to connect to Redis: ", err)
                ngx.exit(500)
            end
            -- 使用 Redis 根据用户 ID 判断是否路由到灰度环境
            local user_id = ngx.req.get_headers()["X-User-ID"]
            local is_gray = red:get("gray:" .. user_id)
            if is_gray == "1" then
                ngx.var.upstream = "gray_backend"
            end
        }
        proxy_pass http://backend;
    }
    location /gray {
        # 灰度环境的配置
        proxy_pass http://gray_backend;
    }
    location /admin {
        # 管理后台的配置
        proxy_pass http://admin_backend;
    }
}

在上面的示例中,我们连接到 Redis,并根据请求中的用户 ID 判断是否将请求路由到灰度环境。ngx.var.upstream 变量用于动态设置上游地址,从而实现灰度环境的路由。

3、在 Redis 中设置灰度用户。您可以在 Redis 中维护一个键值对,其中键是用户 ID,值表示是否是灰度用户(例如,1 表示是灰度用户,0 表示不是)。您可以使用 Redis 的 SET 和 GET 命令来操作这些值。

-- 设置用户为灰度用户
local ok, err = red:set("gray:" .. user_id, 1)
if not ok then
    ngx.log(ngx.ERR, "failed to set gray status for user: ", err)
    ngx.exit(500)
end

-- 设置用户为非灰度用户
local ok, err = red:set("gray:" .. user_id, 0)
if not ok then
    ngx.log(ngx.ERR, "failed to set gray status for user: ", err)
    ngx.exit(500)
end

通过在 Redis 中设置用户的灰度状态,您可以动态地控制用户是否应该被路由到灰度环境。

4、根据需要,配置其他路径或功能的灰度规则。可以根据需要在 Nginx 配置中添加其他路径或功能的灰度规则,以实现更复杂的灰度发布策略。

实践:

这里主要使用OpenResty

nginx+lua 实现灰度----主要使用OpenResty

1、根据post请求url参数匹配进行路由

nginx配置如下:

#user  nobody;
worker_processes  1;
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
#pid        logs/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    #log_format  main '$time_local 客户端地址:$remote_addr–$remote_port 请求的URI和HTTP协议:$request 请求地址:$http_host HTTP请求状态:$status upstream状态:$upstream_status 负载地址:$upstream_addr url跳转来源:$http_referer $body_bytes_sent $http_user_agent $request_uri';
    log_format  logFormat '$group $time_local 客户端:$remote_addr–$remote_port 请求的URI和HTTP协议:$request 请求:$http_host HTTP状态:$status upstream状态:$upstream_status 负载:$upstream_addr 
                          url跳转:$http_referer $body_bytes_sent $http_user_agent $request_uri 请求参数 $query_string $args $document_root $uri
                          -----$request_uri $request_filename $http_cookie';
    access_log  logs/access.log  logFormat;
    sendfile        on;
    #tcp_nopush     on;
    #keepalive_timeout  0;
    keepalive_timeout  65;
    server{
        listen       80;   #监听端口
        server_name  域名; #监听地址 
        access_log  logs/xx.com.access.log  logFormat;
              location /hello {
                default_type 'text/plain';
                content_by_lua 'ngx.say("hello ,lua scripts")';
        }
        location /myip {
                default_type 'text/plain';
                content_by_lua '
                        clientIP = ngx.req.get_headers()["x_forwarded_for"]
                        ngx.say("Forwarded_IP:",clientIP)
                        if clientIP == nli then
                                clientIP = ngx.var.remote_addr
                                ngx.say("Remote_IP:",clientIP)
                        end
                        ';
        }
        location / {
                default_type 'text/plain';
                lua_need_request_body on;
                #content_by_lua_file /etc/nginx/lua/dep.lua;
                #content_by_lua_file D:/sortware/openresty/openresty-1.17.8.2-win64/conf/dep.lua; # 指定由lua文件处理http请求
                content_by_lua_file D:/user/Downloads/openresty-1.19.9.1-win64/conf/dep.lua; # 指定由lua文件处理http请求
        }
        location @default_version {
            proxy_pass http://default; 
            proxy_set_header  Host  $http_host;
            proxy_set_header  X-Real-IP  $remote_addr;
            proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        }
        location @new_version {
            proxy_pass http://new_version;
            proxy_set_header Host $http_host;
            #proxy_redirect default;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
        location @old_version {
            proxy_pass http://old_version; 
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    
        }
    }
    #标准预发环境
    upstream default {
         server ip:port;
    }
    #预发2
    upstream new_version {
         server ip:port;
    }
    #预发3
    upstream old_version {
        server ip:port;
    }
}

lua脚本如下:

--get请求uri参数

function SaveTableContent(file, obj)
      local szType = type(obj);
      print(szType);
      if szType == "number" then
            file:write(obj);
      elseif szType == "string" then
            file:write(string.format("%q", obj));
      elseif szType == "table" then
            --把table的内容格式化写入文件
            --file:write("{\n");
            for i, v in pairs(obj) do
                  SaveTableContent(file, i);
                  file:write(":");
                  SaveTableContent(file, v);
                  file:write(",");
            end
            --file:write("}\n");
      else
            error("can't serialize a "..szType);
      end
end

function SaveTable(obj)

      local file = io.open("D:\\user\\Downloads\\openresty-1.19.9.1-win64\\logs\\parmas.txt", "a");
      assert(file);
      SaveTableContent(file,obj);
      file:close();
end

local request_method = ngx.var.request_method
local getargs = nil
local args = nil
local read_body = nil
local body_data = nil
local thirdPolicystatus = nil
if "GET" == request_method then
    args = ngx.req.get_uri_args()
elseif "POST"== request_method then
    getargs = ngx.req.get_uri_args()
    args      = ngx.req.get_post_args()
    read_body = ngx.req.read_body()
    body_data = ngx.req.get_body_data()
end
if getargs ~= nil then
    SaveTable(getargs);
    thirdPolicystatus= getargs["thirdPolicystatus"];
    if thirdPolicystatus ~= nil then
        SaveTable(thirdPolicystatus);
    end
end

if args ~= nil then
    SaveTable(args);
end

if read_body ~= nil then
    SaveTable(read_body);
end

if body_data ~= nil then
    SaveTable(body_data);
end

if getargs ~= nil then
    thirdPolicystatus = getargs["thirdPolicystatus"]
    if thirdPolicystatus ~= nil and thirdPolicystatus == "1" then
        SaveTable("new_version-getargs");
        ngx.exec('@new_version')
    elseif thirdPolicystatus ~= nil and thirdPolicystatus == "2" then
        SaveTable("old_version-getargs");
        ngx.exec('@old_version')
    else
        SaveTable("default_version-getargs");
        ngx.exec('@default_version')
    end
end

if args ~= nil then
    if type(args) == "table" then
        thirdPolicystatus = tostring(args["thirdPolicystatus"])
        if thirdPolicystatus ~= nil and thirdPolicystatus == 1 then
            SaveTable("new_version-args-table");
            ngx.exec('@new_version')
        elseif thirdPolicystatus ~= nil and thirdPolicystatus == 2 then
            SaveTable("old_version-args-table");
            ngx.exec('@old_version')
        else
            SaveTable("default_version-args-table");
            ngx.exec('@default_version')
        end
    elseif type(args) == "string" then
        local json = require("cjson")
        local jsonObj = json.decode(args)
            thirdPolicystatus = jsonObj['thirdPolicystatus']
        if thirdPolicystatus ~= nil and thirdPolicystatus == 1 then
            SaveTable("new_version-args-string");
            ngx.exec('@new_version')
        elseif thirdPolicystatus ~= nil and thirdPolicystatus == 2 then
            SaveTable("old_version-args-string");
            ngx.exec('@old_version')
        else
            SaveTable("default_version-args-string");
            ngx.exec('@default_version')
        end
    end
end
return

host如下:

127.0.0.1  域名

访问地址:

域名

菜单运营数据---保单数据,默认走default集群,保单状态承保成功走new_version集群,保单状态终止走old_version集群

2、根据请求参数或ip等进行匹配redis缓存数据进行路由,灵活性更高。

redis下载地址:https://github.com/tporadowski/redis/releases

nginx配置如下:

#user  nobody;
worker_processes  1;
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
#pid        logs/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    #log_format  main '$time_local 客户端地址:$remote_addr–$remote_port 请求的URI和HTTP协议:$request 请求地址:$http_host HTTP请求状态:$status upstream状态:$upstream_status 负载地址:$upstream_addr url跳转来源:$http_referer $body_bytes_sent $http_user_agent $request_uri';
    log_format  logFormat '$group $time_local 客户端:$remote_addr–$remote_port 请求的URI和HTTP协议:$request 请求:$http_host HTTP状态:$status upstream状态:$upstream_status 负载:$upstream_addr 
                          url跳转:$http_referer $body_bytes_sent $http_user_agent $request_uri 请求参数 $query_string $args $document_root $uri
                          -----$request_uri $request_filename $http_cookie';
    access_log  logs/access.log  logFormat;
    sendfile        on;
    #tcp_nopush     on;
    #keepalive_timeout  0;
    keepalive_timeout  65;
   server{
        listen       80;   #监听端口
        server_name  域名; #监听地址 
        access_log  logs/xx.com.access.log  logFormat;
              location /redis {
                default_type 'text/plain';
                content_by_lua 'ngx.say("hello ,lua scripts redis")';
        }
        location / {
                default_type 'text/plain';
                lua_need_request_body on;
                content_by_lua_file D:/user/Downloads/openresty-1.19.9.1-win64/conf/redis.lua; # 指定由lua文件处理http请求
        }
        location @pre-prd {
            proxy_pass http://pre-prd;
            proxy_set_header Host $http_host;
            #proxy_redirect default;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
        location @prd {
            proxy_pass http://prd; 
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    
        }
    }
    #预发2演示线上
    upstream prd {
         server ip:port;
    }
    #预发演示预发线上
    upstream pre-prd {
         server ip:port;
    }
}

lua脚本如下:

--get请求uri参数
function SaveTableContent(file, obj)
      local szType = type(obj);
      print(szType);
      if szType == "number" then
            file:write(obj);
      elseif szType == "string" then
            file:write(string.format("%q", obj));
      elseif szType == "table" then
            --把table的内容格式化写入文件
            --file:write("{\n");
            for i, v in pairs(obj) do
                  SaveTableContent(file, i);
                  file:write(":");
                  SaveTableContent(file, v);
                  file:write(",");
            end
            --file:write("}\n");
      else
            error("can't serialize a "..szType);
      end
end

function SaveTable(obj)
      --local file = io.open("D:\\user\\Downloads\\openresty-1.19.9.1-win64\\logs\\parmas.txt", "a");
      local file = io.open("D:\\user\\Downloads\\openresty-1.19.9.1-win64\\logs\\redis.txt", "a");
      assert(file);
      SaveTableContent(file,obj);
      file:close();
end


local request_method = ngx.var.request_method
local getargs = nil
local args = nil
local read_body = nil
local body_data = nil
local thirdPolicystatus = nil
if "GET" == request_method then
    args = ngx.req.get_uri_args()
elseif "POST"== request_method then
    getargs = ngx.req.get_uri_args()
    args      = ngx.req.get_post_args()
    read_body = ngx.req.read_body()
    body_data = ngx.req.get_body_data()
end

if getargs ~= nil then
    SaveTable("getargs");
    SaveTable(getargs);
    thirdPolicystatus= getargs["thirdPolicystatus"];
    if thirdPolicystatus ~= nil then
        SaveTable("thirdPolicystatus");
        SaveTable(thirdPolicystatus);
    end
end

if args ~= nil then
    SaveTable("args");
    SaveTable(args);
end

if read_body ~= nil then
    SaveTable("read_body");
    SaveTable(read_body);
end

if body_data ~= nil then
    SaveTable("body_data");
    SaveTable(body_data);
end


local redis = require "resty.redis"
local cache = redis.new()
cache:set_timeout(60000)

local ok, err = cache.connect(cache, '127.0.0.1', 6379)
if not ok then
    SaveTable("not ok");
    ngx.exec("@prd")
    return
end

local local_ip = ngx.req.get_headers()["X-Real-IP"]
if local_ip == nil then
        local_ip = ngx.req.get_headers()["x_forwarded_for"]
        SaveTable("local_ip1");
        if local_id ~= nil then
        SaveTable(local_id);
        end
end

if local_ip == nil then
    local_ip = ngx.var.remote_addr
    SaveTable("local_ip2");
    if local_id ~= nil then
    SaveTable(local_id);
    end
end

-- 在 redis 中根据客户端 ip 获取是否存在值
local res, err = cache:get(local_ip)
-- 如果存在则转发到 @pre-prd
if res == "1" then
        SaveTable(res);
        SaveTable("pre-prd");
        ngx.exec("@pre-prd")
        return
else
    SaveTable("-------");
    SaveTable(local_ip);
    SaveTable(res);
    cache:set(local_ip)
end

-- 如果不存在,则转发到 @prd
SaveTable("prd");
ngx.exec("@prd")

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

使用时这里根据redis缓里缓存的ip地址进行负载路由。

三、相关配置与语法

1、Nginx配置文件详解

源码:https://trac.nginx.org/nginx/browser

官网:http://www.nginx.org/

windows 安装包下载地址:https://nginx.org/en/download.html

nginx.conf

########### 每个指令必须有分号结束。#################
# 全局块  比如工作进程数,定义日志路径;
#配置用户或者组,默认为nobody nobody。
#user  nobody;
#user administrator administrators;

#允许生成的进程数,默认为1,一般建议设成CPU核数1-2倍
worker_processes 1;
#worker_processes  8;


#指定nginx进程运行文件存放地址
#pid /nginx/pid/nginx.pid;

#制定日志路径,级别。这个设置可以放入全局块,http块,server块,级别依次为:#debug|info|notice|warn|error|crit|alert|emerg
error_log logs/error.log error;
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#Events块 设置处理轮询事件模型,每个工作进程最大连接数及http层的keep-alive超时时间;
events {
    #使用epoll的I/O 模型处理轮询事件。
    #可以不设置,nginx会根据操作系统选择合适的模型
    #事件驱动模型,select|poll|kqueue|epoll|resig|/dev/poll|eventport
    #use epoll; 
    #工作进程的最大连接数量, 默认1024个
    worker_connections  2048;

    #设置网路连接序列化,防止惊群现象发生,默认为on
    accept_mutex on;  
    #设置一个进程是否同时接受多个网络连接,默认为off   
    multi_accept on;  
}

# http块 路由匹配、静态文件服务器、反向代理、负载均衡等
http {
    # 导入文件扩展名与文件类型映射表 mime.types
    include       mime.types;
    #默认文件类型,默认为text/plain
    default_type  application/octet-stream;
    #取消服务日志 
    #access_log off; 
    #日志格式及access日志路径 自定义格式
    log_format myFormat '$time_local 客户端地址:$remote_addr–$remote_port 请求的URI和HTTP协议:$request 请求地址:$http_host HTTP请求状态:$status upstream状态:$upstream_status 负载地址:$upstream_addr url跳转来源:$http_referer $upstream_addr $body_bytes_sent $http_user_agent';
    #combined为日志格式的默认值
    access_log logs/access.log myFormat;
    #允许sendfile方式传输文件,默认为off,可以在http块,server块,location块。
    sendfile on; 
    #sendfile开启时才开启。
    tcp_nopush   on; 
    server_names_hash_bucket_size 64; 
    #每个进程每次调用传输数量不能大于设定的值,默认为0,即不设上限。
    sendfile_max_chunk 100k;
    #连接超时时间,默认为75s,可以在http,server,location块。
    keepalive_timeout 65;

    #--------------------静态文件压缩-----------------------------#
    #Nginx可以对网站的css、js 、xml、html 文件在传输前进行压缩,大幅提高页面加载速度。经过Gzip压缩后页面大小可以变为原来的30%甚至更小。使用时仅需开启Gzip压缩功能即可。你可以在http全局块或server块增加这个配置。
    # 开启gzip压缩功能
    #gzip  on;
    gzip on;

    # 设置允许压缩的页面最小字节数; 这里表示如果文件小于10k,压缩没有意义.
    gzip_min_length 10k; 

    # 设置压缩比率,最小为1,处理速度快,传输速度慢;
    # 9为最大压缩比,处理速度慢,传输速度快; 推荐6
    gzip_comp_level 6; 

    # 设置压缩缓冲区大小,此处设置为16个8K内存作为压缩结果缓冲
    gzip_buffers 16 8k; 

    # 设置哪些文件需要压缩,一般文本,css和js建议压缩。图片视需要要锁。
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 
    #--------------------静态文件压缩-----------------------------#
    server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
    #http server块
    server {
        keepalive_requests 120; #单连接请求上限次数。
        listen       8081;   #监听端口
        server_name  域名 #监听地址
        #ssi on;
        #autoindex on;
        charset utf-8;
        client_max_body_size 10M; # 限制用户上传文件大小,默认1M
        #access_log  logs/host.access.log  myFormat; #定义访问日志,可以针对每一个server(即每一个站点)设置它们自己的访问日志。

        # 转发动态请求到web应用服务器
        #location ^~ /api {
            #rewrite ^/api/(.*)$ /$1 break;
            #proxy_pass https://stream;
            #break;#终止匹配
        #}
        location / {
           # 使用proxy_pass转发请求到通过upstream定义的一组应用服务器
            proxy_pass      http://stream ;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_redirect off;
            proxy_set_header X-Real-IP  $remote_addr;
        }
        location  ~*^.+$ {       #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。  
            proxy_pass      http://stream ;  #请求转向stream 定义的服务器列表          
        } 

        #location / {
            #autoindex on;
            #try_files $uri $uri/ /index.html?$args;
        #}

        # 规则1:通用匹配
        #location / {
            #ssi on;
            #autoindex on;                 #自动显示目录
            #autoindex_exact_size off;     #人性化方式显示文件大小否则以byte显示
            #autoindex_localtime on;       #按服务器时间显示,否则以gmt时间显示
            #root   /root;                 #定义服务器的默认网站根目录位置 
            #index index.html index.htm;   #定义首页索引文件的名称  设置默认页
            # 使用proxy_pass转发请求到通过upstream定义的一组应用服务器
            #proxy_pass   http://mysvr;      #负载配置
            #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            #proxy_set_header Host $http_host;
            #proxy_redirect off;
            #proxy_set_header X-Real-IP  $remote_addr; 
            #deny ip;  # 拒绝的ip
            #allow ip; # 允许的ip 
        #}

        # 规则2:处理以/static/开头的url
        location ^~ /static {                         
            alias /usr/share/nginx/html/static; # 静态资源路径
        }
        #= 精确匹配 1
        #^~ 以某个字符串开头 2
        #~ 区分大小写的正则匹配 3
        #~* 不区分大小写的正则匹配 4
        #!~ 区分大小写的不匹配正则 5
        #!~* 不区分大小写的不匹配正则 6
        #/ 通用匹配,任何请求都会匹配到 7
        #location  ~*^.+$ {       #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。  
            #root path;  #根目录
            #index vv.txt;  #设置默认页
            #proxy_pass  http://stream;  #请求转向stream 定义的服务器列表
            #deny 127.0.0.1;  #拒绝的ip
            #allow ip; #允许的ip           
        #} 
        #-----------------------------静态文件缓存--------------------#
        #缓存可以加快下次静态文件加载速度。我们很多与网站样式相关的文件比如css和js文件一般不怎么变化,缓存有效器可以通过expires选项设置得长一些。
        # 使用expires选项开启静态文件缓存,10天有效
        location ~ ^/(images|javascript|js|css|flash|media|static)/  {
             root    /var/www/big.server.com/static_files;
            expires 10d;
        }
        #-----------------------------静态文件缓存--------------------#
        # 错误页面
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

    #-------------$符号的全局变量含义--------------#
    #$args, 请求中的参数;
    #$content_length, HTTP请求信息里的"Content-Length";
    #$content_type, 请求信息里的"Content-Type";
    #$document_root, 针对当前请求的根路径设置值;
    #$document_uri, 与$uri相同;
    #$host, 请求信息中的"Host",如果请求中没有Host行,则等于设置的服务器名;
    #$limit_rate, 对连接速率的限制;
    #$request_method, 请求的方法,比如"GET"、"POST"等;
    #$remote_addr, 客户端地址;
    #$remote_port, 客户端端口号;
    #$remote_user, 客户端用户名,认证用;
    #$request_filename, 当前请求的文件路径名
    #$request_body_file,当前请求的文件
    #$request_uri, 请求的URI,带查询字符串;
    #$query_string, 与$args相同;
    #$scheme, 所用的协议,比如http或者是https,比如rewrite ^(.+)$
    #$scheme://example.com$1 redirect;        
    #$server_protocol, 请求的协议版本,"HTTP/1.0"或"HTTP/1.1";
    #$server_addr, 服务器地址;
    #$server_name, 请求到达的服务器名;
    #$server_port, 请求到达的服务器端口号;
    #$uri, 请求的URI,可能和最初的值有不同,比如经过重定向之类的。
    #-------------$符号的全局变量含义--------------#


    #错误页面
    #error_page 404 https://www.baidu.com; #错误页
    #error_page 404 500 502 503 504 403 /error.shtml;

    # 负载均衡
    upstream insurance-pre {   
      #weigth参数表示权值,权值越高被分配到的几率越大
      #--------------------负载均衡方式------------------#
      #1.轮询(默认)
      #2.权重,weight越大,承担任务越多
      #server ip:port weight=5
      #3.ip_hash   
      #ip_hash;
      #4.url_hash
      #hash $request_uri;
      #5. fair(第三方)--按后端服务器的响应时间来分配请求,响应时间短的优先分配。使用这个算法需要安装nginx-upstream-fair这个库。
      #fair;
      #--------------------负载均衡方式------------------#
      server ip:port   weight=5; # weight越高,权重越大
      server ip:port weight=1;
      server ip:port  weight=1;
      server ip:port backup; # 热备
    }
    # 转发动态请求
    #server {  
        #listen 80;                                                         
        #server_name  localhost;                                               
        #client_max_body_size 1024M;
        #location / {
            #proxy_pass http://localhost:8080;   
            #proxy_set_header Host $host:$server_port;
        #}
    #} 
    # http请求重定向到https请求
    #server {
        #listen 80;
        #server_name 域名;
        #return 301 https://$server_name$request_uri;
    #}
    server {
        keepalive_requests 120; #单连接请求上限次数。
        listen       80;   #监听端口
        server_name  域名 #监听地址
        #ssi on;
        #autoindex on;
        charset utf-8;
        client_max_body_size 10M; # 限制用户上传文件大小,默认1M
        #access_log  logs/host.access.log  myFormat; #定义访问日志,可以针对每一个server(即每一个站点)设置它们自己的访问日志。
        # 转发动态请求到web应用服务器
        #location ^~ /api {
            #rewrite ^/api/(.*)$ /$1 break;
            #proxy_pass https://域名;
            #break;#终止匹配
        #}
        location / {
           # 使用proxy_pass转发请求到通过upstream定义的一组应用服务器
            proxy_pass       http://tomcat_gray1;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_redirect off;
            proxy_set_header X-Real-IP  $remote_addr;
        }
        location  ~*^.+$ {       #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。  
            proxy_pass      http://域名;  #请求转向域名 定义的服务器列表          
        } 
    }
    #标准预发环境
    upstream tomcat_gray1 {
        server ip; 
        server 域名;
    }

    upstream tomcat_gray2 {
        server 域名;
    }
}

host 配置

127.0.0.1  域名

浏览器访问 域名

可以通过观察access.log发现请求接入日志。

2、lua基础语法

教程:https://www.runoob.com/lua/if-else-statement-in-lua.html

lua的IDE编辑器:https://github.com/rjpcomputing/luaforwindows

3、nginx实现灰度

根据前端请求参数进行灰度到不同节点。

#user  nobody;
worker_processes  1;
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
#pid        logs/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    #log_format  main '$time_local 客户端地址:$remote_addr–$remote_port 请求的URI和HTTP协议:$request 请求地址:$http_host HTTP请求状态:$status upstream状态:$upstream_status 负载地址:$upstream_addr url跳转来源:$http_referer $body_bytes_sent $http_user_agent $request_uri';
    log_format  logFormat '$group $time_local 客户端:$remote_addr–$remote_port 请求的URI和HTTP协议:$request 请求:$http_host HTTP状态:$status upstream状态:$upstream_status 负载:$upstream_addr 
                          url跳转:$http_referer $body_bytes_sent $http_user_agent $request_uri 请求参数 $query_string $args $document_root $uri
                          -----$request_uri $request_filename $http_cookie';
    access_log  logs/access.log  logFormat;
    sendfile        on;
    #tcp_nopush     on;
    #keepalive_timeout  0;
    keepalive_timeout  65;
    #gzip  on;
    server {
        listen       80;   #监听端口
        server_name  域名; #监听地址 
        access_log  logs/xx.com.access.log  logFormat;
        #方式二、nginx+lua实现灰度
        ## 1、将对localhost访问由/opt/app/lua/dep.lua进行处理
        ## 2、根据逻辑处理后,决定回调如下两个其中1个内部跳转
        #方式三根据请求参数值匹配进行路由
        #/policy/policyInfoList?thirdPolicystatus=2
        set $group "default";
        if ($query_string ~* "thirdPolicystatus=1"){ #动态控制路由
            set $group new_version;
        }
        if ($query_string ~* "thirdPolicystatus=2"){
            set $group old_version;
        }
        location / 
        {
            default_type "text/html"; 
            #content_by_lua_file D:/sortware/openresty/openresty-1.17.8.2-win64/conf/dep.lua; # 指定由lua文件处理http请求
            proxy_pass http://$group;
            proxy_set_header  Host       $host;
            proxy_set_header  X-Real-IP    $remote_addr;
            proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
            index index.html index.htm;
        }
    }
    #标准预发环境
    upstream default {
        server ip:port; 
    }
    #预发2
    upstream new_version {
        server ip:port;
    }
    #预发3
    upstream old_version {
        server ip:port;
    }
}

host如下:

127.0.0.1  域名

访问地址:

域名

菜单运营数据---保单数据,默认走default集群,保单状态承保成功走new_version集群,保单状态终止走old_version集群

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;
    #log_format  main '$time_local 客户端地址:$remote_addr–$remote_port 请求的URI和HTTP协议:$request 请求地址:$http_host HTTP请求状态:$status upstream状态:$upstream_status 负载地址:$upstream_addr url跳转来源:$http_referer $body_bytes_sent $http_user_agent $request_uri';
    log_format  logFormat '$http_cookie $group $time_local 客户端:$remote_addr–$remote_port 请求的URI和HTTP协议:$request 请求:$http_host HTTP状态:$status upstream状态:$upstream_status 负载:$upstream_addr 
                          url跳转:$http_referer $body_bytes_sent $http_user_agent $request_uri 请求参数 $query_string $args $document_root $uri
                          -----$request_uri $request_filename ';
    access_log  logs/access.log  logFormat;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;
    #gzip  on;
    server {
        listen       80;   #监听端口
        server_name  域名; #监听地址 
        access_log  logs/xx.com.access.log  logFormat;
        #方式二、nginx+lua实现灰度
        ## 1、将对localhost访问由/opt/app/lua/dep.lua进行处理
        ## 2、根据逻辑处理后,决定回调如下两个其中1个内部跳转
        #方式三根据请求参数值匹配进行路由
        #域名policy/policyInfoList?thirdPolicystatus=2
        set $group "default";
        if ($query_string ~* "thirdPolicystatus=1"){ #动态控制路由
            set $group new_version;
            }
        if ($query_string ~* "thirdPolicystatus=2"){
            set $group old_version;
            }
        if ($http_cookie ~* "sso.xx.com=BJ.E2C7D319112E7F6252BF010770269E235820211121073248"){
            set $group pro_version;
            }
        if ($http_cookie ~* "sso.xx.com!=BJ.E2C7D319112E7F6252BF010770269E235820211121073248"){
            set $group grey_version;
            }
        location / 
        {
            default_type "text/html"; 
            #content_by_lua_file D:/sortware/openresty/openresty-1.17.8.2-win64/conf/dep.lua; # 指定由lua文件处理http请求
            proxy_pass http://$group;
            proxy_set_header  Host       $host;
            proxy_set_header  X-Real-IP    $remote_addr;
            proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
            index index.html index.htm;
        }
    }
    #标准预发环境
    upstream default {
        server ip:port; 
    }
    #预发2
    upstream new_version {
        server ip:port;
    }
    #预发3
    upstream old_version {
        server ip:port;
    }
        #预发2
    upstream pro_version {
        server ip:port;
    }
    #预发3
    upstream grey_version {
        server ip:port;
    }
}

根据cookie内容转发到不同的集群

四、相关可操作和替换性

想法一:如果这个时候我门需要一个动态化配置控制台则可以通过javaweb等工程进行操作redis实现实时更新redis数据从而控制灰度

想法二:切换其他数据源比如

  1. MySQL/MariaDB: 使用 Lua 的 lua-mysql 或 LuaSQL 库,您可以在 Lua 中连接和查询 MySQL 或 MariaDB 数据库。
  2. PostgreSQL: 使用 Lua 的 lua-postgres 或 LuaSQL 库,您可以在 Lua 中连接和查询 PostgreSQL 数据库。
  3. MongoDB: 使用 Lua 的 mongo-lua-driver 库,您可以在 Lua 中连接和操作 MongoDB 数据库。
  4. HTTP API: 使用 Lua 的 LuaHTTP 库,您可以在 Lua 中发起 HTTP 请求,并与远程的 HTTP API 进行通信。
  5. Cassandra: 使用 Lua 的 lua-cassandra 库,您可以在 Lua 中连接和查询 Cassandra 数据库。

想法三:切换其他脚本语言

  1. JavaScript: 通过使用 Nginx 的 ngx_http_js_module,您可以在 Nginx 中使用 JavaScript。这可以让您使用 JavaScript 脚本来实现一些灰度发布或其他功能。此外,JavaScript 也广泛用于前端开发,因此可以在前后端一体化的项目中更容易共享代码逻辑。
  2. LuaJIT: LuaJIT 是一个通过即时编译实现高性能的 Lua 解释器。它提供了与标准 Lua 解释器兼容的 API,但是比标准 Lua 解释器更快。使用 LuaJIT,您可以获得更高的性能,同时保持与 Lua 的兼容性。
  3. Python: 如果您已经熟悉 Python,您可以使用 Python-NGINX-Module 在 Nginx 中嵌入 Python。这样可以使用 Python 编写 Nginx 的配置文件和处理请求的逻辑。
  4. Java: 使用 nginx-jvm-clojure 或 nginx-jwt 等模块,您可以在 Nginx 中嵌入 Java 或 Clojure。这些模块提供了在 Nginx 上运行 Java 或 Clojure 代码的功能,可以与其他 Java 或 Clojure 库和框架进行集成。

想法四:切换其他web服务器或反向代理服务器

  1. Apache HTTP Server: Apache 是一个广泛使用的开源 Web 服务器和反向代理服务器,它支持多种模块和扩展,提供了丰富的功能和配置选项。
  2. Microsoft IIS: Internet Information Services (IIS) 是由 Microsoft 开发的 Web 服务器,专为 Windows 操作系统设计。它是 Windows Server 默认的 Web 服务器,提供了广泛的功能和集成。
  3. Caddy: Caddy 是一个用 Go 编写的现代化的 Web 服务器和反向代理服务器。它具有简单配置、自动 HTTPS、HTTP/2 支持等特性。
  4. HAProxy: HAProxy 是一个高性能的负载均衡器和反向代理服务器,适用于高流量的 Web 应用程序。它具有丰富的负载均衡和代理功能。
  5. Envoy: Envoy 是一个轻量级的开源代理服务器和通信总线,适用于云原生和微服务架构。它具有动态配置、负载平衡、流量管理等功能。

大家可以根据自己的想法或者兴趣进行研究,本文不做过多介绍