nginx+lua在我司的实践

导读:nginx是一个高性能的反向代理服务器,lua是一个小巧的脚本语言,这两个的巧妙结合会擦出怎样的火花呢。

关键词:nginx,lua,nginx+lua



前言


nginx,lua,nginx+lua,这三个名词不知道大家熟悉多少。为了后面内容的展示,我简单的介绍一下它们,想深入了解的网上资料很多,在这就不啰嗦了。nginx是一个高性能的反向代理服务器,一般会处在网站的最前端(有可能前面还会加一层slb,在这暂时忽略),用来做后端web服务的代理;lua是一个小巧的脚本语言,其设计的目的就是嵌入应用程序中,为其提供一些扩展和增强,比如redis,nginx等等;nginx+lua顾名思义就是nginx和lua的结合体,这两者之间沟通的桥梁是 nginx_lua_module
,它是nginx的一个模块,有了它nginx和lua才能互通,笔者在最近几年的工作中恰好有这方面的开发经验,所以想把这些真实的使用场景分享出来供大家做参考,具体的api doc还请大家参考官方资料。

案例1-入口层流量的灰度识别

何谓入口层流量的灰度识别呢,简单来说就是A用户的请求打到线上环境,B用户的请求打到灰度环境,目的就是做新功能的验证,实现逻辑很简单,大体流程如下:
1.测试同学在灰度控制台配置灰度规则,规则里会约束哪些url下哪些商户的请求进入灰度环境;

2.灰度控制台推送规则给入口层nginx,nginx会将规则存储到本地内存中,借助ngx.shared.DICT;
3.请求进入的时候(通过rewrite_by_lua_file触发)获取本地内存中的规则进行比对,如何命中规则就将请求转发到灰度环境,对nginx来说就是切换不同的upstream,比如线上是prod_serverA,灰度是gray_serverA;
代码片段如下:

upstream  gray_serverA {
    server 192.68.1.1:8080;
}

upstream prod_serverA {
    server 192.68.1.2:8080;
}

server {
    listen 80;
    server_name graytest.demo.com;
    charset utf-8;
    
    location  ~ \.do$ {
        set $backend 'prod_serverA';   #默认的upstream为线上服务
        rewrite_by_lua_file "conf/lua-gray/rewriter.lua"; # rewrite_by_lua_file 可以简单的理解为一个过滤器,nginx在rewrite阶段会执行你指定的脚本文件,
                               #在这个文件中我们会判断请求是否为灰度请求如果是灰度请求就将backend改为gray_serverA
        proxy_pass http://$backend;
    }   
}

案例2-入口层记录错误日志

之前有同学反馈说springmvc偶尔会报415错误,这个错误的一个常见原因是传递的Content-Type头不对,比如后端需要application/json,但是前端传递了application/x-www-form-urlencoded,那就会报这个错。但是跟前端确认了传递的头是没有问题的,有人猜测可能是头信息有特殊字符,导致后端web 容器(tomcat、resin)解析头出现了问题,既然这样把所有请求头打出来一看究竟,于是lua再一次出场,这次主要用来输出请求头到日志文件中,主要用到了 log_by_lua_block
这个指令,代码片段如下:

location  ~ \.do$ {
        proxy_pass http://$backend;
        log_by_lua_block {
      //判断下如果http 响应状态码为415就输出请求头到文件中
          if tonumber(ngx.var.status) == 415 then
                ngx.log(ngx.ERR,"upstream reponse status is 415,please notice it,here are request headers:")
                local h, err = ngx.req.get_headers(100,true)
 
               if err == "truncated" then
                    ngx.log(ngx.ERR,"request headers beyond 100,will not return")
                else
                   local cjson = require("cjson")
                   ngx.log(ngx.ERR,cjson.encode(h))
              end
          end
       }
   }

想详细了解这个案例的,请移步到我的另一篇博客( https://www.cnblogs.com/chopper-poet/p/11625144.html
)。

案例3-将nginx信息注册到监控平台

需求很简单,当nginx启动的时候将自身信息上报到redis中,上报内容包扣自身的ip,代理的域名信息等,有个监控平台会定期从redis读取这些信息做展示,方便运维干活,流程很简单就是定时读取自身ip和代理的域名信息写到redis中,为什么要定时呢,这里还起到一种心跳的效果,当长时间没有上报时可能是nginx出问题了,主要用到的指令如下:

1. init_worker_by_lua_file
“conf/ua-grayngxReloadListener.lua”,当nginx启动或者reload的时候会执行指定的文件;
2.ngx.worker.id() 这个方法会返回nginx worker进程的编号,从0开始,如果nginx有4个worker,那取值范围为0-3,这个主要是为了防止并发上报,因为第一步里面提到的init_worker_by_lua_file,如果有几个worker就会触发几次,所以为了只上报一次,会判断下返回值是否为0,也就是只让第一个worker执行;

3. ngx.timer.every
用来执行定时任务;
代码片段如下:

local workerId = ngx.worker.id()

if(workerId == 0) then
    ngx.log(ngx.INFO,'workerId is 0 will startup task')
        
    
    local ok, err = ngx.timer.every(4,function()
        #1. get local ip and domins
            #2. write to redis 
    end)

else
    ngx.log(ngx.INFO,'workerId is not 0 ,just ignore it')
end

案例4-将入口层流量同时转发到多个后端服务

类似于消息队列的发布订阅一样,在nginx这一层可以将一个请求同时发到多个地址,代码片段如下:

location  ~ /capture_test$ {
            content_by_lua_block {
            ngx.req.read_body()
            
            res1, res3 = ngx.location.capture_multi{
                        { "/capture_test1",{ method = ngx.HTTP_POST, always_forward_body=true}},
                       { "/capture_test2",{ method = ngx.HTTP_POST, always_forward_body=true}},
                  }

           ngx.say(res3.body)
           ngx.exit(res3.status)
    }

总结

上面列举了nginx+lua在我司使用的四个案例,真实场景要复杂很多,为了方便大家理解,特意将案例做了简化。如果觉得有用,请点个推荐。

参考资料

https://github.com/openresty/lua-nginx-module