【网关开发】9.Openresty 自定义流量分流策略支持灰度(金丝雀)等发布业务场景
背景
随着云技术和基础架构的成熟,发布过程中可以通过引入相应的发布策略,能让我们在早期实验阶段就可以发现、调整问题,来保证整体系统的稳定性
网关作为流量入口,要求有能力进行流量分流配置支持各种灰度发布、金丝雀发布、滚动更新等模式
发布模式
蓝绿发布
通过部署两套环境来解决新老版本的发布问题,流量逐渐的从老系统切换到新系统中,同时保持两个系统中在线,同时切换
金丝雀发布
是灰度发布的一种实现,部署的时候让一小部分用户先试用功能 ,通过日志监控或者服务器监控,或新用户的反馈。如果没有严重问题,尽快部署这个新版本,否则快速回滚,小代价去试错
滚动更新
一般是取出一个或者多个服务器停止服务,执行更新,并重新将其投入使用
实现原理
分成标记和代理策略两部分,标记是根据用户的header中key或者IP等特征,对流量进行标记,代理策略则根据用户的标记值选择对应的机器列表,实现特定的用户访问特定的机器
架构图
流程说明
金丝雀与灰度的流量原理一致
1.云平台开始进行金丝雀发布,一般新启动一个POD,将机器元数据增加标签(runtime:v1.0.0)
2.通知网关管理后台增加流量规则
3.网关管理后台将数据写入etcd,etcd推送给所有网关节点的openresty更新配置
4.特殊用户访问请求会将这部分流量进行标记,标记结果是将header中会带有特殊的值
5.在负载均衡模块会将标签特殊值与带有元数据标签的机器进行选择。
核心代码
本次代码库:https://github.com/zhaoshoucheng/openresty/blob/main/pkg/lua_script/upstream/traffic_policy.lua
在设计上实现两种标记方式一种是
- 流量分组:一个流量只能有一个分组,多个分组之间的流量是相互独立
- 流量标签:一个流量可以有很多个标签。所以流量可以在多个标签间共用。
数据结构
流量标记
set_group 是分组标记 所有ip是10.99.4.179会被标记成gray分组
set_tag 是标签标记 所有header中带有x-canary-test:test 会打上标签ruversion:ruversion_server_test
{
"available_domain": ["server_test.com", "server_test1.com"],
"name": "server_test",
"rules": [{
"actions": {
"action": "set_group",
"value": "gray"
},
"key": "",
"op": "equal",
"type": "ip",
"value": "10.99.4.179"
}, {
"actions": {
"action": "set_tag",
"key": "ruversion",
"value": "ruversion_server_test"
},
"key": "x-canary-test",
"op": "equal",
"type": "headers",
"value": "test"
}]
}
代理策略
当分组是gray,则匹配元数据traffic_strategy:gray的机器
{
"apply_on": ["server_test.com", "server_test1.com"],
"enabled_when": {
"match_group": "gray",
"match_tags": {}
},
"endpoint_metadata_match": {
"traffic_strategy": "gray"
},
"name": "server_test_match_group"
}
当标记的标签是ruversion:ruversion_server_test,则匹配元数据app_version:server_test-v1.0.0的机器
{
"apply_on": ["server_test.com", "server_test1.com"],
"enabled_when": {
"match_group": "",
"match_tags": {
"ruversion": "ruversion_server_test"
}
},
"endpoint_metadata_match": {
"app_version": "server_test-v1.0.0"
},
"name": "server_test_match_tags"
}
机器元数据信息
metadata中保存的标签
{
"endpoints": [{
"address": "10.218.22.239:8090",
"metadata": {
"app_version": "server_test-v2.0.0",
"traffic_strategy": "gray",
},
"modify_date": 0,
"state": "up",
"weight": 1
}, {
"address": "10.218.22.246:8090",
"metadata": {
"app_version": "server_test-v1.0.0",
"traffic_strategy": "default",
},
"modify_date": 0,
"state": "up",
"weight": 1
}]
}
剩下的就是通过程序,把这些关联起来
流量标记
apis.conf 配置access_by_lua_block阶段需要调用的lua块
access_by_lua_block {require "upstream.traffic_policy".do_coloring()}
元表数据结构 _coloring_policy 标记配置,proxy_policy代理策略配置
function _M.new(opt)
return setmetatable({
_coloring_policy = {}, -- map[domain]coloring policy
_proxy_policy = {}, -- map[domain]proxy policy
_etcd_revision = "0",
}, _MT)
end
local function do_coloring(self)
if not coloring_policy then
return
end
local policy = coloring_policy:get_coloring_policy() -- 根据域名获取配置
if not policy then
return
end
local parts = { }
local rules = policy.rules
for i = 1, #rules do
local rule = rules[i]
local cond = rule.op
if not cond then
return "invalid rule op `"..tostring(rule.op).."`"
end
local _match = false
if rule.type and rule.type == "headers" then
local headers = ngx.req.get_headers()
if headers[rule.key] and headers[rule.key] == rule.value then
_match = true
end
end
if rule.type and rule.type == "ip" then
if ngx.var.remote_addr == rule.value then
_match = true
end
end
-- TODO 可以添加其他条件
local actions = rule.actions
if _match then
if actions.action == "set_group" then
ngx.req.set_header("X-Traffic-Group", actions.value)
end
if actions.action == "set_tag" then
table.insert(parts, actions.key.."="..actions.value)
end
end
end
if #parts ~= 0 then
ngx.req.set_header("X-Traffic-Metadata", table.concat(parts, "; "))
end
end
测试
经过流量标记后,header中的值,命中两种不同的规则
group
tag
代理策略
代理策略主要是根据流量标记,选择合适的机器列表,传给负载均衡模块
upstream_context.lua 调用方
-- 将所有机器列表输入,选择合适的机器输出
local ups_nodes = traffoc_policy:do_proxy(self._ups.nodes)
traffic_policy.lua
local function do_proxy(self, nodes)
if not coloring_policy then
return
end
local policies = coloring_policy:get_proxy_policy() --根据域名选择对应的策略
if not policies then
return nodes
end
return _match_metadata(policies, nodes)
end
local function _match_metadata(policies, nodes)
local headers = ngx.req.get_headers()
local header_traffic_group = headers["x-traffic-group"] -- 查看分组标记
local header_traffic_tags = get_header_metadata(headers["x-traffic-metadata"]) -- 标签标记
local endpoint_match = {}
for i = 1, #policies do
local policy = policies[i]
local enabled_when = policy["enabled_when"]
if not enabled_when then
break
end
if enabled_when["match_group"] ~= "" and header_traffic_group and enabled_when["match_group"] == header_traffic_group then
-- group 检测命中
endpoint_match = policy["endpoint_metadata_match"]
break
end
if not header_traffic_tags then
goto continue
end
if enabled_when["match_tags"] and enabled_when["match_tags"] ~= {} then
for key, value in pairs(enabled_when["match_tags"]) do
if header_traffic_tags[key] ~= value then
goto continue
end
end
-- tag 检测命中
endpoint_match = policy["endpoint_metadata_match"]
break
end
::continue::
end
local match_nodes = {}
for i = 1, #nodes do -- 选择机器
local metadata = nodes[i]["metadata"]
for key, value in pairs(endpoint_match) do
if metadata[key] ~= value then
goto nextnode
end
end
table.insert(match_nodes, nodes[i])
::nextnode::
end
return match_nodes
end
测试
策略命中与机器选择
标记分流方式
分组分流方式
总结与思考
最核心的功能是如何使流量分流,也就是特殊的流量转发到特殊的机器,只有可以控制流量的分流才有后续的灰度发布、金丝雀发布、滚动更新等业务场景。
所以所有的流量标记、代理策略实际上都是在实现流量动态分流的功能。
优先级
本文只是为了理解流量分流策略,实际在匹配会有优先级来控制匹配过程
扩展
把思路打开,实际上流量标记就是识别流量,在header中添加数据,代理策略就是根据header中选择机器,两个也不一定成对使用,也可以单独进行配置使用,实现更复杂和独特的业务场景。