This is an automated email from the ASF dual-hosted git repository. spacewander pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push: new 1d79ce5 feat: the traffic-split plugin supports upstream_id (#3512) 1d79ce5 is described below commit 1d79ce57eb84113e44756618a39f2fd4e0dfc8ee Author: Yuelin Zheng <2226815...@qq.com> AuthorDate: Thu Feb 25 20:59:16 2021 +0800 feat: the traffic-split plugin supports upstream_id (#3512) --- apisix/init.lua | 54 ++-- apisix/plugins/traffic-split.lua | 31 +- docs/en/latest/plugins/traffic-split.md | 42 ++- docs/zh/latest/plugins/traffic-split.md | 42 ++- t/plugin/traffic-split.t | 501 ++++++++++++++++++++++++++++++++ 5 files changed, 625 insertions(+), 45 deletions(-) diff --git a/apisix/init.lua b/apisix/init.lua index 5bf9974..26b2bf6 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -352,6 +352,33 @@ function _M.http_access_phase() api_ctx.route_id = route.value.id api_ctx.route_name = route.value.name + if route.value.script then + script.load(route, api_ctx) + script.run("access", api_ctx) + else + local plugins = plugin.filter(route) + api_ctx.plugins = plugins + + plugin.run_plugin("rewrite", plugins, api_ctx) + if api_ctx.consumer then + local changed + route, changed = plugin.merge_consumer_route( + route, + api_ctx.consumer, + api_ctx + ) + + core.log.info("find consumer ", api_ctx.consumer.username, + ", config changed: ", changed) + + if changed then + core.table.clear(api_ctx.plugins) + api_ctx.plugins = plugin.filter(route, api_ctx.plugins) + end + end + plugin.run_plugin("access", plugins, api_ctx) + end + local up_id = route.value.upstream_id if up_id then local upstreams = core.config.fetch_created_obj("/upstreams") @@ -419,33 +446,6 @@ function _M.http_access_phase() core.log.info("enabled websocket for route: ", route.value.id) end - if route.value.script then - script.load(route, api_ctx) - script.run("access", api_ctx) - else - local plugins = plugin.filter(route) - api_ctx.plugins = plugins - - plugin.run_plugin("rewrite", plugins, api_ctx) - if api_ctx.consumer then - local changed - route, changed = plugin.merge_consumer_route( - route, - api_ctx.consumer, - api_ctx - ) - - core.log.info("find consumer ", api_ctx.consumer.username, - ", config changed: ", changed) - - if changed then - core.table.clear(api_ctx.plugins) - api_ctx.plugins = plugin.filter(route, api_ctx.plugins) - end - end - plugin.run_plugin("access", plugins, api_ctx) - end - if route.value.service_protocol == "grpc" then api_ctx.upstream_scheme = "grpc" end diff --git a/apisix/plugins/traffic-split.lua b/apisix/plugins/traffic-split.lua index bf348cc..c070865 100644 --- a/apisix/plugins/traffic-split.lua +++ b/apisix/plugins/traffic-split.lua @@ -97,7 +97,7 @@ local upstreams_schema = { items = { type = "object", properties = { - upstream_id = schema_def.id_schema, -- todo: support upstream_id method + upstream_id = schema_def.id_schema, upstream = schema_def.upstream, weight = { description = "used to split traffic between different" .. @@ -258,18 +258,19 @@ end local function new_rr_obj(weighted_upstreams) local server_list = {} for i, upstream_obj in ipairs(weighted_upstreams) do - if not upstream_obj.upstream then - -- If the `upstream` object has only the `weight` value, it means - -- that the `upstream` weight value on the default `route` has been reached. - -- Need to set an identifier to mark the empty upstream. - upstream_obj.upstream = "empty_upstream" - end - - if type(upstream_obj.upstream) == "table" then - -- Add a virtual id field to uniquely identify the upstream `key`. + if upstream_obj.upstream_id then + server_list[upstream_obj.upstream_id] = upstream_obj.weight + elseif upstream_obj.upstream then + -- Add a virtual id field to uniquely identify the upstream key. upstream_obj.upstream.vid = i + server_list[upstream_obj.upstream] = upstream_obj.weight + else + -- If the upstream object has only the weight value, it means + -- that the upstream weight value on the default route has been reached. + -- Mark empty upstream services in the plugin. + upstream_obj.upstream = "plugin#upstream#is#empty" + server_list[upstream_obj.upstream] = upstream_obj.weight end - server_list[upstream_obj.upstream] = upstream_obj.weight end return roundrobin:new(server_list) @@ -315,11 +316,17 @@ function _M.access(conf, ctx) end local upstream = rr_up:find() - if upstream and upstream ~= "empty_upstream" then + if upstream and type(upstream) == "table" then core.log.info("upstream: ", core.json.encode(upstream)) return set_upstream(upstream, ctx) + elseif upstream and upstream ~= "plugin#upstream#is#empty" then + ctx.matched_route.value.upstream_id = upstream + core.log.info("upstream_id: ", upstream) + return end + core.log.info("route_up: ", upstream) + ctx.matched_route.value.upstream_id = nil return end diff --git a/docs/en/latest/plugins/traffic-split.md b/docs/en/latest/plugins/traffic-split.md index 79886ca..ebf8ef7 100644 --- a/docs/en/latest/plugins/traffic-split.md +++ b/docs/en/latest/plugins/traffic-split.md @@ -41,7 +41,7 @@ Note: The ratio between each upstream may not so accurate since the drawback of | rules.match | array[object] | optional | | | List of matching rules. | | rules.match.vars | array[array] | optional | | | A list consisting of one or more {var, operator, val} elements, like this: {{var, operator, val}, {var, operator, val}, ...}}. For example: {"arg_name", "==", "json"}, which means that the current request parameter name is json. The var here is consistent with the naming of Nginx internal variables, so request_uri, host, etc. can also be used; for the operator part, the currently supported operators are ==, ~=, ~~, [...] | rules.weighted_upstreams | array[object] | optional | | | List of upstream configuration rules. | -| weighted_upstreams.upstream_id | string/integer| optional | | | The upstream id is bound to the corresponding upstream(not currently supported). | +| weighted_upstreams.upstream_id | string/integer| optional | | | The upstream id is bound to the corresponding upstream. | | weighted_upstreams.upstream | object | optional | | | Upstream configuration information. | | upstream.type | enum | optional | roundrobin | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives. | | upstream.nodes | object | optional | | | In the hash table, the key of the internal element is the list of upstream machine addresses, in the format of address + Port, where the address part can be an IP or a domain name, such as 192.168.1.100:80, foo.com:80, etc. value is the weight of the node. In particular, when the weight value is 0, it has special meaning, which usually means that the upstream node is invalid and never wants to be selected. | @@ -53,7 +53,7 @@ Note: The ratio between each upstream may not so accurate since the drawback of The traffic-split plugin is mainly composed of two parts: `match` and `weighted_upstreams`. `match` is a custom conditional rule, and `weighted_upstreams` is upstream configuration information. If you configure `match` and `weighted_upstreams` information, then after the `match` rule is verified, it will be based on the `weight` value in `weighted_upstreams`; the ratio of traffic between each upstream in the plugin will be guided, otherwise, all traffic will be directly Reach the `upstre [...] ->Note: 1. In `match`, the expression in vars is the relationship of `and`, and the relationship between multiple `vars` is the relationship of `or`. 2. In the weighted_upstreams field of the plugin, if there is a structure with only `weight`, it means the upstream traffic weight value on `route` or `service`. Such as: +Note: 1. In `match`, the expression in vars is the relationship of `and`, and the relationship between multiple `vars` is the relationship of `or`. 2. In the weighted_upstreams field of the plugin, if there is a structure with only `weight`, it means the upstream traffic weight value on `route` or `service`. Such as: ```json "weighted_upstreams": [ @@ -66,7 +66,9 @@ The traffic-split plugin is mainly composed of two parts: `match` and `weighted_ ## How To Enable -Create a route and enable the `traffic-split` plugin: +Create a route and enable the `traffic-split` plugin. When configuring the upstream information of the plugin, there are two ways: + +1. Configure upstream information through the `upstream` attribute in the plugin. ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -109,6 +111,40 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 }' ``` +2. Use the `upstream_id` attribute in the plugin to bind upstream. + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "plugins": { + "traffic-split": { + "rules": [ + { + "weighted_upstreams": [ + { + "upstream_id": 1, + "weight": 1 + }, + { + "weight": 1 + } + ] + } + ] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +>Note: **1.** Use the `upstream_id` to bind the defined upstream, it can reuse upstream health detection, retry and other functions. **2.** Support the two configuration methods of `upstream` and `upstream_id` to be used together. + ## Example ### Grayscale Release diff --git a/docs/zh/latest/plugins/traffic-split.md b/docs/zh/latest/plugins/traffic-split.md index 16fa1df..9c8cfb4 100644 --- a/docs/zh/latest/plugins/traffic-split.md +++ b/docs/zh/latest/plugins/traffic-split.md @@ -41,7 +41,7 @@ traffic-split 插件使用户可以逐步引导各个上游之间的流量百分 | rules.match | array[object] | 可选 | | | 匹配规则列表 | | rules.match.vars | array[array] | 可选 | | | 由一个或多个{var, operator, val}元素组成的列表,类似这样:{{var, operator, val}, {var, operator, val}, ...}}。例如:{"arg_name", "==", "json"},表示当前请求参数 name 是 json。这里的 var 与 Nginx 内部自身变量命名是保持一致,所以也可以使用 request_uri、host 等;对于 operator 部分,目前已支持的运算符有 ==、~=、~~、>、<、in、has 和 ! 。操作符的具体用法请看 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) 的 `operator-list` 部分。 | | rules.weighted_upstreams | array[object] | 可选 | | | 上游配置规则列表。 | -| weighted_upstreams.upstream_id | string / integer | 可选 | | | 通过上游 id 绑定对应上游(暂不支持)。 | +| weighted_upstreams.upstream_id | string / integer | 可选 | | | 通过上游 id 绑定对应上游。 | | weighted_upstreams.upstream | object | 可选 | | | 上游配置信息。 | | upstream.type | enum | 可选 | roundrobin | [roundrobin, chash] | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 | | upstream.nodes | object | 可选 | | | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 | @@ -53,7 +53,7 @@ traffic-split 插件使用户可以逐步引导各个上游之间的流量百分 traffic-split 插件主要由 `match` 和 `weighted_upstreams` 两部分组成,`match` 是自定义的条件规则,`weighted_upstreams` 是 upstream 的配置信息。如果配置 `match` 和 `weighted_upstreams` 信息,那么在 `match` 规则校验通过后,会根据 `weighted_upstreams` 中的 `weight` 值;引导插件中各个 upstream 之间的流量比例,否则,所有流量直接到达 `route` 或 `service` 上配置的 `upstream`。当然你也可以只配置 `weighted_upstreams` 部分,这样会直接根据 `weighted_upstreams` 中的 `weight` 值,引导插件中各个 upstream 之间的流量比例。 ->注:1、在 `match` 里,vars 中的表达式是 `and` 的关系,多个 `vars` 之间是 `or` 的关系。2、在插件的 weighted_upstreams 域中,如果存在只有 `weight` 的结构,表示 `route` 或 `service` 上的 upstream 流量权重值。例如: +注:1、在 `match` 里,vars 中的表达式是 `and` 的关系,多个 `vars` 之间是 `or` 的关系。2、在插件的 weighted_upstreams 域中,如果存在只有 `weight` 的结构,表示 `route` 或 `service` 上的 upstream 流量权重值。例如: ```json "weighted_upstreams": [ @@ -66,7 +66,9 @@ traffic-split 插件主要由 `match` 和 `weighted_upstreams` 两部分组成 ## 如何启用 -创建一个路由并启用 `traffic-split` 插件: +创建一个路由并启用 `traffic-split` 插件,在配置插件上游信息时,有以下两种方式: + +1、通过插件中的 `upstream` 属性配置上游信息。 ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -109,6 +111,40 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 }' ``` +2、通过插件中的 `upstream_id` 属性绑定上游服务。 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "plugins": { + "traffic-split": { + "rules": [ + { + "weighted_upstreams": [ + { + "upstream_id": 1, + "weight": 1 + }, + { + "weight": 1 + } + ] + } + ] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +>注:1、通过 `upstream_id` 方式来绑定已定义的上游,它可以复用上游具有的健康检测、重试等功能。2、支持 `upstream` 和 `upstream_id` 的两种配置方式一起使用。 + ## 示例 ### 灰度发布 diff --git a/t/plugin/traffic-split.t b/t/plugin/traffic-split.t index 7c38b68..dc58c60 100644 --- a/t/plugin/traffic-split.t +++ b/t/plugin/traffic-split.t @@ -1550,3 +1550,504 @@ GET /t 1980, 1981, 1981, 1982, 1982 --- no_error_log [error] + + + +=== TEST 44: set upstream(id: 1) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/upstreams/1', + ngx.HTTP_PUT, + [[{ + "nodes": { + "127.0.0.1:1981": 1 + }, + "type": "roundrobin", + "desc": "new upstream" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 45: set upstream(id: 2) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/upstreams/2', + ngx.HTTP_PUT, + [[{ + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin", + "desc": "new upstream" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 46: the upstream_id is used in the plugin +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "uri": "/server_port", + "plugins": { + "traffic-split": { + "rules": [ + { + "match": [ + { + "vars": [["arg_x-api-name", "==", "jack"]] + } + ], + "weighted_upstreams": [ + {"upstream_id": 1, "weight": 2}, + {"upstream_id": 2, "weight": 1}, + {"weight": 2} + ] + } + ] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]=] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 47: `match` rule passed(upstream_id) +--- config +location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local bodys = {} + local headers = {} + for i = 1, 5 do + local _, _, body = t('/server_port?x-api-name=jack', ngx.HTTP_GET, "", nil, headers) + bodys[i] = body + end + table.sort(bodys) + ngx.say(table.concat(bodys, ", ")) + } +} +--- request +GET /t +--- response_body +1980, 1980, 1981, 1981, 1982 +--- no_error_log +[error] + + + +=== TEST 48: only use upstream_id in the plugin +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "uri": "/server_port", + "plugins": { + "traffic-split": { + "rules": [ + { + "match": [ + { + "vars": [["arg_x-api-name", "==", "jack"]] + } + ], + "weighted_upstreams": [ + {"upstream_id": 1, "weight": 1}, + {"upstream_id": 2, "weight": 1} + ] + } + ] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]=] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 49: `match` rule passed(only use upstream_id) +--- config +location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local bodys = {} + for i = 1, 4 do + local _, _, body = t('/server_port?x-api-name=jack', ngx.HTTP_GET, "", nil, headers) + bodys[i] = body + end + table.sort(bodys) + ngx.say(table.concat(bodys, ", ")) + } +} +--- request +GET /t +--- response_body +1981, 1981, 1982, 1982 +--- no_error_log +[error] + + + +=== TEST 50: use upstream and upstream_id in the plugin +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "uri": "/server_port", + "plugins": { + "traffic-split": { + "rules": [ + { + "match": [ + { + "vars": [["arg_x-api-name", "==", "jack"]] + } + ], + "weighted_upstreams": [ + {"upstream_id": 1, "weight": 2}, + {"upstream": {"type": "roundrobin", "nodes": [{"host":"127.0.0.1", "port":1982, "weight": 1}]}, "weight": 1}, + {"weight": 2} + ] + } + ] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]=] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 51: `match` rule passed(upstream + upstream_id) +--- config +location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local bodys = {} + local headers = {} + headers["x-api-appkey"] = "hello" + for i = 1, 5 do + local _, _, body = t('/server_port?x-api-name=jack', ngx.HTTP_GET, "", nil, headers) + bodys[i] = body + end + table.sort(bodys) + ngx.say(table.concat(bodys, ", ")) + } +} +--- request +GET /t +--- response_body +1980, 1980, 1981, 1981, 1982 +--- no_error_log +[error] + + + +=== TEST 52: set route + upstream (two upstream node: one healthy + one unhealthy) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/upstreams/1', + ngx.HTTP_PUT, + [[{ + "type": "roundrobin", + "nodes": { + "127.0.0.1:1981": 1, + "127.0.0.1:1970": 1 + }, + "checks": { + "active": { + "http_path": "/status", + "host": "foo.com", + "healthy": { + "interval": 1, + "successes": 1 + }, + "unhealthy": { + "interval": 1, + "http_failures": 2 + } + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "uri": "/server_port", + "plugins": { + "traffic-split": { + "rules": [ + { + "weighted_upstreams": [ + {"upstream_id": 1, "weight": 1} + ] + } + ] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]=] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 53: hit routes, ensure the checker is bound to the upstream +--- config +location /t { + content_by_lua_block { + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/server_port" + + do + local httpc = http.new() + local res, err = httpc:request_uri(uri, {method = "GET", keepalive = false}) + end + + ngx.sleep(2.5) + + local ports_count = {} + for i = 1, 6 do + local httpc = http.new() + local res, err = httpc:request_uri(uri, {method = "GET", keepalive = false}) + if not res then + ngx.say(err) + return + end + + ports_count[res.body] = (ports_count[res.body] or 0) + 1 + end + + local ports_arr = {} + for port, count in pairs(ports_count) do + table.insert(ports_arr, {port = port, count = count}) + end + + local function cmd(a, b) + return a.port > b.port + end + table.sort(ports_arr, cmd) + + ngx.say(require("toolkit.json").encode(ports_arr)) + ngx.exit(200) + } +} +--- request +GET /t +--- response_body +[{"count":6,"port":"1981"}] +--- grep_error_log eval +qr/\([^)]+\) unhealthy .* for '.*'/ +--- grep_error_log_out +(upstream#/apisix/upstreams/1) unhealthy TCP increment (1/2) for 'foo.com(127.0.0.1:1970)' +(upstream#/apisix/upstreams/1) unhealthy TCP increment (2/2) for 'foo.com(127.0.0.1:1970)' +--- timeout: 10 + + + +=== TEST 54: set upstream(id: 1), by default retries count = number of nodes +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/upstreams/1', + ngx.HTTP_PUT, + [[{ + "nodes": { + "127.0.0.1:1": 1, + "127.0.0.2:1": 1, + "127.0.0.3:1": 1 + }, + "type": "roundrobin" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 55: set route(id: 1, upstream_id: 1) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "uri": "/hello", + "plugins": { + "traffic-split": { + "rules": [ + { + "weighted_upstreams": [ + {"upstream_id": 1, "weight": 1} + ] + } + ] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]=] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 56: hit routes +--- request +GET /hello +--- error_code: 502 +--- grep_error_log eval +qr/\([^)]+\) while connecting to upstream/ +--- grep_error_log_out +(111: Connection refused) while connecting to upstream +(111: Connection refused) while connecting to upstream +(111: Connection refused) while connecting to upstream