zl程序教程

您现在的位置是:首页 >  其他

当前栏目

【网关开发】9.Openresty 自定义流量分流策略支持灰度(金丝雀)等发布业务场景

2023-04-18 15:18:42 时间

背景

随着云技术和基础架构的成熟,发布过程中可以通过引入相应的发布策略,能让我们在早期实验阶段就可以发现、调整问题,来保证整体系统的稳定性
网关作为流量入口,要求有能力进行流量分流配置支持各种灰度发布、金丝雀发布、滚动更新等模式

发布模式

蓝绿发布

通过部署两套环境来解决新老版本的发布问题,流量逐渐的从老系统切换到新系统中,同时保持两个系统中在线,同时切换

金丝雀发布

是灰度发布的一种实现,部署的时候让一小部分用户先试用功能 ,通过日志监控或者服务器监控,或新用户的反馈。如果没有严重问题,尽快部署这个新版本,否则快速回滚,小代价去试错

滚动更新

一般是取出一个或者多个服务器停止服务,执行更新,并重新将其投入使用

实现原理

分成标记和代理策略两部分,标记是根据用户的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中选择机器,两个也不一定成对使用,也可以单独进行配置使用,实现更复杂和独特的业务场景。