使用Nginx+Lua实现Web项目的灰度发布

需求:

领导对时间要求紧迫、研发对现有系统摸不透、做到数据的兼容性,基于这样的要求就必须做到系统上线采用灰度的方式,指定忠实用户进行线上测试、选取有特征的群体进行线上测试和基于流量切换的方式进行线上测试等。

常规的部署做法:

常规的部署方式是采用Nginx的 upstream  配置来简单的实现新旧机器的切换, 在开发过程中,开发完成,完成测试阶段,修复bug后都要重启后台服务,测试又在测试,每次重启都要一两分钟,平凡的重启,测试不干了;所以想到就是部署两台服务器;用nginx upstream 模块实现 无感知部署,发现一个bug,修复;直接部署不会打断测试;常用的是一台部署完毕之后部署另外一台机器; 

灰度发布概述:

灰度发布,简单来说,就是根据各种条件,让一部分用户使用旧版本,另一部分用户使用新版本。

灰度发布是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面 来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

灰度部署还可以根据设定的规则将请求路由到我们的灰度版本(灰度机器)上来。比如对于API来说,一般有如下几个需求:特定用户(比如测试帐号)、 特定的App(比如测试app或者合作App)、特定的模块、接口(只有某些接口需要灰度,这种一般是API Container的修改,拿一些不是很重要的API做灰度测试)、特定的机器(某些请求IP转发到灰度机)等。

本章只是简单的简述灰度部署的实现思路:

这里我们所做的灰度发布稍作改变:用1-2台机器作为B,B测试成功再部署A。用于WEB系统新代码的测试发布,让一部分(IP)用户访问新版本,一部分用户仍然访问正常版本,原理如图:

 image.png


执行过程:
1
、当用户请求到达前端web(代理)服务器Openresty,内嵌的lua模块解析Nginx配置文件中的lua脚本代码;
2
、Lua获取客户端IP地址,去查询Redis中是否有该键值,如果有返回值执行@clien2,否则执行@client1。
3
、Location @client2把请求转发给预发布服务器,location @client1把请求转发给生产服务器,服务器返回结果,整个过程完成。

Lua 的好处并不至于这个,可以使用LUA语言是实现一些业务上的负载,比如热点分离; 热点数据的自动降级机制;

  Lua教程: https://www.runoob.com/lua/lua-tutorial.html


案例实现:

1.       安装部署OpenResty:

OpenResty由Nginx核心加很多第三方模块组成,默认集成了Lua开发环境,使得Nginx可以作为一个Web Server使用。
     借助于Nginx的事件驱动模型和非阻塞IO,可以实现高性能的Web应用程序。
     而且OpenResty提供了大量组件如Mysql、Redis、Memcached等等,使在Nginx上开发Web应用更方便更简单。

1、部署第一个nginx,作为应用层nginx(192.168.1.104那个机器上)

1、  创建目录/usr/servers,以后我们把所有软件安装在此目录

mkdir -p /usr/servers 

cd /usr/servers/

2、  安装依赖(我的环境是centos,可以使用如下命令安装,其他的可以参考openresty安装步骤)

   yum install -y readline-devel pcre-devel openssl-devel gcc

 3、  下载ngx_openresty-xxx.tar.gz并解压(ngx_openresty-xxx/bundle目录里存nginx核心和很多第三方模块,比如有我们需要的Lua和LuaJIT。)

wget https://openresty.org/download/ngx_openresty-1.9.7.1.tar.gz

tar xvf ngx_openresty-1.9.7.1.tar.gz

 cd ngx_openresty-1.9.7.1

2. 安装LuaJIT

cd bundle/LuaJIT-2.1-20151219/

make clean && make && make install

 ln -sf luajit-2.1.0-alpha /usr/local/bin/luajit

 下载ngx_cache_purge模块,该模块用于清理nginx缓存

root@user:/usr/servers/ngx_openresty-1.9.7.1/bundle# wget https://github.com/FRiCKLE/ngx_cache_purge/archive/2.3.tar.gz
root@user:/usr/servers/ngx_openresty-1.9.7.1/bundle# tar -xvf 2.3.tar.gz

下载nginx_upstream_check_module模块,该模块用于ustream健康检查

             root@user:/usr/servers/ngx_openresty-1.9.7.1/bundle# 

                   wget https://github.com/yaoweibin/nginx_upstream_check_module/archive/v0.3.0.tar.gz

root@user:/usr/servers/ngx_openresty-1.9.7.1/bundle# tar -xvf v0.3.0.tar.gz


  安装ngx_openresty

root@user:/usr/servers/ngx_openresty-1.9.7.1/bundle# cd .. 
root@user:/usr/servers/ngx_openresty-1.9.7.1# ./configure –prefix=/usr/servers –with-http_realip_module –with-pcre –with-luajit –add-module=./bundle/ngx_cache_purge-2.3/ –add-module=./bundle/nginx_upstream_check_module-0.3.0/ -j2 
root@user:/usr/servers/ngx_openresty-1.9.7.1# make && make install

参数说明:
–with***
安装一些内置/集成的模块
–with-http_realip_module
取用户真实ip模块
-with-pcre Perl
兼容的达式模块
–with-luajit
集成luajit模块
–add-module
添加自定义的第三方模块,如此次的ngx_che_purge

  到/usr/servers目录下用ll命令查看,会发现多出来了如下目录,说明安装成功

root@user:/usr/servers/ngx_openresty-1.9.7.1# cd /usr/servers/ 
root@user:/usr/servers# ll

image.png

说明:
/usr/servers/luajit
:luajit环境,luajit类似于java的jit,即即时编译,lua是一种解释语言,通过luajit可以即时编译lua代码到机器代码,得到很好的性能;
/usr/servers/lualib
:要使用的lua库,里边提供了一些默认的lua库,如redis,json库等,也可以把一些自己开发的或第三方的放在这;
/usr/servers/nginx
:安装的nginx,通过/usr/servers/nginx/sbin/nginx -V 查看nginx版本和安装的模块

启动nginx

root@user:/usr/servers# /usr/servers/nginx/sbin/nginx
检测配置是否正确(需要先切换到root用户):
root@user:/usr/servers# /usr/servers/nginx/sbin/nginx -t
重启nginx:
root@user:/usr/servers# /usr/servers/nginx/sbin/nginx -s reload


LUA环境测试:

1、              为了方便开发我们在/usr/servers/nginx/conf目录下创建一个lua.conf 
root@user:/home/user# cd /usr/servers/nginx/conf
root@user:/usr/servers/nginx/conf# vim lua.conf

server  {

       listen 80;

       server_name _;

       #HelloWorld

       location /lua {

              default_type 'text/html';

              content_by_lua 'ngx.say("hello world")';

       }

}

编辑nginx.conf配置文件 

vim /usr/servers/nginx/conf/nginx.conf 
在http部分添加如下配置 
lua_package_path "/usr/servers/lualib/?.lua;;"; #lua
模块 
lua_package_cpath "/usr/servers/lualib/?.so;;"; #c
模块 
include lua.conf; #
单独lua配置

重启nginx

  /usr/servers/nginx/sbin/nginx -s reload

 访问如http://192.168.1.104/lua(自己的机器根据实际情况换ip),可以看到如下内容 

hello world
说明配置成功。

灰度部署测试:

采用redis 方式;比如 192.168.0.101这个IP采用的是访问服务器的项目资源,其他IP是访问旧项目的资源;进行测试完毕并且完成之后,可以切换正式环境;

image.png

默认情况下lua_code_cache  是开启的,即缓存lua代码,即每次lua代码变更必须reload nginx才生效,如果在开发阶段可以通过lua_code_cache  off;关闭缓存,这样调试时每次修改lua代码不需要reload nginx;但是正式环境一定记得开启缓存

开启后reload nginx会看到如下报警
nginx: [alert] lua_code_cache is off; this will hurt performance ******;


配置文件:

Nginx.conf:

#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;
       
       lua_package_path "/usr/servers/lualib/?.lua;;"; #lua 模块 
       lua_package_cpath "/usr/servers/lualib/?.so;;"; #c模块 
       
 
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
 
    access_log  logs/access.log  main;
 
    sendfile        on;
    #tcp_nopush     on;
 
    #keepalive_timeout  0;
    keepalive_timeout  65;
 
    #gzip  on;
       
       proxy_buffering             off;
    proxy_set_header            Host $host;
    proxy_set_header            X-real-ip $remote_addr;
    proxy_set_header            X-Forwarded-For $proxy_add_x_forwarded_for;
       
       
       upstream productServer {
         server 127.0.0.1:8080 weight=1 max_fails=3 fail_timeout=100s; #模拟生产服务器
       }
 
       upstream preServer {
         server 127.0.0.1:8081 weight=1 max_fails=3 fail_timeout=100s;  #模拟预发布服务器
       }
 
 
    server {
        listen       80;
        server_name  localhost;
 
        #charset koi8-r;
 
        #access_log  logs/host.access.log  main;
 
        location / {
                default_type 'text/html';  
          lua_code_cache off;  
          content_by_lua_file /usr/servers/nginx/conf/huidu.lua;
        }
 
        #error_page  404              /404.html;
        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
 
              location @productServer{
                proxy_pass http://productServer;
        }
              
        location @preServer{
                proxy_pass http://preServer;
        }
    
    }
 
 
 
}

huidu.lua:

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 
    ngx.say("failed to connect:", err) 
    return 
end 
 
--local red, err = cache:auth("foobared")
--if not red then
    --ngx.say("failed to authenticate: ", err)
    --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"]
end
 
if local_ip == nil then
    local_ip = ngx.var.remote_addr
end
--ngx.say("local_ip is : ", local_ip)
 
local intercept = cache:get(local_ip) 
 
 
if intercept == local_ip then
    ngx.exec("@preServer")
    return
end
 
ngx.exec("@productServer")
 
local ok, err = cache:close() 
 
if not ok then 
    ngx.say("failed to close:", err) 
    return 
end

上面的代码是简单的IP相等,可以采用IP段形式;


上面的例子测试:

image.png

image.png

image.png

额外参考资料:

https://my.oschina.net/izhangll/blog/884713