前段时间看着学弟学妹们张罗论坛的事,就想着我这把老骨头能做点啥,想着想着就发现好像精弘还没有一套可行的灰度方案,所以我就想研究一套业务层几乎无感的灰度方案给大家试试。

方案选型 链接到标题

想要对业务层的侵入最小甚至于无感其实方法还是蛮多的,比如 Spring 里的 AOP,或者 gin 里的中间件,但这些都只能对服务端的接口进行灰度,一旦到了前端部分就失效了。

当然前端跟后端各自维护一套灰度机制也可以完美解决这个问题,但我还是很想大一统的用一套体系完美解决这个问题,所以就开始查资料了。

前端跟后端从 nginx 层面开始就分出了两条路,因此想要大一统解决灰度问题只能是在 nginx 上做文章了。

我查了一些文章发现 nginx 是可以增加一个 lua 扩展,允许开发者通过 lua 脚本对 nginx 的能力进行加强的,这让我很兴奋,我不正是想要对 nginx 进行一些二次开发吗。

但再研究下来发现只是 lua 能力还是有限,并且开启这个扩展需要重新编译 nginx,有很多人吐槽这个难度很大,一时间我又陷入了迷茫。

好在天无绝人之路,我发现了一个叫 openresty 的基于 nginx 的 web 平台,既有 nginx 的能力,还允许开发者通过 lua 脚本对它进行二次开发,更可贵的还有一些三方包允许开发者方便的解析 JWT、操作 redis,这下我想要的功能都齐活了,走起!

实施过程 链接到标题

方案和技术选型定好了,下面就是要梳理逻辑思考流程了。

我想做的就是流量打进来的时候用 lua 编写的脚本把 header 里的 JWT 拿出来,解码后判断对应的 uid 有没有在 redis 里存着的灰度 uid 列表中,如果在的话就转发到灰度环境里,否则转发到线上环境里。

把这些梳理清楚后就可以正式开始开发了。

开发过程 链接到标题

环境准备 链接到标题

首先先得把 lua 脚本嵌入到 nginx 的配置文件里,然后配一个灰度环境一个线上环境,还要有一个具备登录和 JWT 鉴权的服务端。

location /pre {
	alias /root/www/pre;
	index index.html;
}

location /gray {
	alias /root/www/gray;
	index index.html;
}

location /prod {
	alias /root/www/prod;
	index index.html;
}
location /test {
	default_type text/html;
	content_by_lua_block {
		 ngx.say("Hello World")
	}
}
location /api {
	proxy_pass http://127.0.0.1:8088;
}

上面的配置里我定义了预发、灰度、线上三个环境,还有一个测试环境用于测试 lua 脚本是否能够正常使用,最后一个服务端提供鉴权。

测下来每一个环境都没问题了过后就可以安心开始逻辑编写了。

获取 header 里的 JWT 并解析 链接到标题

这部分没什么好讲解的,直接看代码吧。

local jwt = require "resty.jwt"
local cjson = require "cjson"
local headers = ngx.req.get_headers() 
local auth_header = headers["Authorization"local secret = "your_secret_key"

if not auth_header then
	ngx.say("Authorization header not found") 
	ngx.exit(ngx.HTTP_UNAUTHORIZED) 
end

-- 解析 JWT
local jwt_obj = jwt:verify(secret, auth_header)

if not jwt_obj.verified then
    ngx.say("JWT verification failed: " .. jwt_obj.reason)
    ngx.exit(ngx.HTTP_UNAUTHORIZED)
end

ngx.say(cjson.encode(jwt_obj)) -- 得知登录后服务端把 uid 放到了 payload.id 里

-- 从 payload 中获取用户ID
local user_id = jwt_obj.payload.id
if not user_id then
    ngx.say("User ID not found in JWT payload")
    ngx.exit(ngx.HTTP_UNAUTHORIZED)
end

ngx.say(jwt_obj.payload.id)

从 redis 里查询灰度 uid 列表 链接到标题

local redis = require "resty.redis"
local user_id = "uid in jwt"

-- 初始化 Redis 连接
local red = redis:new()
red:set_timeout(1000)  -- 设置超时时间为 1 秒

-- 连接到 Redis
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.say("Failed to connect to Redis: ", err)
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end

-- 获取 grayUserList 列表中的所有元素
local gray_user_list, err = red:lrange("grayUserList", 0, -1)
if not gray_user_list then
    ngx.say("Failed to get grayUserList from Redis: ", err)
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end

-- 检查 user_id 是否在 grayUserList 中
local user_in_gray_list = false
for _, id in ipairs(gray_user_list) do
    if id == tostring(user_id) then
        user_in_gray_list = true
        break
    end
end

if user_in_gray_list then
    ngx.say("User ID ", user_id, " is in the gray list")
else
    ngx.say("User ID ", user_id, " is not in the gray list")
end

-- 关闭 Redis 连接
local ok, err = red:set_keepalive(10000, 100)
if not ok then
    ngx.say("Failed to set keepalive: ", err)
    return
end

最终成品 链接到标题

server {
	listen       80;

	location / {
		set $target_path "";
		access_by_lua_block {
			local headers = ngx.req.get_headers()
			local redis = require "resty.redis"
			local secret = "secretKey"
			local jwt = require "resty.jwt"

			local function is_user_in_gray_list(token)
				local jwt_obj = jwt:verify(secret, token)
	
				if not jwt_obj.verified then
					return false, "JWT verification failed: " .. jwt_obj.reason
				end
	
				local user_id = jwt_obj.payload.id
				if not user_id then
					return false, "User ID not found in JWT payload"
				end
	
				local red = redis:new()
				red:set_timeout(1000)
	
				local ok, err = red:connect("127.0.0.1", 6379)
				if not ok then
					return false, "Failed to connect to Redis: " .. err
				end
	
				local gray_user_list, err = red:lrange("grayUserList", 0, -1)
				if not gray_user_list then
					return false, "Failed to get grayUserList from Redis: " .. err
				end
	
				local user_in_gray_list = false
				for _, id in ipairs(gray_user_list) do
					if id == tostring(user_id) then
						user_in_gray_list = true
						break
					end
				end
	
				local ok, err = red:set_keepalive(10000, 100)
				if not ok then
					return false, "Failed to set keepalive: " .. err
				end
	
				if user_in_gray_list then
					ngx.var.target_path = "/root/www/gray"
				else
					ngx.var.target_path = "/root/www/prod"
				end
	
			end
		
			is_user_in_gray_list(ngx.var.http_authorization)
		}
		
		root $target_path;
		try_files $uri $uri/ /index.html;
	}
	
	location /pre {
		alias /root/www/pre;
		index index.html;
	}

	location /api {
		proxy_pass http://127.0.0.1:8088;
	}
}

写在最后 链接到标题

当然这个方案只是我的一个玩具罢了,性能到底怎么样仍是一个未知数,希望后续有机会能对他进行一些压测,找到一条可以较为完美解决论坛开发维护所需的路线吧。