You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by GitBox <gi...@apache.org> on 2020/12/07 01:27:46 UTC

[GitHub] [apisix] Firstsawyou opened a new pull request #2935: feat: Implement traffic splitting plugin

Firstsawyou opened a new pull request #2935:
URL: https://github.com/apache/apisix/pull/2935


   close #2303  close #2603
   
   ### What this PR does / why we need it:
   <!--- Why is this change required? What problem does it solve? -->
   <!--- If it fixes an open issue, please link to the issue here. -->
   
   Implement a traffic splitting plugin, the plugin is named `traffic-split`. The main function of the plug-in is to divide the request traffic according to the specified ratio and divert it to the corresponding "upstream". The functions of `Gray release`, `Blue-green release` and `Custom release` can be realized through this plugin.
   
   Related issue:
   [https://github.com/apache/apisix/issues/2303 ](https://github.com/apache/apisix/issues/2303)
   
   [https://github.com/apache/apisix/issues/2603 ](https://github.com/apache/apisix/issues/2603)
   
   Mailing list discussion address:
   https://lists.apache.org/thread.html/rf02dc53a4af5d98d2513d89256b47466934d129af06d0bdcdb49cc8e%40%3Cdev.apisix.apache.org%3E 
   
   ### Pre-submission checklist:
   
   * [x] Did you explain what problem does this PR solve? Or what new features have been added?
   * [ ] Have you added corresponding test cases?
   * [ ] Have you modified the corresponding document?
   * [ ] Is this PR backward compatible? **If it is not backward compatible, please discuss on the [mailing list](https://github.com/apache/apisix/tree/master#community) first**
   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548665974



##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,413 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [测试插件](#测试插件)
+  - [灰度测试](#灰度测试)
+  - [蓝绿测试](#蓝绿测试)
+  - [自定义测试](#自定义测试)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+请求流量分割插件,对流量按指定的比例划分,并将其分流到对应的 upstream ;通过该插件可以实现灰度发布、蓝绿发布和自定义发布功能。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+在插件的 weighted_upstreams 中只有 `weight` 值,表示到达默认 `route` 上的 upstream 流量权重值。
+
+```json
+{
+    "weight": 2
+}
+```
+

Review comment:
       updated.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548021337



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,

Review comment:
       I think this does not affect the results of our choice of different upstream. Here wrr is used as the picker the same as the picker in upstream nodes. They are able to work normally.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] spacewander commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
spacewander commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548525337



##########
File path: t/plugin/traffic-split.t
##########
@@ -0,0 +1,1289 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+log_level("info");
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: schema validation passed
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.traffic-split")
+            local ok, err = plugin.check_schema({
+                rules = {
+                    {
+                        match = {
+                            {
+                                vars = {
+                                    {"arg_name", "==", "jack"},
+                                    {"arg_age", "!", "<", "16"}
+                                }
+                            },
+                             {
+                                vars = {
+                                    {"arg_name", "==", "rose"},
+                                    {"arg_age", "!", ">", "32"}
+                                }
+                            }
+                        },
+                        weighted_upstreams = {
+                            {
+                                upstream = {
+                                    name = "upstream_A",
+                                    type = "roundrobin",
+                                    nodes = {["127.0.0.1:1981"]=2},
+                                    timeout = {connect = 15, send = 15, read = 15}
+                                },
+                                weight = 2
+                            },
+                            {
+                                upstream = {
+                                    name = "upstream_B",
+                                    type = "roundrobin",
+                                    nodes = {["127.0.0.1:1982"]=2},
+                                    timeout = {connect = 15, send = 15, read = 15}
+                                },
+                                weight = 2
+                            },
+                            {
+                                weight = 1
+                            }
+                        }
+                    }
+                }
+            })
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say("done")
+        }
+    }
+--- request
+GET /t
+--- response_body
+done
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: schema validation passed, and `match` configuration is missing
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.traffic-split")
+            local ok, err = plugin.check_schema({
+                rules = {
+                    {
+                        weighted_upstreams = {
+                            {
+                                upstream = {
+                                    name = "upstream_A",
+                                    type = "roundrobin",
+                                    nodes = {["127.0.0.1:1981"]=2},
+                                    timeout = {connect = 15, send = 15, read = 15}
+                                },
+                                weight = 2
+                            },
+                            {
+                                weight = 1
+                            }
+                        }
+                    }
+                }
+            })
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say("done")
+        }
+    }
+--- request
+GET /t
+--- response_body
+done
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: schema validation failed, `vars` expression operator type is wrong
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.traffic-split")
+            local ok, err = plugin.check_schema({
+                rules = {
+                    {
+                        match = {
+                            {
+                                vars = {
+                                    {"arg_name", 123, "jack"}
+                                }
+                            }
+                        },
+                        weighted_upstreams = {
+                            {
+                                upstream = {
+                                    name = "upstream_A",
+                                    type = "roundrobin",
+                                    nodes = {["127.0.0.1:1981"]=2},
+                                    timeout = {connect = 15, send = 15, read = 15}
+                                },
+                                weight = 2
+                            },
+                            {
+                                weight = 1
+                            }
+                        }
+                    }
+                }
+            })
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say("done")
+        }
+    }
+--- request
+GET /t
+--- response_body eval
+qr/property "rules" validation failed:.* failed to validate item 2: wrong type: expected string, got number/
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: missing `rules` configuration, the upstream of the default `route` takes effect
+--- 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": {}
+                    },
+                    "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 5: the upstream of the default `route` takes effect
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 6 do
+            local _, _, body = t('/server_port', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1980, 1980, 1980, 1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 6: when `weighted_upstreams` is empty, the upstream of `route` is used by default
+--- 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": [
+                                {
+                                    "weighted_upstreams": [{}]
+                                }
+                            ]
+                        }
+                    },
+                    "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 7: the upstream of the default `route` takes effect
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 6 do
+            local _, _, body = t('/server_port', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1980, 1980, 1980, 1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 8: single `vars` expression and single plugin `upstream`, and the upstream traffic on `route` accounts for 1/3
+--- 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_name", "==", "jack"],["arg_age", "!","<", "16"]]
+                                        }
+                                    ],
+                                    "weighted_upstreams": [
+                                        {
+                                           "upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": {"127.0.0.1:1981":2}, "timeout": {"connect": 15, "send": 15, "read": 15}},
+                                            "weight": 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 9: expression validation failed, return to the default `route` upstream port `1980`
+--- request
+GET /server_port?name=jack&age=14
+--- response_body eval
+1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 10: the expression passes and initiated multiple requests, the upstream traffic of `route` accounts for 1/3, and the upstream traffic of plugins accounts for 2/3
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 6 do
+            local _, _, body = t('/server_port?name=jack&age=16', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1981, 1981, 1981, 1981
+--- no_error_log
+[error]
+
+
+
+=== TEST 11: Multiple vars rules and multiple plugin upstream
+--- 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_name", "==", "jack"], ["arg_age", "~~", "^[1-9]{1,2}"]]},
+                                        {"vars": [["arg_name2", "in", ["jack", "rose"]], ["arg_age2", "!", "<", 18]]}
+                                    ],
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": {"127.0.0.1:1981":20}}, "weight": 2},
+                                        {"upstream": {"name": "upstream_B", "type": "roundrobin", "nodes": {"127.0.0.1:1982":10}}, "weight": 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 12: expression validation failed, return to the default `route` upstream port `1980`
+--- request
+GET /server_port?name=jack&age=0
+--- response_body eval
+1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 13: the expression passes and initiated multiple requests, the upstream traffic of `route` accounts for 1/5, and the upstream traffic of plugins accounts for 4/5
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 5 do
+            local _, _, body = t('/server_port?name=jack&age=22', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1981, 1981, 1982, 1982
+--- no_error_log
+[error]
+
+
+
+=== TEST 14: Multiple vars rules and multiple plugin upstream, do not split traffic to the upstream of `route`
+--- 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_name", "==", "jack"], ["arg_age", "~~", "^[1-9]{1,2}"]]},
+                                        {"vars": [["arg_name2", "in", ["jack", "rose"]], ["arg_age2", "!", "<", 18]]}
+                                    ],
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": {"127.0.0.1:1981":20}}, "weight": 2},
+                                        {"upstream": {"name": "upstream_B", "type": "roundrobin", "nodes": {"127.0.0.1:1982":10}}, "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 15: the expression passes and initiated multiple requests, do not split traffic to the upstream of `route`
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 6 do
+            local _, _, body = t('/server_port?name=jack&age=22', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1981, 1981, 1981, 1982, 1982, 1982
+--- no_error_log
+[error]
+
+
+
+=== TEST 16: support multiple ip configuration of `nodes`, and missing upstream configuration on `route`
+--- 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_name", "==", "jack"], ["arg_age", "~~", "^[1-9]{1,2}"]]
+                                        }
+                                    ],
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": {"127.0.0.1:1980":1, "127.0.0.1:1981":2, "127.0.0.1:1982":2}, "timeout": {"connect": 15, "send": 15, "read": 15}}, "weight": 1}
+                                    ]
+                                }
+                            ]
+                        }
+                    }
+                }]=]
+            )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 17: the expression passes and initiated multiple requests, roundrobin the ip of nodes
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 5 do
+            local _, _, body = t('/server_port?name=jack&age=22', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1981, 1981, 1982, 1982
+--- no_error_log
+
+
+
+=== TEST 18: host is domain name
+--- 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": [
+                                {
+                                    "weighted_upstreams": [
+                                        {
+                                            "upstream": {
+                                                "name": "upstream_A",
+                                                "type": "roundrobin",
+                                                "nodes": {
+                                                    "foo.com:80": 0
+                                                }
+                                            },
+                                            "weight": 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 19: domain name resolved successfully
+--- request
+GET /server_port
+--- error_code: 502
+--- error_log eval
+qr/dns resolver domain: foo.com to \d+.\d+.\d+.\d+/
+
+
+
+=== TEST 20: mock Grayscale Release
+--- 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": [
+                                {
+                                    "weighted_upstreams": [
+                                        {
+                                            "upstream": {
+                                                "name": "upstream_A",
+                                                "type": "roundrobin",
+                                                "nodes": {
+                                                    "127.0.0.1:1981":1
+                                                }
+                                            },
+                                            "weight": 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 21: 2/3 request traffic hits the upstream of the plugin, 1/3 request traffic hits the upstream of `route`
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 6 do
+            local _, _, body = t('/server_port', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1981, 1981, 1981, 1981
+--- no_error_log
+[error]
+
+
+
+=== TEST 22: mock Blue-green Release
+--- 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": [["http_release","==","blue"]]
+                                        }
+                                    ],
+                                    "weighted_upstreams": [
+                                        {
+                                            "upstream": {
+                                                "name": "upstream_A",
+                                                "type": "roundrobin",
+                                                "nodes": {
+                                                    "127.0.0.1:1981":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 23: release is equal to `blue`
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        local headers = {}
+        headers["release"] = "blue"
+        for i = 1, 6 do
+            local _, _, body = t('/server_port', ngx.HTTP_GET, "", nil, headers)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1981, 1981, 1981, 1981, 1981, 1981
+--- no_error_log
+[error]
+
+
+
+=== TEST 24: release is equal to `green`
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        local headers = {}
+        headers["release"] = "green"
+        for i = 1, 6 do
+            local _, _, body = t('/server_port', ngx.HTTP_GET, "", nil, headers)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1980, 1980, 1980, 1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 25: mock Custom Release
+--- 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_name", "==", "jack"], ["arg_age", ">", "23"],["http_appkey", "~~", "[a-z]{1,5}"]]
+                                        }
+                                    ],
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": {"127.0.0.1:1981":20}}, "weight": 2},
+                                        {"upstream": {"name": "upstream_B", "type": "roundrobin", "nodes": {"127.0.0.1:1982":10}}, "weight": 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 26: `match` rule passed
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 5 do
+            local _, _, body = t('/server_port?name=jack&age=36', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1981, 1981, 1982, 1982
+--- no_error_log
+[error]
+
+
+
+=== TEST 27: `match` rule failed, `age` condition did not match
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        local headers = {}
+        headers["release"] = "green"
+        for i = 1, 6 do
+            local _, _, body = t('/server_port?name=jack&age=16', ngx.HTTP_GET, "", nil, headers)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1980, 1980, 1980, 1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 28: upstream nodes are array type and node is the domain name
+--- 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": [
+                                {
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": [{"host":"foo.com", "port": 80, "weight": 0}]}, "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 29: domain name resolved successfully
+--- request
+GET /server_port
+--- error_code: 502
+--- error_log eval
+qr/dns resolver domain: foo.com to \d+.\d+.\d+.\d+/
+
+
+
+=== TEST 30: the nodes of upstream are array type, with multiple nodes
+--- 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_name", "==", "jack"], ["arg_age", ">", "23"],["http_appkey", "~~", "[a-z]{1,5}"]]
+                                        }
+                                    ],
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": [{"host":"127.0.0.1", "port":1981, "weight": 2}, {"host":"127.0.0.1", "port":1982, "weight": 2}]}, "weight": 4},
+                                        {"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 31: `match` rule passed
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 5 do
+            local _, _, body = t('/server_port?name=jack&age=36', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1981, 1981, 1982, 1982
+--- no_error_log
+[error]
+
+
+
+=== TEST 32: the upstream node is an array type and has multiple upstream
+--- 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_name", "==", "jack"], ["arg_age", ">", "23"],["http_appkey", "~~", "[a-z]{1,5}"]]
+                                        }
+                                    ],
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": [{"host":"127.0.0.1", "port":1981, "weight": 2}]}, "weight": 2},
+                                        {"upstream": {"name": "upstream_B", "type": "roundrobin", "nodes": [{"host":"127.0.0.1", "port":1982, "weight": 2}]}, "weight": 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 33: `match` rule passed
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 5 do
+            local _, _, body = t('/server_port?name=jack&age=36', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1981, 1981, 1982, 1982
+--- no_error_log
+[error]
+
+
+
+=== TEST 34: multi-upstream, test with unique upstream key
+--- 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": [
+                                {
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": [{"host":"127.0.0.1", "port":1981, "weight": 2}]}, "weight": 2},
+                                        {"upstream": {"name": "upstream_B", "type": "roundrobin", "nodes": [{"host":"127.0.0.1", "port":1982, "weight": 2}]}, "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 35: the upstream `key` is unique
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 2 do
+            local _, _, body = t('/server_port', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1981, 1982
+--- grep_error_log eval
+qr/upstream_key: roundrobin#route_1_\d/
+--- grep_error_log_out
+upstream_key: roundrobin#route_1_1
+upstream_key: roundrobin#route_1_2
+--- no_error_log
+[error]
+--- LAST

Review comment:
       Should remove the mark




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548807680



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: The ratio between each upstream may not so accurate since the drawback of weighted round robin algorithm (especially when the wrr state is reset).
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For specific usage of operators, please see the `operator-list` part of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). |
+| rules.weighted_upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.weighted_upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream(not currently supported).            |
+| rules.weighted_upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.weighted_upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.weighted_upstreams.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. |
+| rules.weighted_upstreams.upstream.timeout  | object | optional    |  15     |   | Set the timeout period for connecting, sending and receiving messages (time unit: second, all default to 15 seconds).  |
+| rules.weighted_upstreams.upstream.pass_host | enum | optional    | "pass"  | ["pass", "node", "rewrite"]  | pass: pass the host requested by the client, node: pass the host requested by the client; use the host configured with the upstream node, rewrite: rewrite the host with the value configured by the upstream_host. |
+| rules.weighted_upstreams.upstream.name      | string | optional    |        |   | Identify the upstream service name, usage scenario, etc.  |
+| rules.weighted_upstreams.upstream.upstream_host | string | optional    |    |   | Only valid when pass_host is configured as rewrite.    |
+| rules.weighted_upstreams.weight | integer | optional    | weight = 1   |  | The traffic is divided according to the `weight` value, and the roundrobin algorithm is used to divide multiple `weight`. |
+
+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 plug-in will be guided, otherwise, all traffic will be directly Reach the `upstream` configured on `route` or `service`. Of course, you can also configure only the `weighted_upstreams` part, which will directly guide the traffic ratio between each upstream in the plugin based on the `weight` value in `weighted_upstreams`.
+
+>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. There is only a `weight` value in the weighted_upstreams of the plug-in, which means reaching the upstream traffic weight value configured on `route` or `service`. Such as:
+
+```json
+{
+    "weight": 2
+}
+```
+
+## How To Enable
+
+The following provides examples of plugin usage, which will help you understand the use of plugin.

Review comment:
       Note the title is "How to enable", not "Example".




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548793999



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: The ratio between each upstream may not so accurate since the drawback of weighted round robin algorithm (especially when the wrr state is reset).
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For specific usage of operators, please see the `operator-list` part of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). |
+| rules.weighted_upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.weighted_upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream(not currently supported).            |
+| rules.weighted_upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.weighted_upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.weighted_upstreams.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. |
+| rules.weighted_upstreams.upstream.timeout  | object | optional    |  15     |   | Set the timeout period for connecting, sending and receiving messages (time unit: second, all default to 15 seconds).  |
+| rules.weighted_upstreams.upstream.pass_host | enum | optional    | "pass"  | ["pass", "node", "rewrite"]  | pass: pass the host requested by the client, node: pass the host requested by the client; use the host configured with the upstream node, rewrite: rewrite the host with the value configured by the upstream_host. |
+| rules.weighted_upstreams.upstream.name      | string | optional    |        |   | Identify the upstream service name, usage scenario, etc.  |
+| rules.weighted_upstreams.upstream.upstream_host | string | optional    |    |   | Only valid when pass_host is configured as rewrite.    |
+| rules.weighted_upstreams.weight | integer | optional    | weight = 1   |  | The traffic is divided according to the `weight` value, and the roundrobin algorithm is used to divide multiple `weight`. |
+
+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 plug-in will be guided, otherwise, all traffic will be directly Reach the `upstream` configured on `route` or `service`. Of course, you can also configure only the `weighted_upstreams` part, which will directly guide the traffic ratio between each upstream in the plugin based on the `weight` value in `weighted_upstreams`.
+
+>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. There is only a `weight` value in the weighted_upstreams of the plug-in, which means reaching the upstream traffic weight value configured on `route` or `service`. Such as:
+
+```json
+{
+    "weight": 2
+}
+```
+
+## How To Enable
+
+The following provides examples of plugin usage, which will help you understand the use of plugin.

Review comment:
       Here you just need to add the necessary steps to enable this plugin.
   
   The `Grayscale`, `BlueGreenVersion` is some features we can implement resorting to this plugin, it should't be written inside `How to Enable`, they are Best Practices.

##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: The ratio between each upstream may not so accurate since the drawback of weighted round robin algorithm (especially when the wrr state is reset).
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For specific usage of operators, please see the `operator-list` part of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). |
+| rules.weighted_upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.weighted_upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream(not currently supported).            |
+| rules.weighted_upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.weighted_upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.weighted_upstreams.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. |
+| rules.weighted_upstreams.upstream.timeout  | object | optional    |  15     |   | Set the timeout period for connecting, sending and receiving messages (time unit: second, all default to 15 seconds).  |
+| rules.weighted_upstreams.upstream.pass_host | enum | optional    | "pass"  | ["pass", "node", "rewrite"]  | pass: pass the host requested by the client, node: pass the host requested by the client; use the host configured with the upstream node, rewrite: rewrite the host with the value configured by the upstream_host. |
+| rules.weighted_upstreams.upstream.name      | string | optional    |        |   | Identify the upstream service name, usage scenario, etc.  |
+| rules.weighted_upstreams.upstream.upstream_host | string | optional    |    |   | Only valid when pass_host is configured as rewrite.    |
+| rules.weighted_upstreams.weight | integer | optional    | weight = 1   |  | The traffic is divided according to the `weight` value, and the roundrobin algorithm is used to divide multiple `weight`. |
+
+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 plug-in will be guided, otherwise, all traffic will be directly Reach the `upstream` configured on `route` or `service`. Of course, you can also configure only the `weighted_upstreams` part, which will directly guide the traffic ratio between each upstream in the plugin based on the `weight` value in `weighted_upstreams`.
+
+>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. There is only a `weight` value in the weighted_upstreams of the plug-in, which means reaching the upstream traffic weight value configured on `route` or `service`. Such as:
+
+```json
+{
+    "weight": 2
+}
+```
+
+## How To Enable
+
+The following provides examples of plugin usage, which will help you understand the use of plugin.

Review comment:
       Here you just need to add the necessary steps to enable this plugin.
   
   The `Grayscale`, `BlueGreenVersion` is some features we can implement resorting to this plugin, it should't be written inside `How to Enable`, they are Best Practices.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547773973



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do

Review comment:
       updated.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548804016



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: The ratio between each upstream may not so accurate since the drawback of weighted round robin algorithm (especially when the wrr state is reset).
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For specific usage of operators, please see the `operator-list` part of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). |
+| rules.weighted_upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.weighted_upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream(not currently supported).            |
+| rules.weighted_upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.weighted_upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.weighted_upstreams.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. |
+| rules.weighted_upstreams.upstream.timeout  | object | optional    |  15     |   | Set the timeout period for connecting, sending and receiving messages (time unit: second, all default to 15 seconds).  |
+| rules.weighted_upstreams.upstream.pass_host | enum | optional    | "pass"  | ["pass", "node", "rewrite"]  | pass: pass the host requested by the client, node: pass the host requested by the client; use the host configured with the upstream node, rewrite: rewrite the host with the value configured by the upstream_host. |
+| rules.weighted_upstreams.upstream.name      | string | optional    |        |   | Identify the upstream service name, usage scenario, etc.  |
+| rules.weighted_upstreams.upstream.upstream_host | string | optional    |    |   | Only valid when pass_host is configured as rewrite.    |
+| rules.weighted_upstreams.weight | integer | optional    | weight = 1   |  | The traffic is divided according to the `weight` value, and the roundrobin algorithm is used to divide multiple `weight`. |
+
+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 plug-in will be guided, otherwise, all traffic will be directly Reach the `upstream` configured on `route` or `service`. Of course, you can also configure only the `weighted_upstreams` part, which will directly guide the traffic ratio between each upstream in the plugin based on the `weight` value in `weighted_upstreams`.
+
+>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. There is only a `weight` value in the weighted_upstreams of the plug-in, which means reaching the upstream traffic weight value configured on `route` or `service`. Such as:
+
+```json
+{
+    "weight": 2
+}
+```
+
+## How To Enable
+
+The following provides examples of plugin usage, which will help you understand the use of plugin.

Review comment:
       I think it is appropriate to write the grayscale version and the blue-green version of the example here, because it includes the basic usage scenarios of this plugin. This shows the usage details of the plugin.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] spacewander commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
spacewander commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r539168215



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,293 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+  - [**Name**](#name)
+  - [**Attributes**](#attributes)
+  - [**How To Enable**](#how-to-enable)
+    - [**Grayscale Release**](#grayscale-release)
+    - [**Blue-green Release**](#blue-green-release)
+    - [**Custom Release**](#custom-release)
+  - [**Test Plugin**](#test-plugin)
+    - [**Grayscale Test**](#grayscale-test)
+    - [**Blue-green Test**](#blue-green-test)
+    - [**Custom Test**](#custom-test)
+  - [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic splitting plug-in divides the request traffic according to a specified ratio and diverts it to the corresponding upstream. The plug-in can realize the functions of gray release, blue-green release and custom release.
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For specific usage of operators, please see the `operator-list` part of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). |
+| rules.upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream(not currently supported).            |
+| rules.upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.upstreams.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. |
+| rules.upstreams.upstream.timeout  | object | optional    |  15     |   | Set the timeout period for connecting, sending and receiving messages (time unit: second, all default to 15 seconds).  |
+| rules.upstreams.upstream.pass_host | enum | optional    | "pass"  | ["pass", "node", "rewrite"]  | pass: pass the host requested by the client, node: pass the host requested by the client; use the host configured with the upstream node, rewrite: rewrite the host with the value configured by the upstream_host. |
+| rules.upstreams.upstream.name      | string | optional    |        |   | Identify the upstream service name, usage scenario, etc.  |
+| rules.upstreams.upstream.upstream_host | string | optional    |    |   | Only valid when pass_host is configured as rewrite.    |
+| rules.upstreams.weight | integer | optional    | weight = 1   |  | The traffic is divided according to the weight value, and the roundrobin algorithm is used to divide multiple weights. |
+
+## How To Enable
+
+### Grayscale Release
+
+Traffic is split according to the weight value configured by upstreams in the plugin (the rule of `match` is not configured, and `match` is passed by default). The request traffic is divided into 4:2, 2/3 of the traffic reaches the upstream of the `1981` port in the plugin, and 1/3 of the traffic reaches the upstream of the default `1980` port on the route.
+
+```json
+{
+    "weight": 2
+}
+```
+
+There is only a `weight` value in the plugin upstreams, which represents the weight value of the upstream traffic arriving on the route.
+
+```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": [
+                {
+                    "upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                },
+                                "timeout": {
+                                    "connect": 15,
+                                    "send": 15,
+                                    "read": 15
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### Blue-green Release
+
+Get the blue and green conditions through the request header (you can also get through the request parameters or NGINX variables). After the `match` rule is matched, it means that all requests hit the upstream configured by the plugin, otherwise the request only hits the configuration on the route 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["http_new-release","==","blue"]
+                            ]
+                        }
+                    ],
+                    "upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### Custom Release
+
+Multiple matching rules can be set in `match` (multiple conditions in `vars` are the relationship of `add`, and the relationship between multiple `vars` rules is the relationship of `or`; as long as one of the vars rules passes, it means `match` passed), only one is configured here, and the traffic is divided into 4:2 according to the value of `weight`. Among them, only the `weight` part represents the proportion of upstream on the route. When `match` fails to match, all traffic will only hit upstream on the route.

Review comment:
       I think we need more examples for the `and` / `or` relations.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547771484



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,

Review comment:
       I have tried the uuid method, but this will result in a situation where all requests go to only one upstream.
   
   ```lua
   local uuid = core.id.gen_uuid_v4()
   upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id .. uuid,
                ctx.conf_version, up_conf, matched_route)
   ```
   
   ```
   got: '1982, 1982, 1982, 1982, 1982
   # '
   #     expected: '1980, 1981, 1981, 1982, 1982
   ```




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] spacewander commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
spacewander commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548464858



##########
File path: t/plugin/traffic-split.t
##########
@@ -1150,3 +1144,75 @@ GET /t
 1980, 1981, 1981, 1982, 1982
 --- no_error_log
 [error]
+
+
+
+=== TEST 34: multiple upstream

Review comment:
       Better to add a test for "multiple upstream + empty_upstream is used"




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] spacewander commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
spacewander commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547059643



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,

Review comment:
       Yes, you are right. We should provide an unique key for the generated upstream.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] wfgydbu commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
wfgydbu commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-744114244


   Hi all, thansk for this great feature! One quick question, In this plugin, how do you deal with healthcheck and
   retry mechanism? are you planning to support it?
   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547879847



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,310 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_pass_host(ctx, upstream_info, host)
+    -- Currently only supports a single upstream of the domain name.
+    -- When the upstream is `IP`, do not do any `pass_host` operation.
+    if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then

Review comment:
       updated.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548790153



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: Since the selection of different upstream in the plugin is based on the roundrobin algorithm, the ratio of traffic distribution is not completely accurate when the algorithm state is reset.
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For specific usage of operators, please see the `operator-list` part of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). |
+| rules.weighted_upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.weighted_upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream(not currently supported).            |
+| rules.weighted_upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.weighted_upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.weighted_upstreams.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. |
+| rules.weighted_upstreams.upstream.timeout  | object | optional    |  15     |   | Set the timeout period for connecting, sending and receiving messages (time unit: second, all default to 15 seconds).  |
+| rules.weighted_upstreams.upstream.pass_host | enum | optional    | "pass"  | ["pass", "node", "rewrite"]  | pass: pass the host requested by the client, node: pass the host requested by the client; use the host configured with the upstream node, rewrite: rewrite the host with the value configured by the upstream_host. |
+| rules.weighted_upstreams.upstream.name      | string | optional    |        |   | Identify the upstream service name, usage scenario, etc.  |
+| rules.weighted_upstreams.upstream.upstream_host | string | optional    |    |   | Only valid when pass_host is configured as rewrite.    |
+| rules.weighted_upstreams.weight | integer | optional    | weight = 1   |  | The traffic is divided according to the `weight` value, and the roundrobin algorithm is used to divide multiple `weight`. |
+
+## How To Enable

Review comment:
       updated.

##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,420 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+traffic-split 插件使用户可以逐步引导各个上游之间的流量百分比。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+traffic-split 插件主要由 `match` 和 `weighted_upstreams` 两部分组成,`match` 是自定义的条件规则,`weighted_upstreams` 是 upstream 的信息。在使用插件时,至少需要配置 `weighted_upstreams` 部分,这样将默认 `match` 规则通过,会根据 `weighted_upstreams` 中的 `weight` 值,逐步引导各个 upstream 之间的流量比例。你也可以同时配置 `match` 和 `weighted_upstreams`,这样只有 `match` 规则匹配通过后,才会对 `weighted_upstreams` 中的流量进行划分。

Review comment:
       updated.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] moonming commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
moonming commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-748955765


   is this PR still active?


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547938095



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,314 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node)
+       and not ipmatcher.parse_ipv6(node)
+    then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_pass_host(ctx, upstream_info, host)
+    -- Currently only supports a single upstream of the domain name.
+    -- When the upstream is `IP`, do not do any `pass_host` operation.
+    if not core.utils.parse_ipv4(host)
+       and not core.utils.parse_ipv6(host)
+    then
+        local pass_host = upstream_info.pass_host or "pass"
+        if pass_host == "pass" then
+            ctx.var.upstream_host = ctx.var.host
+            return
+        end
+
+        if pass_host == "rewrite" then
+            ctx.var.upstream_host = upstream_info.upstream_host
+            return
+        end
+
+        ctx.var.upstream_host = host
+        return
+    end
+
+    return
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    if core.table.isarray(nodes) then
+        for _, node in ipairs(nodes) do
+            set_pass_host(ctx, upstream_info, node.host)
+            node.host = parse_domain_for_node(node.host)
+            node.port = node.port
+            node.weight = node.weight
+            table_insert(new_nodes, node)
+        end
+    else
+        for addr, weight in pairs(nodes) do
+            local node = {}
+            local ip, port, host
+            host, port = core.utils.parse_addr(addr)
+            set_pass_host(ctx, upstream_info, host)
+            ip = parse_domain_for_node(host)
+            node.host = ip
+            node.port = port
+            node.weight = weight
+            table_insert(new_nodes, node)
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(upstreams) do
+        if not upstream_obj.upstream then
+            -- If the `upstream` object has only the `weighted_upstream` 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
+        server_list[upstream_obj.upstream] = upstream_obj.weighted_upstream
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in ipairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then

Review comment:
       The design logic here is: the relationship of `or` between multiple `vars` in `match`. Therefore, when there is a `vars` match that is `true`, it can no longer match.
   For example, in the following example: When the first `vars` matches `true`, then the second `vars` does not need to match.
   
   ```lua
   "match": [
         {
             "vars": [
                 ["arg_name","==","jack"],
                 ["http_user-id",">","23"],
                 ["http_apisix-key","~~","[a-z]+"]
             ],
             "vars": [
                 ["arg_name2","==","rose"],
                 ["http_user-id2","!",">","33"],
                 ["http_apisix-key2","~~","[a-z]+"]
             ]
         }
     ]
   ```
   
   




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] spacewander commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
spacewander commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547752819



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,

Review comment:
       What about generating a UUID as a suffix? We don't need to reuse in this occasion.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-742243058


   > This plugin needs to be mentioned on the project README and the readme of the plugins.
   
   Yes i will update later.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r539157910



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",

Review comment:
       Style: keep the double quote sign aligned.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r537535396



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,308 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Test Plugin**](#test-plugin)
+  - [**Grayscale Test**](#grayscale-test)
+  - [**Blue-green Test**](#blue-green-test)
+  - [**Custom Test**](#custom-test)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic splitting plug-in divides the request traffic according to a specified ratio and diverts it to the corresponding upstream. The plug-in can realize the functions of gray release, blue-green release and custom release.
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For the specific usage of operators, please see the section `Operator List` below. |

Review comment:
       Ok i will update later.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548809598



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: The ratio between each upstream may not so accurate since the drawback of weighted round robin algorithm (especially when the wrr state is reset).
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For specific usage of operators, please see the `operator-list` part of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). |
+| rules.weighted_upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.weighted_upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream(not currently supported).            |
+| rules.weighted_upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.weighted_upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.weighted_upstreams.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. |
+| rules.weighted_upstreams.upstream.timeout  | object | optional    |  15     |   | Set the timeout period for connecting, sending and receiving messages (time unit: second, all default to 15 seconds).  |
+| rules.weighted_upstreams.upstream.pass_host | enum | optional    | "pass"  | ["pass", "node", "rewrite"]  | pass: pass the host requested by the client, node: pass the host requested by the client; use the host configured with the upstream node, rewrite: rewrite the host with the value configured by the upstream_host. |
+| rules.weighted_upstreams.upstream.name      | string | optional    |        |   | Identify the upstream service name, usage scenario, etc.  |
+| rules.weighted_upstreams.upstream.upstream_host | string | optional    |    |   | Only valid when pass_host is configured as rewrite.    |
+| rules.weighted_upstreams.weight | integer | optional    | weight = 1   |  | The traffic is divided according to the `weight` value, and the roundrobin algorithm is used to divide multiple `weight`. |
+
+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 plug-in will be guided, otherwise, all traffic will be directly Reach the `upstream` configured on `route` or `service`. Of course, you can also configure only the `weighted_upstreams` part, which will directly guide the traffic ratio between each upstream in the plugin based on the `weight` value in `weighted_upstreams`.
+
+>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. There is only a `weight` value in the weighted_upstreams of the plug-in, which means reaching the upstream traffic weight value configured on `route` or `service`. Such as:
+
+```json
+{
+    "weight": 2
+}
+```
+
+## How To Enable
+
+The following provides examples of plugin usage, which will help you understand the use of plugin.

Review comment:
       "How to enable" has been changed to "Example". ^ _ ^




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548554835



##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,413 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [测试插件](#测试插件)
+  - [灰度测试](#灰度测试)
+  - [蓝绿测试](#蓝绿测试)
+  - [自定义测试](#自定义测试)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+请求流量分割插件,对流量按指定的比例划分,并将其分流到对应的 upstream ;通过该插件可以实现灰度发布、蓝绿发布和自定义发布功能。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+在插件的 weighted_upstreams 中只有 `weight` 值,表示到达默认 `route` 上的 upstream 流量权重值。
+
+```json
+{
+    "weight": 2
+}
+```
+
+### 灰度发布
+
+根据插件中 weighted_upstreams 配置的 `weight` 值做流量分流(不配置 `match` 的规则,已经默认 `match` 通过)。将请求流量按 4:2 划分,2/3 的流量到达插件中的 `1981` 端口上游, 1/3 的流量到达 route 上默认的 `1980` 端口上游。

Review comment:
       Let me update the document again, it is a bit confusing to understand




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547627744



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,

Review comment:
       @spacewander 
   I tried it, but couldn't find a good way to generate unique keys, because in the plug-in, except for the nodes information of upstream, the other configuration information is the same.
   Do you have any good suggestions for this problem?




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548526363



##########
File path: t/plugin/traffic-split.t
##########
@@ -0,0 +1,1289 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+log_level("info");
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: schema validation passed
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.traffic-split")
+            local ok, err = plugin.check_schema({
+                rules = {
+                    {
+                        match = {
+                            {
+                                vars = {
+                                    {"arg_name", "==", "jack"},
+                                    {"arg_age", "!", "<", "16"}
+                                }
+                            },
+                             {
+                                vars = {
+                                    {"arg_name", "==", "rose"},
+                                    {"arg_age", "!", ">", "32"}
+                                }
+                            }
+                        },
+                        weighted_upstreams = {
+                            {
+                                upstream = {
+                                    name = "upstream_A",
+                                    type = "roundrobin",
+                                    nodes = {["127.0.0.1:1981"]=2},
+                                    timeout = {connect = 15, send = 15, read = 15}
+                                },
+                                weight = 2
+                            },
+                            {
+                                upstream = {
+                                    name = "upstream_B",
+                                    type = "roundrobin",
+                                    nodes = {["127.0.0.1:1982"]=2},
+                                    timeout = {connect = 15, send = 15, read = 15}
+                                },
+                                weight = 2
+                            },
+                            {
+                                weight = 1
+                            }
+                        }
+                    }
+                }
+            })
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say("done")
+        }
+    }
+--- request
+GET /t
+--- response_body
+done
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: schema validation passed, and `match` configuration is missing
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.traffic-split")
+            local ok, err = plugin.check_schema({
+                rules = {
+                    {
+                        weighted_upstreams = {
+                            {
+                                upstream = {
+                                    name = "upstream_A",
+                                    type = "roundrobin",
+                                    nodes = {["127.0.0.1:1981"]=2},
+                                    timeout = {connect = 15, send = 15, read = 15}
+                                },
+                                weight = 2
+                            },
+                            {
+                                weight = 1
+                            }
+                        }
+                    }
+                }
+            })
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say("done")
+        }
+    }
+--- request
+GET /t
+--- response_body
+done
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: schema validation failed, `vars` expression operator type is wrong
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.traffic-split")
+            local ok, err = plugin.check_schema({
+                rules = {
+                    {
+                        match = {
+                            {
+                                vars = {
+                                    {"arg_name", 123, "jack"}
+                                }
+                            }
+                        },
+                        weighted_upstreams = {
+                            {
+                                upstream = {
+                                    name = "upstream_A",
+                                    type = "roundrobin",
+                                    nodes = {["127.0.0.1:1981"]=2},
+                                    timeout = {connect = 15, send = 15, read = 15}
+                                },
+                                weight = 2
+                            },
+                            {
+                                weight = 1
+                            }
+                        }
+                    }
+                }
+            })
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say("done")
+        }
+    }
+--- request
+GET /t
+--- response_body eval
+qr/property "rules" validation failed:.* failed to validate item 2: wrong type: expected string, got number/
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: missing `rules` configuration, the upstream of the default `route` takes effect
+--- 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": {}
+                    },
+                    "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 5: the upstream of the default `route` takes effect
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 6 do
+            local _, _, body = t('/server_port', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1980, 1980, 1980, 1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 6: when `weighted_upstreams` is empty, the upstream of `route` is used by default
+--- 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": [
+                                {
+                                    "weighted_upstreams": [{}]
+                                }
+                            ]
+                        }
+                    },
+                    "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 7: the upstream of the default `route` takes effect
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 6 do
+            local _, _, body = t('/server_port', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1980, 1980, 1980, 1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 8: single `vars` expression and single plugin `upstream`, and the upstream traffic on `route` accounts for 1/3
+--- 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_name", "==", "jack"],["arg_age", "!","<", "16"]]
+                                        }
+                                    ],
+                                    "weighted_upstreams": [
+                                        {
+                                           "upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": {"127.0.0.1:1981":2}, "timeout": {"connect": 15, "send": 15, "read": 15}},
+                                            "weight": 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 9: expression validation failed, return to the default `route` upstream port `1980`
+--- request
+GET /server_port?name=jack&age=14
+--- response_body eval
+1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 10: the expression passes and initiated multiple requests, the upstream traffic of `route` accounts for 1/3, and the upstream traffic of plugins accounts for 2/3
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 6 do
+            local _, _, body = t('/server_port?name=jack&age=16', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1981, 1981, 1981, 1981
+--- no_error_log
+[error]
+
+
+
+=== TEST 11: Multiple vars rules and multiple plugin upstream
+--- 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_name", "==", "jack"], ["arg_age", "~~", "^[1-9]{1,2}"]]},
+                                        {"vars": [["arg_name2", "in", ["jack", "rose"]], ["arg_age2", "!", "<", 18]]}
+                                    ],
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": {"127.0.0.1:1981":20}}, "weight": 2},
+                                        {"upstream": {"name": "upstream_B", "type": "roundrobin", "nodes": {"127.0.0.1:1982":10}}, "weight": 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 12: expression validation failed, return to the default `route` upstream port `1980`
+--- request
+GET /server_port?name=jack&age=0
+--- response_body eval
+1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 13: the expression passes and initiated multiple requests, the upstream traffic of `route` accounts for 1/5, and the upstream traffic of plugins accounts for 4/5
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 5 do
+            local _, _, body = t('/server_port?name=jack&age=22', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1981, 1981, 1982, 1982
+--- no_error_log
+[error]
+
+
+
+=== TEST 14: Multiple vars rules and multiple plugin upstream, do not split traffic to the upstream of `route`
+--- 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_name", "==", "jack"], ["arg_age", "~~", "^[1-9]{1,2}"]]},
+                                        {"vars": [["arg_name2", "in", ["jack", "rose"]], ["arg_age2", "!", "<", 18]]}
+                                    ],
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": {"127.0.0.1:1981":20}}, "weight": 2},
+                                        {"upstream": {"name": "upstream_B", "type": "roundrobin", "nodes": {"127.0.0.1:1982":10}}, "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 15: the expression passes and initiated multiple requests, do not split traffic to the upstream of `route`
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 6 do
+            local _, _, body = t('/server_port?name=jack&age=22', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1981, 1981, 1981, 1982, 1982, 1982
+--- no_error_log
+[error]
+
+
+
+=== TEST 16: support multiple ip configuration of `nodes`, and missing upstream configuration on `route`
+--- 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_name", "==", "jack"], ["arg_age", "~~", "^[1-9]{1,2}"]]
+                                        }
+                                    ],
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": {"127.0.0.1:1980":1, "127.0.0.1:1981":2, "127.0.0.1:1982":2}, "timeout": {"connect": 15, "send": 15, "read": 15}}, "weight": 1}
+                                    ]
+                                }
+                            ]
+                        }
+                    }
+                }]=]
+            )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 17: the expression passes and initiated multiple requests, roundrobin the ip of nodes
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 5 do
+            local _, _, body = t('/server_port?name=jack&age=22', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1981, 1981, 1982, 1982
+--- no_error_log
+
+
+
+=== TEST 18: host is domain name
+--- 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": [
+                                {
+                                    "weighted_upstreams": [
+                                        {
+                                            "upstream": {
+                                                "name": "upstream_A",
+                                                "type": "roundrobin",
+                                                "nodes": {
+                                                    "foo.com:80": 0
+                                                }
+                                            },
+                                            "weight": 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 19: domain name resolved successfully
+--- request
+GET /server_port
+--- error_code: 502
+--- error_log eval
+qr/dns resolver domain: foo.com to \d+.\d+.\d+.\d+/
+
+
+
+=== TEST 20: mock Grayscale Release
+--- 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": [
+                                {
+                                    "weighted_upstreams": [
+                                        {
+                                            "upstream": {
+                                                "name": "upstream_A",
+                                                "type": "roundrobin",
+                                                "nodes": {
+                                                    "127.0.0.1:1981":1
+                                                }
+                                            },
+                                            "weight": 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 21: 2/3 request traffic hits the upstream of the plugin, 1/3 request traffic hits the upstream of `route`
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 6 do
+            local _, _, body = t('/server_port', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1981, 1981, 1981, 1981
+--- no_error_log
+[error]
+
+
+
+=== TEST 22: mock Blue-green Release
+--- 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": [["http_release","==","blue"]]
+                                        }
+                                    ],
+                                    "weighted_upstreams": [
+                                        {
+                                            "upstream": {
+                                                "name": "upstream_A",
+                                                "type": "roundrobin",
+                                                "nodes": {
+                                                    "127.0.0.1:1981":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 23: release is equal to `blue`
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        local headers = {}
+        headers["release"] = "blue"
+        for i = 1, 6 do
+            local _, _, body = t('/server_port', ngx.HTTP_GET, "", nil, headers)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1981, 1981, 1981, 1981, 1981, 1981
+--- no_error_log
+[error]
+
+
+
+=== TEST 24: release is equal to `green`
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        local headers = {}
+        headers["release"] = "green"
+        for i = 1, 6 do
+            local _, _, body = t('/server_port', ngx.HTTP_GET, "", nil, headers)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1980, 1980, 1980, 1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 25: mock Custom Release
+--- 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_name", "==", "jack"], ["arg_age", ">", "23"],["http_appkey", "~~", "[a-z]{1,5}"]]
+                                        }
+                                    ],
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": {"127.0.0.1:1981":20}}, "weight": 2},
+                                        {"upstream": {"name": "upstream_B", "type": "roundrobin", "nodes": {"127.0.0.1:1982":10}}, "weight": 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 26: `match` rule passed
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 5 do
+            local _, _, body = t('/server_port?name=jack&age=36', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1981, 1981, 1982, 1982
+--- no_error_log
+[error]
+
+
+
+=== TEST 27: `match` rule failed, `age` condition did not match
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        local headers = {}
+        headers["release"] = "green"
+        for i = 1, 6 do
+            local _, _, body = t('/server_port?name=jack&age=16', ngx.HTTP_GET, "", nil, headers)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1980, 1980, 1980, 1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 28: upstream nodes are array type and node is the domain name
+--- 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": [
+                                {
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": [{"host":"foo.com", "port": 80, "weight": 0}]}, "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 29: domain name resolved successfully
+--- request
+GET /server_port
+--- error_code: 502
+--- error_log eval
+qr/dns resolver domain: foo.com to \d+.\d+.\d+.\d+/
+
+
+
+=== TEST 30: the nodes of upstream are array type, with multiple nodes
+--- 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_name", "==", "jack"], ["arg_age", ">", "23"],["http_appkey", "~~", "[a-z]{1,5}"]]
+                                        }
+                                    ],
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": [{"host":"127.0.0.1", "port":1981, "weight": 2}, {"host":"127.0.0.1", "port":1982, "weight": 2}]}, "weight": 4},
+                                        {"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 31: `match` rule passed
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 5 do
+            local _, _, body = t('/server_port?name=jack&age=36', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1981, 1981, 1982, 1982
+--- no_error_log
+[error]
+
+
+
+=== TEST 32: the upstream node is an array type and has multiple upstream
+--- 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_name", "==", "jack"], ["arg_age", ">", "23"],["http_appkey", "~~", "[a-z]{1,5}"]]
+                                        }
+                                    ],
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": [{"host":"127.0.0.1", "port":1981, "weight": 2}]}, "weight": 2},
+                                        {"upstream": {"name": "upstream_B", "type": "roundrobin", "nodes": [{"host":"127.0.0.1", "port":1982, "weight": 2}]}, "weight": 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 33: `match` rule passed
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 5 do
+            local _, _, body = t('/server_port?name=jack&age=36', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1981, 1981, 1982, 1982
+--- no_error_log
+[error]
+
+
+
+=== TEST 34: multi-upstream, test with unique upstream key
+--- 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": [
+                                {
+                                    "weighted_upstreams": [
+                                        {"upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": [{"host":"127.0.0.1", "port":1981, "weight": 2}]}, "weight": 2},
+                                        {"upstream": {"name": "upstream_B", "type": "roundrobin", "nodes": [{"host":"127.0.0.1", "port":1982, "weight": 2}]}, "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 35: the upstream `key` is unique
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 2 do
+            local _, _, body = t('/server_port', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1981, 1982
+--- grep_error_log eval
+qr/upstream_key: roundrobin#route_1_\d/
+--- grep_error_log_out
+upstream_key: roundrobin#route_1_1
+upstream_key: roundrobin#route_1_2
+--- no_error_log
+[error]
+--- LAST

Review comment:
       fixed.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] moonming commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
moonming commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548771731



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: Since the selection of different upstream in the plugin is based on the roundrobin algorithm, the ratio of traffic distribution is not completely accurate when the algorithm state is reset.

Review comment:
       Can not understand 

##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: Since the selection of different upstream in the plugin is based on the roundrobin algorithm, the ratio of traffic distribution is not completely accurate when the algorithm state is reset.
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For specific usage of operators, please see the `operator-list` part of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). |
+| rules.weighted_upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.weighted_upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream(not currently supported).            |
+| rules.weighted_upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.weighted_upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.weighted_upstreams.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. |
+| rules.weighted_upstreams.upstream.timeout  | object | optional    |  15     |   | Set the timeout period for connecting, sending and receiving messages (time unit: second, all default to 15 seconds).  |
+| rules.weighted_upstreams.upstream.pass_host | enum | optional    | "pass"  | ["pass", "node", "rewrite"]  | pass: pass the host requested by the client, node: pass the host requested by the client; use the host configured with the upstream node, rewrite: rewrite the host with the value configured by the upstream_host. |
+| rules.weighted_upstreams.upstream.name      | string | optional    |        |   | Identify the upstream service name, usage scenario, etc.  |
+| rules.weighted_upstreams.upstream.upstream_host | string | optional    |    |   | Only valid when pass_host is configured as rewrite.    |
+| rules.weighted_upstreams.weight | integer | optional    | weight = 1   |  | The traffic is divided according to the `weight` value, and the roundrobin algorithm is used to divide multiple `weight`. |
+
+## How To Enable

Review comment:
       The content is nothing with how to enable 

##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,420 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+traffic-split 插件使用户可以逐步引导各个上游之间的流量百分比。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+traffic-split 插件主要由 `match` 和 `weighted_upstreams` 两部分组成,`match` 是自定义的条件规则,`weighted_upstreams` 是 upstream 的信息。在使用插件时,至少需要配置 `weighted_upstreams` 部分,这样将默认 `match` 规则通过,会根据 `weighted_upstreams` 中的 `weight` 值,逐步引导各个 upstream 之间的流量比例。你也可以同时配置 `match` 和 `weighted_upstreams`,这样只有 `match` 规则匹配通过后,才会对 `weighted_upstreams` 中的流量进行划分。

Review comment:
       The grammar is not clear

##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,420 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+traffic-split 插件使用户可以逐步引导各个上游之间的流量百分比。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+traffic-split 插件主要由 `match` 和 `weighted_upstreams` 两部分组成,`match` 是自定义的条件规则,`weighted_upstreams` 是 upstream 的信息。在使用插件时,至少需要配置 `weighted_upstreams` 部分,这样将默认 `match` 规则通过,会根据 `weighted_upstreams` 中的 `weight` 值,逐步引导各个 upstream 之间的流量比例。你也可以同时配置 `match` 和 `weighted_upstreams`,这样只有 `match` 规则匹配通过后,才会对 `weighted_upstreams` 中的流量进行划分。
+
+>注:1、在 `match` 里,vars 中的表达式是 `and` 的关系,多个 `vars` 之间是 `or` 的关系。2、在插件的 weighted_upstreams 中只有 `weight` 值,表示到达默认 `route` 上的 upstream 流量权重值。如:
+
+```json
+{
+    "weight": 2
+}
+```
+
+下面提供插件的使用示例,这将有助于你对插件使用上的理解。
+
+### 灰度发布
+
+不配置 `match` 规则部分(已经默认 `match` 通过),根据插件中 weighted_upstreams 配置的 `weight` 值做流量分流。将 `插件 upstream` 与 `route 的 upstream` 请求流量按 3:2 进行划分,其中 60% 的流量到达插件中的 `1981` 端口的 upstream, 40% 的流量到达 route 上默认 `1980` 端口的 upstream。

Review comment:
       ditto




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] spacewander commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
spacewander commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547976741



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,

Review comment:
       Look like it is because the wrr is used as the picker? If we use random, this isn't a problem. Since the weight and node are changed per request, it is not safe to reuse the picker.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547065857



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,

Review comment:
       Yes, thank you very much for your comments.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548666294



##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,413 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [测试插件](#测试插件)
+  - [灰度测试](#灰度测试)
+  - [蓝绿测试](#蓝绿测试)
+  - [自定义测试](#自定义测试)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+请求流量分割插件,对流量按指定的比例划分,并将其分流到对应的 upstream ;通过该插件可以实现灰度发布、蓝绿发布和自定义发布功能。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+在插件的 weighted_upstreams 中只有 `weight` 值,表示到达默认 `route` 上的 upstream 流量权重值。
+
+```json
+{
+    "weight": 2
+}
+```
+
+### 灰度发布
+
+根据插件中 weighted_upstreams 配置的 `weight` 值做流量分流(不配置 `match` 的规则,已经默认 `match` 通过)。将请求流量按 4:2 划分,2/3 的流量到达插件中的 `1981` 端口上游, 1/3 的流量到达 route 上默认的 `1980` 端口上游。
+
+```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": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                },
+                                "timeout": {
+                                    "connect": 15,
+                                    "send": 15,
+                                    "read": 15
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### 蓝绿发布
+
+通过请求头获取蓝绿条件(也可以通过请求参数获取或NGINX变量),在 `match` 规则匹配通过后,表示所有请求都命中到插件配置的 upstream ,否则所以请求只命中 `route` 上配置的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["http_new-release","==","blue"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### 自定义发布
+
+`match` 中可以设置多个匹配规则,`vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 规则之间是 `or` 的关系;只要其中一个 vars 规则通过,则表示 `match` 通过。
+
+示例1:只配置了一个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系。根据 `weight` 值将流量按 4:2 划分。其中只有 `weight` 部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+插件设置了请求的匹配规则并设置端口为`1981`的 upstream,route 上具有端口为`1980`的upstream。
+
+示例2:配置多个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 之间是 `and` 的关系。根据 `weight` 值将流量按 4:2 划分。其中只有 `weight` 部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ],
+                            "vars": [
+                                ["arg_name2","==","rose"],
+                                ["http_user-id2","!",">","33"],
+                                ["http_apisix-key2","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+插件设置了请求的匹配规则并设置端口为`1981`的 upstream,route 上具有端口为`1980`的upstream。
+
+## 测试插件
+
+### 灰度测试

Review comment:
       updated.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-741654491


   @Firstsawyou Please resolve the conflicts.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] spacewander commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
spacewander commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r537446150



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,308 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Test Plugin**](#test-plugin)
+  - [**Grayscale Test**](#grayscale-test)
+  - [**Blue-green Test**](#blue-green-test)
+  - [**Custom Test**](#custom-test)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic splitting plug-in divides the request traffic according to a specified ratio and diverts it to the corresponding upstream. The plug-in can realize the functions of gray release, blue-green release and custom release.
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For the specific usage of operators, please see the section `Operator List` below. |
+| rules.upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream.            |
+| rules.upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.upstreams.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. |
+| rules.upstreams.upstream.timeout  | object | optional    |        |   | Set the timeout period for connecting, sending and receiving messages.  |
+| rules.upstreams.upstream.enable_websocket      | boolean | optional    |        |   | Whether to enable websocket, it is not enabled by default.  |

Review comment:
       For fields don't use by this plugin, we should leave a link to the upstream resource section of admin-api doc.

##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,308 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Test Plugin**](#test-plugin)
+  - [**Grayscale Test**](#grayscale-test)
+  - [**Blue-green Test**](#blue-green-test)
+  - [**Custom Test**](#custom-test)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic splitting plug-in divides the request traffic according to a specified ratio and diverts it to the corresponding upstream. The plug-in can realize the functions of gray release, blue-green release and custom release.
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For the specific usage of operators, please see the section `Operator List` below. |

Review comment:
       We should just leave a link to lua-resty-expr, instead of copying the `Operator List`

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,311 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain(host)

Review comment:
       Better to refactor `apisix/init.lua` to take this function out, instead of copying its code.

##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,308 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Test Plugin**](#test-plugin)
+  - [**Grayscale Test**](#grayscale-test)
+  - [**Blue-green Test**](#blue-green-test)
+  - [**Custom Test**](#custom-test)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic splitting plug-in divides the request traffic according to a specified ratio and diverts it to the corresponding upstream. The plug-in can realize the functions of gray release, blue-green release and custom release.
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For the specific usage of operators, please see the section `Operator List` below. |
+| rules.upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream.            |
+| rules.upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.upstreams.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. |
+| rules.upstreams.upstream.timeout  | object | optional    |        |   | Set the timeout period for connecting, sending and receiving messages.  |

Review comment:
       Should mention the default timeout value

##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,308 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Test Plugin**](#test-plugin)
+  - [**Grayscale Test**](#grayscale-test)
+  - [**Blue-green Test**](#blue-green-test)
+  - [**Custom Test**](#custom-test)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic splitting plug-in divides the request traffic according to a specified ratio and diverts it to the corresponding upstream. The plug-in can realize the functions of gray release, blue-green release and custom release.
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For the specific usage of operators, please see the section `Operator List` below. |
+| rules.upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream.            |
+| rules.upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.upstreams.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. |
+| rules.upstreams.upstream.timeout  | object | optional    |        |   | Set the timeout period for connecting, sending and receiving messages.  |
+| rules.upstreams.upstream.enable_websocket      | boolean | optional    |        |   | Whether to enable websocket, it is not enabled by default.  |
+| rules.upstreams.upstream.pass_host | enum | optional    | "pass"  | ["pass", "node", "rewrite"]  | pass: pass the host requested by the client, node: pass the host requested by the client; use the host configured with the upstream node, rewrite: rewrite the host with the value configured by the upstream_host. |
+| rules.upstreams.upstream.name      | string | optional    |        |   | Identify the upstream service name, usage scenario, etc.  |
+| rules.upstreams.upstream.upstream_host | string | optional    |    |   | Only valid when pass_host is configured as rewrite.    |
+| rules.upstreams.weight | integer | optional    | weight = 1   |  | According to the weight value to do traffic segmentation. The roundrobin algorithm is used by default, and currently only the roundrobin algorithm is supported.    |
+
+### Operator List
+
+|operator|         description             |      example                        |
+|--------|---------------------------------|-------------------------------------|
+|==      |equal                            |{"arg_name", "==", "json"}           |
+|~=      |not equal                        |{"arg_name", "~=", "json"}           |
+|>       |greater than                     | {"arg_age", ">", 24}                |
+|<       |less than                        |{"arg_age", "<", 24}                 |
+|~~      |Regular match                    |{"arg_name", "~~", "[a-z]+"}         |
+|~*      |Case insensitive regular match   |{"arg_name", "~*", "[a-z]+"}         |
+|in      |find in array                    |{"arg_name", "in", {"1","2"}}        |
+|has     |left value array has value in the right |{"graphql_root_fields", "has", "repo"}|
+|!       |reverse the result               |{"arg_name", "!", "~~", "[a-z]+"}    |
+
+## How To Enable
+
+### Grayscale Release
+
+Traffic is split according to the weight value configured by upstreams in the plugin (the rule of `match` is not configured, and `match` is passed by default). The request traffic is divided into 4:2, 2/3 of the traffic reaches the upstream of the `1981` port in the plugin, and 1/3 of the traffic reaches the upstream of the default `1980` port on the route.
+
+```json
+{
+    "weight": 2
+}
+```
+
+There is only a `weight` value in the plugin upstreams, which represents the weight value of the upstream traffic arriving on the route.
+
+```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": [
+                {
+                    "upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                },
+                                "timeout": {
+                                    "connect": 15,
+                                    "send": 15,
+                                    "read": 15
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### Blue-green Release
+
+Get the blue and green conditions through the request header (you can also get through the request parameters or NGINX variables). After the `match` rule is matched, it means that all requests hit the upstream configured by the plugin, otherwise the request only hits the configuration on the route 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["http_new-release","==","blue"]
+                            ]
+                        }
+                    ],
+                    "upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### Custom Release
+
+Multiple matching rules can be set in `match` (multiple conditions in `vars` are the relationship of `add`, and the relationship between multiple `vars` rules is the relationship of `or`; as long as one of the vars rules passes, it means `match` passed), only one is configured here, and the traffic is divided into 4:2 according to the value of `weight`. Among them, only the `weight` part represents the proportion of upstream on the route. When `match` fails to match, all traffic will only hit upstream on the route.
+
+```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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+The plug-in sets the request matching rules and sets the port to upstream with `1981`, and the route has upstream with port `1980`.
+
+## Test Plugin
+
+### Grayscale Test
+
+**2/3 of the requests hit the upstream on port 1981, and 1/3 of the traffic hit the upstream on port 1980.**
+
+```shell
+$ curl http://127.0.0.1:9080/index.html -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+hello 1980
+
+$ curl http://127.0.0.1:9080/index.html -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+world 1981
+```
+
+### Blue-green Test
+
+```shell
+$ curl http://127.0.0.1:9080/index.html?name=jack -H 'new-release: blue' -i

Review comment:
       We should wrap the uri with `''`, otherwise `?` will be interpreted as bash syntax




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-737622657


   @Firstsawyou CI failed, please fix it.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-750000976


   > Hi all, thansk for this great feature! One quick question, In this plugin, how do you deal with healthcheck and
   > retry mechanism? are you planning to support it?
   
   Hi @wfgydbu ,
   I am very sorry. Regarding the health check and retry mechanism, I plan to implement it in the next version, which is currently not supported. I have established a related issue to follow up on this. You can continue to follow the progress of this issue.
   issue:  [https://github.com/apache/apisix/issues/3094 ](https://github.com/apache/apisix/issues/3094)


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] spacewander commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
spacewander commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r539173573



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then

Review comment:
       Yes. It is expected to use `and`.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548446270



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,314 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node)
+       and not ipmatcher.parse_ipv6(node)
+    then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_pass_host(ctx, upstream_info, host)
+    -- Currently only supports a single upstream of the domain name.
+    -- When the upstream is `IP`, do not do any `pass_host` operation.
+    if not core.utils.parse_ipv4(host)
+       and not core.utils.parse_ipv6(host)
+    then
+        local pass_host = upstream_info.pass_host or "pass"
+        if pass_host == "pass" then
+            ctx.var.upstream_host = ctx.var.host
+            return
+        end
+
+        if pass_host == "rewrite" then
+            ctx.var.upstream_host = upstream_info.upstream_host
+            return
+        end
+
+        ctx.var.upstream_host = host
+        return
+    end
+
+    return
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    if core.table.isarray(nodes) then
+        for _, node in ipairs(nodes) do
+            set_pass_host(ctx, upstream_info, node.host)
+            node.host = parse_domain_for_node(node.host)
+            node.port = node.port
+            node.weight = node.weight
+            table_insert(new_nodes, node)
+        end
+    else
+        for addr, weight in pairs(nodes) do
+            local node = {}
+            local ip, port, host
+            host, port = core.utils.parse_addr(addr)
+            set_pass_host(ctx, upstream_info, host)
+            ip = parse_domain_for_node(host)
+            node.host = ip
+            node.port = port
+            node.weight = weight
+            table_insert(new_nodes, node)
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(upstreams) do
+        if not upstream_obj.upstream then
+            -- If the `upstream` object has only the `weighted_upstream` 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
+        server_list[upstream_obj.upstream] = upstream_obj.weighted_upstream
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in ipairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then
+                break
+            end
+        end
+
+        if match_flag then
+            upstreams = rule.upstreams
+            break
+        end
+    end
+
+    core.log.info("match_flag: ", match_flag)
+
+    if not match_flag then
+        return
+    end
+
+    local rr_up, err = lrucache(upstreams, nil, new_rr_obj, upstreams)
+    if not rr_up then
+        core.log.error("lrucache roundrobin failed: ", err)
+        return 500
+    end
+
+    local upstream = rr_up:find()

Review comment:
       I set up a related `issue` to record this issue, let’s look at the community’s feedback first. Then make adjustments.
   issue:[https://github.com/apache/apisix/issues/3118 ](https://github.com/apache/apisix/issues/3118)




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548513789



##########
File path: conf/config-default.yaml
##########
@@ -246,6 +246,7 @@ plugins:                          # plugin list (sorted in alphabetical order)
   - wolf-rbac
   - zipkin
   # - server-info
+  - traffic-split

Review comment:
       This plugin does not involve changes to the core code, I think we can enable.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548521831



##########
File path: t/plugin/traffic-split.t
##########
@@ -1150,3 +1144,75 @@ GET /t
 1980, 1981, 1981, 1982, 1982
 --- no_error_log
 [error]
+
+
+
+=== TEST 34: multiple upstream

Review comment:
       updated.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] membphis commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
membphis commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547862478



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,310 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_pass_host(ctx, upstream_info, host)
+    -- Currently only supports a single upstream of the domain name.
+    -- When the upstream is `IP`, do not do any `pass_host` operation.
+    if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then

Review comment:
       bad style too




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-739533103


   There was a problem when this `pr` handled the conflict. A related `pr` has been newly submitted.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou closed pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou closed pull request #2935:
URL: https://github.com/apache/apisix/pull/2935


   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-748970021


   > is this PR still active?
   
   Not active anymore.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] moonming commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
moonming commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548546765



##########
File path: doc/README.md
##########
@@ -81,6 +81,7 @@
 * [request-validation](plugins/request-validation.md): Validates requests before forwarding to upstream.
 * [proxy-mirror](plugins/proxy-mirror.md): Provides the ability to mirror client requests.
 * [api-breaker](plugins/api-breaker.md): Circuit Breaker for API that stops requests forwarding to upstream in case of unhealthy state.
+* [traffic-split](plugins/traffic-split.md): The traffic split plugin divides the request traffic according to the specified ratio and diverts it to the corresponding upstream; through this plug-in, gray-scale publishing, blue-green publishing and custom publishing functions can be realized.

Review comment:
       I think you should keep the desc as readme.md

##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,413 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [测试插件](#测试插件)
+  - [灰度测试](#灰度测试)
+  - [蓝绿测试](#蓝绿测试)
+  - [自定义测试](#自定义测试)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+请求流量分割插件,对流量按指定的比例划分,并将其分流到对应的 upstream ;通过该插件可以实现灰度发布、蓝绿发布和自定义发布功能。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+在插件的 weighted_upstreams 中只有 `weight` 值,表示到达默认 `route` 上的 upstream 流量权重值。
+
+```json
+{
+    "weight": 2
+}
+```
+
+### 灰度发布
+
+根据插件中 weighted_upstreams 配置的 `weight` 值做流量分流(不配置 `match` 的规则,已经默认 `match` 通过)。将请求流量按 4:2 划分,2/3 的流量到达插件中的 `1981` 端口上游, 1/3 的流量到达 route 上默认的 `1980` 端口上游。

Review comment:
       What is "4:2" and 1/3?

##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,413 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+  - [**Name**](#name)
+  - [**Attributes**](#attributes)
+  - [**How To Enable**](#how-to-enable)
+    - [**Grayscale Release**](#grayscale-release)
+    - [**Blue-green Release**](#blue-green-release)
+    - [**Custom Release**](#custom-release)
+  - [**Test Plugin**](#test-plugin)
+    - [**Grayscale Test**](#grayscale-test)
+    - [**Blue-green Test**](#blue-green-test)
+    - [**Custom Test**](#custom-test)
+  - [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin divides the request traffic according to the specified ratio and diverts it to the corresponding upstream; through this plugin, gray-scale publishing, blue-green publishing and custom publishing functions can be realized.

Review comment:
       ditto

##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,413 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [测试插件](#测试插件)
+  - [灰度测试](#灰度测试)
+  - [蓝绿测试](#蓝绿测试)
+  - [自定义测试](#自定义测试)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+请求流量分割插件,对流量按指定的比例划分,并将其分流到对应的 upstream ;通过该插件可以实现灰度发布、蓝绿发布和自定义发布功能。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+在插件的 weighted_upstreams 中只有 `weight` 值,表示到达默认 `route` 上的 upstream 流量权重值。
+
+```json
+{
+    "weight": 2
+}
+```
+

Review comment:
       I still don't how to enable the plugin 

##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,413 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [测试插件](#测试插件)
+  - [灰度测试](#灰度测试)
+  - [蓝绿测试](#蓝绿测试)
+  - [自定义测试](#自定义测试)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+请求流量分割插件,对流量按指定的比例划分,并将其分流到对应的 upstream ;通过该插件可以实现灰度发布、蓝绿发布和自定义发布功能。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+在插件的 weighted_upstreams 中只有 `weight` 值,表示到达默认 `route` 上的 upstream 流量权重值。
+
+```json
+{
+    "weight": 2
+}
+```
+
+### 灰度发布
+
+根据插件中 weighted_upstreams 配置的 `weight` 值做流量分流(不配置 `match` 的规则,已经默认 `match` 通过)。将请求流量按 4:2 划分,2/3 的流量到达插件中的 `1981` 端口上游, 1/3 的流量到达 route 上默认的 `1980` 端口上游。
+
+```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": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                },
+                                "timeout": {
+                                    "connect": 15,
+                                    "send": 15,
+                                    "read": 15
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### 蓝绿发布
+
+通过请求头获取蓝绿条件(也可以通过请求参数获取或NGINX变量),在 `match` 规则匹配通过后,表示所有请求都命中到插件配置的 upstream ,否则所以请求只命中 `route` 上配置的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["http_new-release","==","blue"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### 自定义发布
+
+`match` 中可以设置多个匹配规则,`vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 规则之间是 `or` 的关系;只要其中一个 vars 规则通过,则表示 `match` 通过。
+
+示例1:只配置了一个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系。根据 `weight` 值将流量按 4:2 划分。其中只有 `weight` 部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+插件设置了请求的匹配规则并设置端口为`1981`的 upstream,route 上具有端口为`1980`的upstream。
+
+示例2:配置多个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 之间是 `and` 的关系。根据 `weight` 值将流量按 4:2 划分。其中只有 `weight` 部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ],
+                            "vars": [
+                                ["arg_name2","==","rose"],
+                                ["http_user-id2","!",">","33"],
+                                ["http_apisix-key2","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+插件设置了请求的匹配规则并设置端口为`1981`的 upstream,route 上具有端口为`1980`的upstream。
+
+## 测试插件
+
+### 灰度测试

Review comment:
       Why are configuration and testing so far apart?  Hard to understand

##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,413 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [测试插件](#测试插件)
+  - [灰度测试](#灰度测试)
+  - [蓝绿测试](#蓝绿测试)
+  - [自定义测试](#自定义测试)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+请求流量分割插件,对流量按指定的比例划分,并将其分流到对应的 upstream ;通过该插件可以实现灰度发布、蓝绿发布和自定义发布功能。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+在插件的 weighted_upstreams 中只有 `weight` 值,表示到达默认 `route` 上的 upstream 流量权重值。
+
+```json
+{
+    "weight": 2
+}
+```
+
+### 灰度发布
+
+根据插件中 weighted_upstreams 配置的 `weight` 值做流量分流(不配置 `match` 的规则,已经默认 `match` 通过)。将请求流量按 4:2 划分,2/3 的流量到达插件中的 `1981` 端口上游, 1/3 的流量到达 route 上默认的 `1980` 端口上游。
+
+```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": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                },
+                                "timeout": {
+                                    "connect": 15,
+                                    "send": 15,
+                                    "read": 15
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### 蓝绿发布
+
+通过请求头获取蓝绿条件(也可以通过请求参数获取或NGINX变量),在 `match` 规则匹配通过后,表示所有请求都命中到插件配置的 upstream ,否则所以请求只命中 `route` 上配置的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["http_new-release","==","blue"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### 自定义发布
+
+`match` 中可以设置多个匹配规则,`vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 规则之间是 `or` 的关系;只要其中一个 vars 规则通过,则表示 `match` 通过。
+
+示例1:只配置了一个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系。根据 `weight` 值将流量按 4:2 划分。其中只有 `weight` 部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+插件设置了请求的匹配规则并设置端口为`1981`的 upstream,route 上具有端口为`1980`的upstream。
+
+示例2:配置多个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 之间是 `and` 的关系。根据 `weight` 值将流量按 4:2 划分。其中只有 `weight` 部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ],
+                            "vars": [
+                                ["arg_name2","==","rose"],
+                                ["http_user-id2","!",">","33"],
+                                ["http_apisix-key2","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+插件设置了请求的匹配规则并设置端口为`1981`的 upstream,route 上具有端口为`1980`的upstream。
+
+## 测试插件
+
+### 灰度测试
+
+**2/3 的请求命中到1981端口的upstream, 1/3 的流量命中到1980端口的upstream。**
+
+```shell
+$ curl http://127.0.0.1:9080/index.html -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+hello 1980
+
+$ curl http://127.0.0.1:9080/index.html -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+world 1981
+```
+
+### 蓝绿测试
+
+```shell
+$ curl 'http://127.0.0.1:9080/index.html?name=jack' -H 'new-release: blue' -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+world 1981
+```
+
+当 `match` 匹配通过后,所有请求都命中到插件配置的 `upstream`,否则命中 `route` 上配置的 upstream 。
+
+### 自定义测试
+
+**示例1:**
+
+**在`match` 规则校验通过后, 2/3 的请求命中到1981端口的upstream, 1/3 命中到1980端口的upstream。**

Review comment:
       Ditto

##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,413 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [测试插件](#测试插件)
+  - [灰度测试](#灰度测试)
+  - [蓝绿测试](#蓝绿测试)
+  - [自定义测试](#自定义测试)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+请求流量分割插件,对流量按指定的比例划分,并将其分流到对应的 upstream ;通过该插件可以实现灰度发布、蓝绿发布和自定义发布功能。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+在插件的 weighted_upstreams 中只有 `weight` 值,表示到达默认 `route` 上的 upstream 流量权重值。
+
+```json
+{
+    "weight": 2
+}
+```
+
+### 灰度发布
+
+根据插件中 weighted_upstreams 配置的 `weight` 值做流量分流(不配置 `match` 的规则,已经默认 `match` 通过)。将请求流量按 4:2 划分,2/3 的流量到达插件中的 `1981` 端口上游, 1/3 的流量到达 route 上默认的 `1980` 端口上游。
+
+```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": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                },
+                                "timeout": {
+                                    "connect": 15,
+                                    "send": 15,
+                                    "read": 15
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### 蓝绿发布
+
+通过请求头获取蓝绿条件(也可以通过请求参数获取或NGINX变量),在 `match` 规则匹配通过后,表示所有请求都命中到插件配置的 upstream ,否则所以请求只命中 `route` 上配置的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["http_new-release","==","blue"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### 自定义发布
+
+`match` 中可以设置多个匹配规则,`vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 规则之间是 `or` 的关系;只要其中一个 vars 规则通过,则表示 `match` 通过。
+
+示例1:只配置了一个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系。根据 `weight` 值将流量按 4:2 划分。其中只有 `weight` 部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+插件设置了请求的匹配规则并设置端口为`1981`的 upstream,route 上具有端口为`1980`的upstream。
+
+示例2:配置多个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 之间是 `and` 的关系。根据 `weight` 值将流量按 4:2 划分。其中只有 `weight` 部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ],
+                            "vars": [
+                                ["arg_name2","==","rose"],
+                                ["http_user-id2","!",">","33"],
+                                ["http_apisix-key2","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+插件设置了请求的匹配规则并设置端口为`1981`的 upstream,route 上具有端口为`1980`的upstream。
+
+## 测试插件
+
+### 灰度测试
+
+**2/3 的请求命中到1981端口的upstream, 1/3 的流量命中到1980端口的upstream。**
+
+```shell
+$ curl http://127.0.0.1:9080/index.html -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+hello 1980
+
+$ curl http://127.0.0.1:9080/index.html -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+world 1981
+```
+
+### 蓝绿测试

Review comment:
       bad desc

##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,413 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [测试插件](#测试插件)
+  - [灰度测试](#灰度测试)
+  - [蓝绿测试](#蓝绿测试)
+  - [自定义测试](#自定义测试)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+请求流量分割插件,对流量按指定的比例划分,并将其分流到对应的 upstream ;通过该插件可以实现灰度发布、蓝绿发布和自定义发布功能。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+在插件的 weighted_upstreams 中只有 `weight` 值,表示到达默认 `route` 上的 upstream 流量权重值。
+
+```json
+{
+    "weight": 2
+}
+```
+
+### 灰度发布
+
+根据插件中 weighted_upstreams 配置的 `weight` 值做流量分流(不配置 `match` 的规则,已经默认 `match` 通过)。将请求流量按 4:2 划分,2/3 的流量到达插件中的 `1981` 端口上游, 1/3 的流量到达 route 上默认的 `1980` 端口上游。
+
+```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": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                },
+                                "timeout": {
+                                    "connect": 15,
+                                    "send": 15,
+                                    "read": 15
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### 蓝绿发布
+
+通过请求头获取蓝绿条件(也可以通过请求参数获取或NGINX变量),在 `match` 规则匹配通过后,表示所有请求都命中到插件配置的 upstream ,否则所以请求只命中 `route` 上配置的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["http_new-release","==","blue"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### 自定义发布
+
+`match` 中可以设置多个匹配规则,`vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 规则之间是 `or` 的关系;只要其中一个 vars 规则通过,则表示 `match` 通过。
+
+示例1:只配置了一个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系。根据 `weight` 值将流量按 4:2 划分。其中只有 `weight` 部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+插件设置了请求的匹配规则并设置端口为`1981`的 upstream,route 上具有端口为`1980`的upstream。
+
+示例2:配置多个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 之间是 `and` 的关系。根据 `weight` 值将流量按 4:2 划分。其中只有 `weight` 部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ],
+                            "vars": [
+                                ["arg_name2","==","rose"],
+                                ["http_user-id2","!",">","33"],
+                                ["http_apisix-key2","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+插件设置了请求的匹配规则并设置端口为`1981`的 upstream,route 上具有端口为`1980`的upstream。
+
+## 测试插件
+
+### 灰度测试
+
+**2/3 的请求命中到1981端口的upstream, 1/3 的流量命中到1980端口的upstream。**
+
+```shell
+$ curl http://127.0.0.1:9080/index.html -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+hello 1980
+
+$ curl http://127.0.0.1:9080/index.html -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+world 1981
+```
+
+### 蓝绿测试
+
+```shell
+$ curl 'http://127.0.0.1:9080/index.html?name=jack' -H 'new-release: blue' -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+world 1981
+```
+
+当 `match` 匹配通过后,所有请求都命中到插件配置的 `upstream`,否则命中 `route` 上配置的 upstream 。

Review comment:
       where is “match”? I don't know how to use this plugin 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r546706390



##########
File path: README.md
##########
@@ -99,6 +99,7 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against
   - [Health Checks](doc/health-check.md): Enable health check on the upstream node, and will automatically filter unhealthy nodes during load balancing to ensure system stability.
   - Circuit-Breaker: Intelligent tracking of unhealthy upstream services.
   - [Proxy Mirror](doc/plugins/proxy-mirror.md): Provides the ability to mirror client requests.
+  - [Traffic Split](doc/plugins/traffic-split.md): Supports dividing the request traffic according to the specified proportional relationship.

Review comment:
       `Support gray-scale release, blue-green release and custom release function`. Is this description feasible?




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] wfgydbu commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
wfgydbu commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-744116293


   > > Hi all, thansk for this great feature! One quick question, In this plugin, how do you deal with healthcheck and
   > > retry mechanism? are you planning to support it?
   > 
   > This feature will be supported, please pay attention to this PR continuously.
   
   Great! Looking forward to it !


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547771484



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,

Review comment:
       I have tried the uuid method, but this will result in a situation where all requests go to only one upstream.
   
   lua code:
   
   ```lua
   local uuid = core.id.gen_uuid_v4()
   upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id .. uuid,
                ctx.conf_version, up_conf, matched_route)
   ```
   
   Test results are inconsistent with expectations:
   
   ```
   got: '1982, 1982, 1982, 1982, 1982
   # '
   #     expected: '1980, 1981, 1981, 1982, 1982
   ```
   
   When there are multiple ip configurations in the upstream node in the plugin, it seems that the upstream key needs to be reused.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547914974



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,314 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node)
+       and not ipmatcher.parse_ipv6(node)
+    then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_pass_host(ctx, upstream_info, host)
+    -- Currently only supports a single upstream of the domain name.
+    -- When the upstream is `IP`, do not do any `pass_host` operation.
+    if not core.utils.parse_ipv4(host)
+       and not core.utils.parse_ipv6(host)
+    then
+        local pass_host = upstream_info.pass_host or "pass"
+        if pass_host == "pass" then
+            ctx.var.upstream_host = ctx.var.host
+            return
+        end
+
+        if pass_host == "rewrite" then
+            ctx.var.upstream_host = upstream_info.upstream_host
+            return
+        end
+
+        ctx.var.upstream_host = host
+        return
+    end
+
+    return
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    if core.table.isarray(nodes) then
+        for _, node in ipairs(nodes) do
+            set_pass_host(ctx, upstream_info, node.host)
+            node.host = parse_domain_for_node(node.host)
+            node.port = node.port
+            node.weight = node.weight
+            table_insert(new_nodes, node)
+        end
+    else
+        for addr, weight in pairs(nodes) do
+            local node = {}
+            local ip, port, host
+            host, port = core.utils.parse_addr(addr)
+            set_pass_host(ctx, upstream_info, host)
+            ip = parse_domain_for_node(host)
+            node.host = ip
+            node.port = port
+            node.weight = weight
+            table_insert(new_nodes, node)
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(upstreams) do
+        if not upstream_obj.upstream then
+            -- If the `upstream` object has only the `weighted_upstream` 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
+        server_list[upstream_obj.upstream] = upstream_obj.weighted_upstream
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do

Review comment:
       For array-like table, use ipairs.

##########
File path: README.md
##########
@@ -99,6 +99,7 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against
   - [Health Checks](doc/health-check.md): Enable health check on the upstream node, and will automatically filter unhealthy nodes during load balancing to ensure system stability.
   - Circuit-Breaker: Intelligent tracking of unhealthy upstream services.
   - [Proxy Mirror](doc/plugins/proxy-mirror.md): Provides the ability to mirror client requests.
+  - [Traffic Split](doc/plugins/traffic-split.md): Support the functions of gray release, blue-green release and custom release.

Review comment:
       Not so intuitive.
   
   Use: allows users to incrementally direct percentages of traffic between various upstreams.

##########
File path: README_CN.md
##########
@@ -97,6 +97,7 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵
   - [健康检查](doc/zh-cn/health-check.md):启用上游节点的健康检查,将在负载均衡期间自动过滤不健康的节点,以确保系统稳定性。
   - 熔断器: 智能跟踪不健康上游服务。
   - [代理镜像](doc/zh-cn/plugins/proxy-mirror.md): 提供镜像客户端请求的能力。
+  - [流量拆分](doc/zh-cn/plugins/traffic-split.md): 支持灰度发布、蓝绿发布和自定义发布的功能。

Review comment:
       Ditto 

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,314 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node)
+       and not ipmatcher.parse_ipv6(node)
+    then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_pass_host(ctx, upstream_info, host)
+    -- Currently only supports a single upstream of the domain name.
+    -- When the upstream is `IP`, do not do any `pass_host` operation.
+    if not core.utils.parse_ipv4(host)
+       and not core.utils.parse_ipv6(host)
+    then
+        local pass_host = upstream_info.pass_host or "pass"
+        if pass_host == "pass" then
+            ctx.var.upstream_host = ctx.var.host
+            return
+        end
+
+        if pass_host == "rewrite" then
+            ctx.var.upstream_host = upstream_info.upstream_host
+            return
+        end
+
+        ctx.var.upstream_host = host
+        return
+    end
+
+    return
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    if core.table.isarray(nodes) then
+        for _, node in ipairs(nodes) do
+            set_pass_host(ctx, upstream_info, node.host)
+            node.host = parse_domain_for_node(node.host)
+            node.port = node.port
+            node.weight = node.weight
+            table_insert(new_nodes, node)
+        end
+    else
+        for addr, weight in pairs(nodes) do
+            local node = {}
+            local ip, port, host
+            host, port = core.utils.parse_addr(addr)
+            set_pass_host(ctx, upstream_info, host)
+            ip = parse_domain_for_node(host)
+            node.host = ip
+            node.port = port
+            node.weight = weight
+            table_insert(new_nodes, node)
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(upstreams) do
+        if not upstream_obj.upstream then
+            -- If the `upstream` object has only the `weighted_upstream` 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
+        server_list[upstream_obj.upstream] = upstream_obj.weighted_upstream
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in ipairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then
+                break
+            end
+        end
+
+        if match_flag then
+            upstreams = rule.upstreams
+            break
+        end
+    end
+
+    core.log.info("match_flag: ", match_flag)
+
+    if not match_flag then
+        return
+    end
+
+    local rr_up, err = lrucache(upstreams, nil, new_rr_obj, upstreams)
+    if not rr_up then
+        core.log.error("lrucache roundrobin failed: ", err)
+        return 500
+    end
+
+    local upstream = rr_up:find()

Review comment:
       Do not use wrr here, instead, please use weighted random algorithm.
   
   Say we have two upstreams and the weight of them are same,  when rr state is just reset, the first selected upstream is fixed, depending on their order, this is the inherent drawback of wrr. it might overwhelm the first selected upstream after the wrr state is reset.
   
   Also, wrr state will be reset once config is changd, the percentage curve is not so smooth.

##########
File path: doc/README.md
##########
@@ -81,6 +81,7 @@
 * [request-validation](plugins/request-validation.md): Validates requests before forwarding to upstream.
 * [proxy-mirror](plugins/proxy-mirror.md): Provides the ability to mirror client requests.
 * [api-breaker](plugins/api-breaker.md): Circuit Breaker for API that stops requests forwarding to upstream in case of unhealthy state.
+* [traffic-split](plugins/traffic-split.md): The traffic division plug-in divides the request traffic according to the specified ratio and diverts it to the corresponding upstream; through this plug-in, gray-scale publishing, blue-green publishing and custom publishing functions can be realized.

Review comment:
       Why traffic division?

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,314 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node)
+       and not ipmatcher.parse_ipv6(node)
+    then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_pass_host(ctx, upstream_info, host)
+    -- Currently only supports a single upstream of the domain name.
+    -- When the upstream is `IP`, do not do any `pass_host` operation.
+    if not core.utils.parse_ipv4(host)
+       and not core.utils.parse_ipv6(host)
+    then
+        local pass_host = upstream_info.pass_host or "pass"
+        if pass_host == "pass" then
+            ctx.var.upstream_host = ctx.var.host
+            return
+        end
+
+        if pass_host == "rewrite" then
+            ctx.var.upstream_host = upstream_info.upstream_host
+            return
+        end
+
+        ctx.var.upstream_host = host
+        return
+    end
+
+    return
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    if core.table.isarray(nodes) then
+        for _, node in ipairs(nodes) do
+            set_pass_host(ctx, upstream_info, node.host)
+            node.host = parse_domain_for_node(node.host)
+            node.port = node.port
+            node.weight = node.weight
+            table_insert(new_nodes, node)
+        end
+    else
+        for addr, weight in pairs(nodes) do
+            local node = {}
+            local ip, port, host
+            host, port = core.utils.parse_addr(addr)
+            set_pass_host(ctx, upstream_info, host)
+            ip = parse_domain_for_node(host)
+            node.host = ip
+            node.port = port
+            node.weight = weight
+            table_insert(new_nodes, node)
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(upstreams) do
+        if not upstream_obj.upstream then
+            -- If the `upstream` object has only the `weighted_upstream` 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
+        server_list[upstream_obj.upstream] = upstream_obj.weighted_upstream
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in ipairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then

Review comment:
       Why we can break when just the current match is true, we should check all matchs.

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,314 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node)
+       and not ipmatcher.parse_ipv6(node)
+    then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_pass_host(ctx, upstream_info, host)
+    -- Currently only supports a single upstream of the domain name.
+    -- When the upstream is `IP`, do not do any `pass_host` operation.
+    if not core.utils.parse_ipv4(host)
+       and not core.utils.parse_ipv6(host)
+    then
+        local pass_host = upstream_info.pass_host or "pass"
+        if pass_host == "pass" then
+            ctx.var.upstream_host = ctx.var.host
+            return
+        end
+
+        if pass_host == "rewrite" then
+            ctx.var.upstream_host = upstream_info.upstream_host
+            return
+        end
+
+        ctx.var.upstream_host = host
+        return
+    end
+
+    return
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    if core.table.isarray(nodes) then
+        for _, node in ipairs(nodes) do
+            set_pass_host(ctx, upstream_info, node.host)
+            node.host = parse_domain_for_node(node.host)
+            node.port = node.port
+            node.weight = node.weight
+            table_insert(new_nodes, node)
+        end
+    else
+        for addr, weight in pairs(nodes) do
+            local node = {}
+            local ip, port, host
+            host, port = core.utils.parse_addr(addr)
+            set_pass_host(ctx, upstream_info, host)
+            ip = parse_domain_for_node(host)
+            node.host = ip
+            node.port = port
+            node.weight = weight
+            table_insert(new_nodes, node)
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(upstreams) do
+        if not upstream_obj.upstream then
+            -- If the `upstream` object has only the `weighted_upstream` 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
+        server_list[upstream_obj.upstream] = upstream_obj.weighted_upstream
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in ipairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then

Review comment:
       We should log this error.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548549652



##########
File path: doc/README.md
##########
@@ -81,6 +81,7 @@
 * [request-validation](plugins/request-validation.md): Validates requests before forwarding to upstream.
 * [proxy-mirror](plugins/proxy-mirror.md): Provides the ability to mirror client requests.
 * [api-breaker](plugins/api-breaker.md): Circuit Breaker for API that stops requests forwarding to upstream in case of unhealthy state.
+* [traffic-split](plugins/traffic-split.md): The traffic split plugin divides the request traffic according to the specified ratio and diverts it to the corresponding upstream; through this plug-in, gray-scale publishing, blue-green publishing and custom publishing functions can be realized.

Review comment:
       ok.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548815490



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: The ratio between each upstream may not so accurate since the drawback of weighted round robin algorithm (especially when the wrr state is reset).
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For specific usage of operators, please see the `operator-list` part of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). |
+| rules.weighted_upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.weighted_upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream(not currently supported).            |
+| rules.weighted_upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.weighted_upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.weighted_upstreams.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. |
+| rules.weighted_upstreams.upstream.timeout  | object | optional    |  15     |   | Set the timeout period for connecting, sending and receiving messages (time unit: second, all default to 15 seconds).  |
+| rules.weighted_upstreams.upstream.pass_host | enum | optional    | "pass"  | ["pass", "node", "rewrite"]  | pass: pass the host requested by the client, node: pass the host requested by the client; use the host configured with the upstream node, rewrite: rewrite the host with the value configured by the upstream_host. |
+| rules.weighted_upstreams.upstream.name      | string | optional    |        |   | Identify the upstream service name, usage scenario, etc.  |
+| rules.weighted_upstreams.upstream.upstream_host | string | optional    |    |   | Only valid when pass_host is configured as rewrite.    |
+| rules.weighted_upstreams.weight | integer | optional    | weight = 1   |  | The traffic is divided according to the `weight` value, and the roundrobin algorithm is used to divide multiple `weight`. |
+
+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 plug-in will be guided, otherwise, all traffic will be directly Reach the `upstream` configured on `route` or `service`. Of course, you can also configure only the `weighted_upstreams` part, which will directly guide the traffic ratio between each upstream in the plugin based on the `weight` value in `weighted_upstreams`.
+
+>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. There is only a `weight` value in the weighted_upstreams of the plug-in, which means reaching the upstream traffic weight value configured on `route` or `service`. Such as:
+
+```json
+{
+    "weight": 2
+}
+```
+
+## How To Enable
+
+The following provides examples of plugin usage, which will help you understand the use of plugin.

Review comment:
       added.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] moonming commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
moonming commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-748807669


   `traffic shaping` is a better ame


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] moonming commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
moonming commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-739590736


   No, please fix it in this PR


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r539391814



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then
+                break
+            end
+        end
+
+        if match_flag then
+            upstreams = rule.upstreams
+            break
+        end
+    end
+
+    core.log.info("match_flag: ", match_flag)
+
+    if not match_flag then
+        return
+    end
+
+    local rr_up, err = lrucache(upstreams, nil, new_rr_obj, upstreams)
+    if not rr_up then
+        core.log.error("lrucache roundrobin failed: ", err)
+        return 500
+    end
+
+    local upstream = rr_up:find()

Review comment:
       The method of `random` is inaccurate in dividing traffic, it is only close to the division ratio. And `roundrobin` will be more precise.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] moonming commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
moonming commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-737685052


   @Firstsawyou What is the name of this plugin? the PR title and code file are not the same.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-741830775


   > @Firstsawyou Could you paste some sample traffic split configurations here to help us understand the schema better.
   
   @tokers These are two cases, which will help you understand:
   ### Grayscale Release
   
   Traffic is split according to the weight value configured by upstreams in the plugin (the rule of `match` is not configured, and `match` is passed by default). The request traffic is divided into 4:2, 2/3 of the traffic reaches the upstream of the `1981` port in the plugin, and 1/3 of the traffic reaches the upstream of the default `1980` port on the route.
   
   ```json
   {
       "weight": 2
   }
   ```
   
   There is only a `weight` value in the plugin upstreams, which represents the weight value of the upstream traffic arriving on the route.
   
   ```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": [
                   {
                       "upstreams": [
                           {
                               "upstream": {
                                   "name": "upstream_A",
                                   "type": "roundrobin",
                                   "nodes": {
                                       "127.0.0.1:1981":10
                                   },
                                   "timeout": {
                                       "connect": 15,
                                       "send": 15,
                                       "read": 15
                                   }
                               },
                               "weight": 4
                           },
                           {
                               "weight": 2
                           }
                       ]
                   }
               ]
           }
       },
       "upstream": {
               "type": "roundrobin",
               "nodes": {
                   "127.0.0.1:1980": 1
               }
       }
   }'
   ```
   
   ### Custom Release
   
   Multiple matching rules can be set in `match` (multiple conditions in `vars` are the relationship of `add`, and the relationship between multiple `vars` rules is the relationship of `or`; as long as one of the vars rules passes, it means `match` passed), only one is configured here, and the traffic is divided into 4:2 according to the value of `weight`. Among them, only the `weight` part represents the proportion of upstream on the route. When `match` fails to match, all traffic will only hit upstream on the route.
   
   ```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": [
                   {
                       "match": [
                           {
                               "vars": [
                                   ["arg_name","==","jack"],
                                   ["http_user-id",">","23"],
                                   ["http_apisix-key","~~","[a-z]+"]
                               ]
                           }
                       ],
                       "upstreams": [
                           {
                               "upstream": {
                                   "name": "upstream_A",
                                   "type": "roundrobin",
                                   "nodes": {
                                       "127.0.0.1:1981":10
                                   }
                               },
                               "weight": 4
                           },
                           {
                               "weight": 2
                           }
                       ]
                   }
               ]
           }
       },
       "upstream": {
               "type": "roundrobin",
               "nodes": {
                   "127.0.0.1:1980": 1
               }
       }
   }'
   ```


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on pull request #2935: feat: Implement dynamic-upstream plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-737687860


   > @Firstsawyou What is the name of this plugin? the PR title and code file are not the same.
   
   updated.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] moonming commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
moonming commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r546695985



##########
File path: README.md
##########
@@ -99,6 +99,7 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against
   - [Health Checks](doc/health-check.md): Enable health check on the upstream node, and will automatically filter unhealthy nodes during load balancing to ensure system stability.
   - Circuit-Breaker: Intelligent tracking of unhealthy upstream services.
   - [Proxy Mirror](doc/plugins/proxy-mirror.md): Provides the ability to mirror client requests.
+  - [Traffic Split](doc/plugins/traffic-split.md): Supports dividing the request traffic according to the specified proportional relationship.

Review comment:
       Boring description.  Why we need this plugin?




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r539766674



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do

Review comment:
       We use array to arrange nodes, you should also support it.

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)

Review comment:
       logic in this function is highly duplicated with the partial logic in `http_access_phase`, we may abstract them so we can reuse it.

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then

Review comment:
       Then the logic here should be:
   
   ```
   if not expr:eval() then
       return
   end
   ```

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then
+                break
+            end
+        end
+
+        if match_flag then
+            upstreams = rule.upstreams
+            break
+        end
+    end
+
+    core.log.info("match_flag: ", match_flag)
+
+    if not match_flag then
+        return
+    end
+
+    local rr_up, err = lrucache(upstreams, nil, new_rr_obj, upstreams)
+    if not rr_up then
+        core.log.error("lrucache roundrobin failed: ", err)
+        return 500
+    end
+
+    local upstream = rr_up:find()

Review comment:
       The weighted random is adopted by Envoy, Is there any docs show the precise difference between them?




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-743754125


   > > 4. the `upstreams` should highlight its function, i.e. weighted selection, so maybe "weighted_upstreams" or "weighted_clusters" is more suitable, since we will have label based selector in the future, we should distinguish them.
   > 
   > `weighted_upstreams` is better than before, but I think we should use the singular form here, this field only works for one upstream.
   > 
   > I prefer the name `weighted_upstream`.
   
   agree, updated.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] moonming commented on pull request #2935: feat: Implement dynamic-upstream plugin

Posted by GitBox <gi...@apache.org>.
moonming commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-737690040


   > > @Firstsawyou What is the name of this plugin? the PR title and code file are not the same.
   > 
   > updated.
   
   Not yet , please read your desc again


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-742247728


   > > > @Firstsawyou Could you paste some sample traffic split configurations here to help us understand the schema better.
   > > 
   > > 
   > > @tokers These are two cases, which will help you understand:
   > > ### Grayscale Release
   > > Traffic is split according to the weight value configured by upstreams in the plugin (the rule of `match` is not configured, and `match` is passed by default). The request traffic is divided into 4:2, 2/3 of the traffic reaches the upstream of the `1981` port in the plugin, and 1/3 of the traffic reaches the upstream of the default `1980` port on the route.
   > > ```json
   > > {
   > >     "weight": 2
   > > }
   > > ```
   > > 
   > > 
   > > There is only a `weight` value in the plugin upstreams, which represents the weight value of the upstream traffic arriving on the route.
   > > ```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": [
   > >                 {
   > >                     "upstreams": [
   > >                         {
   > >                             "upstream": {
   > >                                 "name": "upstream_A",
   > >                                 "type": "roundrobin",
   > >                                 "nodes": {
   > >                                     "127.0.0.1:1981":10
   > >                                 },
   > >                                 "timeout": {
   > >                                     "connect": 15,
   > >                                     "send": 15,
   > >                                     "read": 15
   > >                                 }
   > >                             },
   > >                             "weight": 4
   > >                         },
   > >                         {
   > >                             "weight": 2
   > >                         }
   > >                     ]
   > >                 }
   > >             ]
   > >         }
   > >     },
   > >     "upstream": {
   > >             "type": "roundrobin",
   > >             "nodes": {
   > >                 "127.0.0.1:1980": 1
   > >             }
   > >     }
   > > }'
   > > ```
   > > 
   > > 
   > > ### Custom Release
   > > Multiple matching rules can be set in `match` (multiple conditions in `vars` are the relationship of `add`, and the relationship between multiple `vars` rules is the relationship of `or`; as long as one of the vars rules passes, it means `match` passed), only one is configured here, and the traffic is divided into 4:2 according to the value of `weight`. Among them, only the `weight` part represents the proportion of upstream on the route. When `match` fails to match, all traffic will only hit upstream on the route.
   > > ```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": [
   > >                 {
   > >                     "match": [
   > >                         {
   > >                             "vars": [
   > >                                 ["arg_name","==","jack"],
   > >                                 ["http_user-id",">","23"],
   > >                                 ["http_apisix-key","~~","[a-z]+"]
   > >                             ]
   > >                         }
   > >                     ],
   > >                     "upstreams": [
   > >                         {
   > >                             "upstream": {
   > >                                 "name": "upstream_A",
   > >                                 "type": "roundrobin",
   > >                                 "nodes": {
   > >                                     "127.0.0.1:1981":10
   > >                                 }
   > >                             },
   > >                             "weight": 4
   > >                         },
   > >                         {
   > >                             "weight": 2
   > >                         }
   > >                     ]
   > >                 }
   > >             ]
   > >         }
   > >     },
   > >     "upstream": {
   > >             "type": "roundrobin",
   > >             "nodes": {
   > >                 "127.0.0.1:1980": 1
   > >             }
   > >     }
   > > }'
   > > ```
   > 
   > I have several questions:
   > 
   > 1. Why the upstream defined in traffic split has name?
   > 2. The block which only contains weight is confused, the configuration should be self-contained as soon as possible, you nedd some mark to tell the admin that it points to the default upstream.
   > 3. The upstream embedded inside traffic split brings some troubles such as health check, people wishes unhealthy nodes should be excluded, which is not implemented in your PR, a better way to achieve this is using upstream_id to reference already defined upstreams.
   > 4. the `upstreams` should highlight its function, i.e. weighted selection, so maybe "weighted_upstreams" or "weighted_clusters" is more suitable, since we will have label based selector in the future, we should distinguish them.
   
   1. The name is to identify the upstream service name, usage scenario, etc.
   2. I think it is not appropriate to add the default upstream configuration of `route` to only include the `weight`, because the configuration is repeated, and currently this plugin does not support some fields of the default upstream.
   3. Yes, this is a version, we need to support it later.
   4. I will update it later.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] membphis commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
membphis commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-743619285


   > 4\. the `upstreams` should highlight its function, i.e. weighted selection, so maybe "weighted_upstreams" or "weighted_clusters" is more suitable, since we will have label based selector in the future, we should distinguish them.
   
   `weighted_upstreams` is better than before, but I think we should use the singular form here, this field only works for one upstream.
   
   I prefer the name `weighted_upstream`.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers edited a comment on pull request #2935: feat: Implement dynamic-upstream plugin

Posted by GitBox <gi...@apache.org>.
tokers edited a comment on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-737697915


   We should use traffic split instead dynamic-upstream, in the future, the traffic split plugin will provide the labels based selector to choose nodes inside a upstream, this function is not covered by the name dynamic-upstream, which gives us an illusion this plugin is used for choosing one upstream from multiple upstreams.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547939144



##########
File path: doc/README.md
##########
@@ -81,6 +81,7 @@
 * [request-validation](plugins/request-validation.md): Validates requests before forwarding to upstream.
 * [proxy-mirror](plugins/proxy-mirror.md): Provides the ability to mirror client requests.
 * [api-breaker](plugins/api-breaker.md): Circuit Breaker for API that stops requests forwarding to upstream in case of unhealthy state.
+* [traffic-split](plugins/traffic-split.md): The traffic division plug-in divides the request traffic according to the specified ratio and diverts it to the corresponding upstream; through this plug-in, gray-scale publishing, blue-green publishing and custom publishing functions can be realized.

Review comment:
       There is a mistake here, I will fix it later.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-741649430


   @Firstsawyou Could you paste some sample traffic split configurations here to help us understand the schema better.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] spacewander commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
spacewander commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548340985



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,

Review comment:
       I think it again. Look like we can assign a virtual id to each upstream of the `upstreams` field, then use the vid as the suffix, so that we can reuse the upstream without messing up.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547771484



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,

Review comment:
       I have tried the uuid method, but this will result in a situation where all requests go to only one upstream.
   
   ```
   got: '1982, 1982, 1982, 1982, 1982
   # '
   #     expected: '1980, 1981, 1981, 1982, 1982
   ```




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] wfgydbu commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
wfgydbu commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r547039395



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,

Review comment:
       `up_conf.type .. "#route_" .. matched_route.value.id` would produce something like "roundrobin#route_1", which will further to be used as the upstream key in https://github.com/apache/apisix/blob/1f72f6726055e81cc245409d498084740cb3e1af/apisix/balancer.lua#L242.
   Will it be a problem? In my opinion, it will cause different upstreams to use the same key when picking node. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou edited a comment on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou edited a comment on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-737626259


   @spacewander  @membphis @tokers 
   Help me take a look at this test case: https://github.com/apache/apisix/pull/2935/files#diff-1badbb10ecfbb255409af122651c06a0b308ba0d51e672180d4e78719dce7e88R128. When executing this test case, the following error message is prompted:
   
   ```
   nginx: [emerg] Lua code block missing the closing long bracket "]=]" in /home/runner/work/apisix/apisix/t/servroot/conf/nginx.conf:148
   nginx: [emerg] Lua code block missing the closing long bracket "]=]" in /home/runner/work/apisix/apisix/t/servroot/conf/nginx.conf:148
   nginx: [emerg] Lua code block missing the closing long bracket "]=]" in /home/runner/work/apisix/apisix/t/servroot/conf/nginx.conf:148
   ```
   
   I don't know what caused this.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement dynamic-upstream plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r534759802



##########
File path: t/plugin/dynamic-upstream.t
##########
@@ -0,0 +1,213 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+log_level("info");
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: a `vars` rule and a plugin `upstream`
+--- 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": {
+                        "dynamic-upstream": {
+                            "rules": [
+                                {
+                                    "match": [
+                                        {
+                                            "vars": [
+                                                ["arg_name", "==", "jack"],
+                                                ["arg_age", "!","<", "16"]
+                                            ]
+                                        }
+                                    ],
+                                    "upstreams": [
+                                        {
+                                           "upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": {"127.0.0.1:1981":2}, "timeout": {"connect": 15, "send": 15, "read": 15}},
+                                            "weight": 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 2: expression validation failed, return to the default `route` upstream port `1980`
+--- request
+GET /server_port?name=jack&age=14
+--- response_body eval
+1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: the expression passes and returns to the `1981` port
+--- request
+GET /server_port?name=jack&age=16
+--- response_body eval
+1981
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: the expression passes and initiated multiple requests
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 6 do
+            local _, _, body = t('/server_port?name=jack&age=16', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1981, 1981, 1981, 1981
+--- no_error_log
+[error]
+
+
+
+=== TEST 5: Multiple vars rules and multiple plugin upstream
+--- config
+    location /t {
+        content_by_lua_block {

Review comment:
       Thank you very much, I will try.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548436880



##########
File path: README.md
##########
@@ -99,6 +99,7 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against
   - [Health Checks](doc/health-check.md): Enable health check on the upstream node, and will automatically filter unhealthy nodes during load balancing to ensure system stability.
   - Circuit-Breaker: Intelligent tracking of unhealthy upstream services.
   - [Proxy Mirror](doc/plugins/proxy-mirror.md): Provides the ability to mirror client requests.
+  - [Traffic Split](doc/plugins/traffic-split.md): Support the functions of gray release, blue-green release and custom release.

Review comment:
       updated.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-742168405


   > > @Firstsawyou Could you paste some sample traffic split configurations here to help us understand the schema better.
   > 
   > @tokers These are two cases, which will help you understand:
   > 
   > ### Grayscale Release
   > Traffic is split according to the weight value configured by upstreams in the plugin (the rule of `match` is not configured, and `match` is passed by default). The request traffic is divided into 4:2, 2/3 of the traffic reaches the upstream of the `1981` port in the plugin, and 1/3 of the traffic reaches the upstream of the default `1980` port on the route.
   > 
   > ```json
   > {
   >     "weight": 2
   > }
   > ```
   > 
   > There is only a `weight` value in the plugin upstreams, which represents the weight value of the upstream traffic arriving on the route.
   > 
   > ```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": [
   >                 {
   >                     "upstreams": [
   >                         {
   >                             "upstream": {
   >                                 "name": "upstream_A",
   >                                 "type": "roundrobin",
   >                                 "nodes": {
   >                                     "127.0.0.1:1981":10
   >                                 },
   >                                 "timeout": {
   >                                     "connect": 15,
   >                                     "send": 15,
   >                                     "read": 15
   >                                 }
   >                             },
   >                             "weight": 4
   >                         },
   >                         {
   >                             "weight": 2
   >                         }
   >                     ]
   >                 }
   >             ]
   >         }
   >     },
   >     "upstream": {
   >             "type": "roundrobin",
   >             "nodes": {
   >                 "127.0.0.1:1980": 1
   >             }
   >     }
   > }'
   > ```
   > 
   > ### Custom Release
   > Multiple matching rules can be set in `match` (multiple conditions in `vars` are the relationship of `add`, and the relationship between multiple `vars` rules is the relationship of `or`; as long as one of the vars rules passes, it means `match` passed), only one is configured here, and the traffic is divided into 4:2 according to the value of `weight`. Among them, only the `weight` part represents the proportion of upstream on the route. When `match` fails to match, all traffic will only hit upstream on the route.
   > 
   > ```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": [
   >                 {
   >                     "match": [
   >                         {
   >                             "vars": [
   >                                 ["arg_name","==","jack"],
   >                                 ["http_user-id",">","23"],
   >                                 ["http_apisix-key","~~","[a-z]+"]
   >                             ]
   >                         }
   >                     ],
   >                     "upstreams": [
   >                         {
   >                             "upstream": {
   >                                 "name": "upstream_A",
   >                                 "type": "roundrobin",
   >                                 "nodes": {
   >                                     "127.0.0.1:1981":10
   >                                 }
   >                             },
   >                             "weight": 4
   >                         },
   >                         {
   >                             "weight": 2
   >                         }
   >                     ]
   >                 }
   >             ]
   >         }
   >     },
   >     "upstream": {
   >             "type": "roundrobin",
   >             "nodes": {
   >                 "127.0.0.1:1980": 1
   >             }
   >     }
   > }'
   > ```
   
   I have several questions:
   
   1. Why the upstream defined in traffic split has name?
   2. The block which only contains weight is confused, the configuration should be self-contained as soon as possible, you nedd some mark to tell the admin that it points to the default upstream.
   3. The upstream embedded inside traffic split brings some troubles such as health check, people wishes unhealthy nodes should be excluded, which is not implemented in your PR, a better way to achieve this is using upstream_id to reference already defined upstreams.
   4. the `upstreams` should highlight its function, i.e. weighted selection, so maybe "weighted_upstreams" or "weighted_clusters" is more suitable, since we will have label based selector in the future, we should distinguish them.
   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-744115371


   > Hi all, thansk for this great feature! One quick question, In this plugin, how do you deal with healthcheck and
   > retry mechanism? are you planning to support it?
   
   This feature will be supported, please pay attention to this PR continuously.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548524411



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,322 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100

Review comment:
       updated.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548465245



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,322 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",

Review comment:
       Bad indent.

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,322 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100

Review comment:
       Ditto

##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,413 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Test Plugin**](#test-plugin)
+  - [**Grayscale Test**](#grayscale-test)
+  - [**Blue-green Test**](#blue-green-test)
+  - [**Custom Test**](#custom-test)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic division plugin divides the request traffic according to the specified ratio and diverts it to the corresponding upstream; through this plugin, gray-scale publishing, blue-green publishing and custom publishing functions can be realized.

Review comment:
       traffic division => traffic split.

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,314 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node)
+       and not ipmatcher.parse_ipv6(node)
+    then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_pass_host(ctx, upstream_info, host)
+    -- Currently only supports a single upstream of the domain name.
+    -- When the upstream is `IP`, do not do any `pass_host` operation.
+    if not core.utils.parse_ipv4(host)
+       and not core.utils.parse_ipv6(host)
+    then
+        local pass_host = upstream_info.pass_host or "pass"
+        if pass_host == "pass" then
+            ctx.var.upstream_host = ctx.var.host
+            return
+        end
+
+        if pass_host == "rewrite" then
+            ctx.var.upstream_host = upstream_info.upstream_host
+            return
+        end
+
+        ctx.var.upstream_host = host
+        return
+    end
+
+    return
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    if core.table.isarray(nodes) then
+        for _, node in ipairs(nodes) do
+            set_pass_host(ctx, upstream_info, node.host)
+            node.host = parse_domain_for_node(node.host)
+            node.port = node.port
+            node.weight = node.weight
+            table_insert(new_nodes, node)
+        end
+    else
+        for addr, weight in pairs(nodes) do
+            local node = {}
+            local ip, port, host
+            host, port = core.utils.parse_addr(addr)
+            set_pass_host(ctx, upstream_info, host)
+            ip = parse_domain_for_node(host)
+            node.host = ip
+            node.port = port
+            node.weight = weight
+            table_insert(new_nodes, node)
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(upstreams) do
+        if not upstream_obj.upstream then
+            -- If the `upstream` object has only the `weighted_upstream` 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
+        server_list[upstream_obj.upstream] = upstream_obj.weighted_upstream
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in ipairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then
+                break
+            end
+        end
+
+        if match_flag then
+            upstreams = rule.upstreams
+            break
+        end
+    end
+
+    core.log.info("match_flag: ", match_flag)
+
+    if not match_flag then
+        return
+    end
+
+    local rr_up, err = lrucache(upstreams, nil, new_rr_obj, upstreams)
+    if not rr_up then
+        core.log.error("lrucache roundrobin failed: ", err)
+        return 500
+    end
+
+    local upstream = rr_up:find()

Review comment:
       OK

##########
File path: conf/config-default.yaml
##########
@@ -246,6 +246,7 @@ plugins:                          # plugin list (sorted in alphabetical order)
   - wolf-rbac
   - zipkin
   # - server-info
+  - traffic-split

Review comment:
       Better to disable this plugin by default.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548791147



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: Since the selection of different upstream in the plugin is based on the roundrobin algorithm, the ratio of traffic distribution is not completely accurate when the algorithm state is reset.

Review comment:
       May change to:
   
   ```
   Note the ratio between each upstream may not so accurate since the drawback of weighted round robin algorithm (especially when the wrr state is reset).
   ```




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on pull request #2935: feat: Implement dynamic-upstream plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-737697915


   We should use traffic split instead dynamic-upstream, in the future, the traffic split plugin will provide the labels based selector to choose nodes inside a upstream, this function is not covered by the name dynamic-upstream, which gives us a illusion this plugin is used for choosing one upstream from multiple upstreams.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] membphis commented on a change in pull request #2935: feat: Implement dynamic-upstream plugin

Posted by GitBox <gi...@apache.org>.
membphis commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r534724221



##########
File path: t/plugin/dynamic-upstream.t
##########
@@ -0,0 +1,213 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+log_level("info");
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: a `vars` rule and a plugin `upstream`
+--- 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": {
+                        "dynamic-upstream": {
+                            "rules": [
+                                {
+                                    "match": [
+                                        {
+                                            "vars": [
+                                                ["arg_name", "==", "jack"],
+                                                ["arg_age", "!","<", "16"]
+                                            ]
+                                        }
+                                    ],
+                                    "upstreams": [
+                                        {
+                                           "upstream": {"name": "upstream_A", "type": "roundrobin", "nodes": {"127.0.0.1:1981":2}, "timeout": {"connect": 15, "send": 15, "read": 15}},
+                                            "weight": 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 2: expression validation failed, return to the default `route` upstream port `1980`
+--- request
+GET /server_port?name=jack&age=14
+--- response_body eval
+1980
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: the expression passes and returns to the `1981` port
+--- request
+GET /server_port?name=jack&age=16
+--- response_body eval
+1981
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: the expression passes and initiated multiple requests
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local bodys = {}
+        for i = 1, 6 do
+            local _, _, body = t('/server_port?name=jack&age=16', ngx.HTTP_GET)
+            bodys[i] = body
+        end
+        table.sort(bodys)
+        ngx.say(table.concat(bodys, ", "))
+    }
+}
+--- request
+GET /t
+--- response_body
+1980, 1980, 1981, 1981, 1981, 1981
+--- no_error_log
+[error]
+
+
+
+=== TEST 5: Multiple vars rules and multiple plugin upstream
+--- config
+    location /t {
+        content_by_lua_block {

Review comment:
       Reduce the total code length in content_by_lua_block




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r541572095



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then
+                break
+            end
+        end
+
+        if match_flag then
+            upstreams = rule.upstreams
+            break
+        end
+    end
+
+    core.log.info("match_flag: ", match_flag)
+
+    if not match_flag then
+        return
+    end
+
+    local rr_up, err = lrucache(upstreams, nil, new_rr_obj, upstreams)
+    if not rr_up then
+        core.log.error("lrucache roundrobin failed: ", err)
+        return 500
+    end
+
+    local upstream = rr_up:find()

Review comment:
       There is no relevant docs to show their differences, but there are obvious differences in writing test cases.

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then

Review comment:
       When `expr:eval()` is `false`, `break` is used to exit the loop of a single `match` expression. We can't use `return` to exit the loop, because when there are multiple `match`s, we need to continue executing the outer loop.

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)

Review comment:
       The logic here is different from the logic of the `http_access_phase` part in many details. It is not easy to reuse logic.

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)

Review comment:
       I think there is no need for caching for the time being, the expr object here is limited. Secondly, they need to be cached, which is not an easy task for me.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] moonming commented on pull request #2935: feat: traffic split plugin.

Posted by GitBox <gi...@apache.org>.
moonming commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-737580435


   Please write a better title and desc


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on pull request #2935: feat: Implement dynamic-upstream plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-737724426


   > We should use traffic split instead dynamic-upstream, in the future, the traffic split plugin will provide the labels based selector to choose nodes inside a upstream, this function is not covered by the name dynamic-upstream, which gives us an illusion this plugin is used for choosing one upstream from multiple upstreams.
   
   Ok i will update later.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548446270



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,314 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weighted_upstream = {
+                description = "used to split traffic between different" ..
+                              "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weighted_upstream = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node)
+       and not ipmatcher.parse_ipv6(node)
+    then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_pass_host(ctx, upstream_info, host)
+    -- Currently only supports a single upstream of the domain name.
+    -- When the upstream is `IP`, do not do any `pass_host` operation.
+    if not core.utils.parse_ipv4(host)
+       and not core.utils.parse_ipv6(host)
+    then
+        local pass_host = upstream_info.pass_host or "pass"
+        if pass_host == "pass" then
+            ctx.var.upstream_host = ctx.var.host
+            return
+        end
+
+        if pass_host == "rewrite" then
+            ctx.var.upstream_host = upstream_info.upstream_host
+            return
+        end
+
+        ctx.var.upstream_host = host
+        return
+    end
+
+    return
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    if core.table.isarray(nodes) then
+        for _, node in ipairs(nodes) do
+            set_pass_host(ctx, upstream_info, node.host)
+            node.host = parse_domain_for_node(node.host)
+            node.port = node.port
+            node.weight = node.weight
+            table_insert(new_nodes, node)
+        end
+    else
+        for addr, weight in pairs(nodes) do
+            local node = {}
+            local ip, port, host
+            host, port = core.utils.parse_addr(addr)
+            set_pass_host(ctx, upstream_info, host)
+            ip = parse_domain_for_node(host)
+            node.host = ip
+            node.port = port
+            node.weight = weight
+            table_insert(new_nodes, node)
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(upstreams) do
+        if not upstream_obj.upstream then
+            -- If the `upstream` object has only the `weighted_upstream` 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
+        server_list[upstream_obj.upstream] = upstream_obj.weighted_upstream
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in ipairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then
+                break
+            end
+        end
+
+        if match_flag then
+            upstreams = rule.upstreams
+            break
+        end
+    end
+
+    core.log.info("match_flag: ", match_flag)
+
+    if not match_flag then
+        return
+    end
+
+    local rr_up, err = lrucache(upstreams, nil, new_rr_obj, upstreams)
+    if not rr_up then
+        core.log.error("lrucache roundrobin failed: ", err)
+        return 500
+    end
+
+    local upstream = rr_up:find()

Review comment:
       I set up a related `issue` to record this issue, let’s look at the community’s feedback first. Then make adjustments.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548777357



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: Since the selection of different upstream in the plugin is based on the roundrobin algorithm, the ratio of traffic distribution is not completely accurate when the algorithm state is reset.

Review comment:
       Currently, the `roundrobin` algorithm is used when selecting the upstream, and the traffic distribution ratio may not be completely consistent. So I made a note in the document.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r539386341



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then

Review comment:
       Yes, the expectation is `and`. Multiple matching rules can be set in `match` ,multiple conditions in `vars` are the relationship of `add`, and the relationship between multiple `vars` rules is the relationship of `or`; as long as one of the vars rules passes, it means `match` passed.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r539897958



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then

Review comment:
       We need `match_flag` to record the result of `expr:eval()`, because we need the value of `match_flag` to judge whether the `match` rule passes.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548793046



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: Since the selection of different upstream in the plugin is based on the roundrobin algorithm, the ratio of traffic distribution is not completely accurate when the algorithm state is reset.

Review comment:
       updated.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] spacewander merged pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
spacewander merged pull request #2935:
URL: https://github.com/apache/apisix/pull/2935


   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r539974966



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then

Review comment:
       I mean, the relationship between each rule in `match` should be `and`, so the judgement can be short circuit, we can quit the loop once one of them is `false` .




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-737626259


   @spacewander  @membphis 
   Help me take a look at this test case: https://github.com/apache/apisix/pull/2935/files#diff-1badbb10ecfbb255409af122651c06a0b308ba0d51e672180d4e78719dce7e88R128. When executing this test case, the following error message is prompted:
   
   ```
   nginx: [emerg] Lua code block missing the closing long bracket "]=]" in /home/runner/work/apisix/apisix/t/servroot/conf/nginx.conf:148
   nginx: [emerg] Lua code block missing the closing long bracket "]=]" in /home/runner/work/apisix/apisix/t/servroot/conf/nginx.conf:148
   nginx: [emerg] Lua code block missing the closing long bracket "]=]" in /home/runner/work/apisix/apisix/t/servroot/conf/nginx.conf:148
   ```
   
   I don't know what caused this.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r539151821



##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then
+                break
+            end
+        end
+
+        if match_flag then
+            upstreams = rule.upstreams
+            break
+        end
+    end
+
+    core.log.info("match_flag: ", match_flag)
+
+    if not match_flag then
+        return
+    end
+
+    local rr_up, err = lrucache(upstreams, nil, new_rr_obj, upstreams)
+    if not rr_up then
+        core.log.error("lrucache roundrobin failed: ", err)
+        return 500
+    end
+
+    local upstream = rr_up:find()

Review comment:
       Why we need weighted round robin here? Just use weighted random is enough.

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)
+            if err then
+                return 500, err
+            end
+
+            match_flag = expr:eval()
+            if match_flag then

Review comment:
       Shouldn't the logic relationship between all conditions is AND rather than OR?
   
   @membphis @moonming @spacewander What do you think?

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do

Review comment:
       `match` is an array, so using `ipairs` here.

##########
File path: apisix/plugins/traffic-split.lua
##########
@@ -0,0 +1,294 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core       = require("apisix.core")
+local upstream   = require("apisix.upstream")
+local schema_def = require("apisix.schema_def")
+local init       = require("apisix.init")
+local roundrobin = require("resty.roundrobin")
+local ipmatcher  = require("resty.ipmatcher")
+local expr       = require("resty.expr.v1")
+local pairs      = pairs
+local ipairs     = ipairs
+local table_insert = table.insert
+
+local lrucache = core.lrucache.new({
+    ttl = 0, count = 512
+})
+
+
+local vars_schema = {
+    type = "array",
+    items = {
+        type = "array",
+        items = {
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 100
+            },
+            {
+                type = "string",
+                minLength = 1,
+                maxLength = 2
+            }
+        },
+        additionalItems = {
+            anyOf = {
+                {type = "string"},
+                {type = "number"},
+                {type = "boolean"},
+                {
+                    type = "array",
+                    items = {
+                        anyOf = {
+                            {
+                            type = "string",
+                            minLength = 1, maxLength = 100
+                            },
+                            {
+                                type = "number"
+                            },
+                            {
+                                type = "boolean"
+                            }
+                        }
+                    },
+                    uniqueItems = true
+                }
+            }
+        },
+        minItems = 0,
+        maxItems = 10
+    }
+}
+
+
+local match_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            vars = vars_schema
+        }
+    },
+    -- When there is no `match` rule, the default rule passes.
+    -- Perform upstream logic of plugin configuration.
+    default = {{ vars = {{"server_port", ">", 0}}}}
+}
+
+
+local upstreams_schema = {
+    type = "array",
+    items = {
+        type = "object",
+        properties = {
+            upstream_id = schema_def.id_schema,    -- todo: support upstream_id method
+            upstream = schema_def.upstream,
+            weight = {
+                description = "used to split traffic between different" ..
+                               "upstreams for plugin configuration",
+                type = "integer",
+                default = 1,
+                minimum = 0
+            }
+        }
+    },
+    -- When the upstream configuration of the plugin is missing,
+    -- the upstream of `route` is used by default.
+    default = {
+        {
+            weight = 1
+        }
+    },
+    minItems = 1,
+    maxItems = 20
+}
+
+
+local schema = {
+    type = "object",
+    properties = {
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    match = match_schema,
+                    upstreams = upstreams_schema
+                }
+            }
+        }
+    }
+}
+
+local plugin_name = "traffic-split"
+
+local _M = {
+    version = 0.1,
+    priority = 966,
+    name = plugin_name,
+    schema = schema
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+local function parse_domain_for_node(node)
+    if not ipmatcher.parse_ipv4(node) and not ipmatcher.parse_ipv6(node) then
+        local ip, err = init.parse_domain(node)
+        if ip then
+            return ip
+        end
+
+        if err then
+            return nil, err
+        end
+    end
+
+    return node
+end
+
+
+local function set_upstream(upstream_info, ctx)
+    local nodes = upstream_info.nodes
+    local new_nodes = {}
+    for addr, weight in pairs(nodes) do
+        local node = {}
+        local ip, port, host
+        host, port = core.utils.parse_addr(addr)
+        ip = parse_domain_for_node(host)
+        node.host = ip
+        node.port = port
+        node.weight = weight
+        table_insert(new_nodes, node)
+
+        -- Currently only supports a single upstream of the domain name.
+        -- When the upstream is `IP`, do not do any `pass_host` operation.
+        if not core.utils.parse_ipv4(host) and not core.utils.parse_ipv6(host) then
+            local pass_host = upstream_info.pass_host or "pass"
+            if pass_host == "pass" then
+                ctx.var.upstream_host = ctx.var.host
+                break
+            end
+
+            if pass_host == "rewrite" then
+                ctx.var.upstream_host = upstream_info.upstream_host
+                break
+            end
+
+            ctx.var.upstream_host = host
+            break
+        end
+    end
+    core.log.info("upstream_host: ", ctx.var.upstream_host)
+
+    local up_conf = {
+        name = upstream_info.name,
+        type = upstream_info.type,
+        nodes = new_nodes,
+        timeout = {
+            send = upstream_info.timeout and upstream_info.timeout.send or 15,
+            read = upstream_info.timeout and upstream_info.timeout.read or 15,
+            connect = upstream_info.timeout and upstream_info.timeout.connect or 15
+        }
+    }
+
+    local ok, err = upstream.check_schema(up_conf)
+    if not ok then
+        return 500, err
+    end
+
+    local matched_route = ctx.matched_route
+    upstream.set(ctx, up_conf.type .. "#route_" .. matched_route.value.id,
+                ctx.conf_version, up_conf, matched_route)
+    return
+end
+
+
+local function new_rr_obj(upstreams)
+    local server_list = {}
+    for _, upstream_obj in ipairs(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
+        server_list[upstream_obj.upstream] = upstream_obj.weight
+    end
+
+    return roundrobin:new(server_list)
+end
+
+
+function _M.access(conf, ctx)
+    if not conf or not conf.rules then
+        return
+    end
+
+    local upstreams, match_flag
+    for _, rule in pairs(conf.rules) do
+        match_flag = true
+        for _, single_match in pairs(rule.match) do
+            local expr, err = expr.new(single_match.vars)

Review comment:
       Please assess the overheads of initilaizing the expr object in each request, maybe we should cache it.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#issuecomment-744474804


   > Hi all, thansk for this great feature! One quick question, In this plugin, how do you deal with healthcheck and
   > retry mechanism? are you planning to support it?
   
   will support soon.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] Firstsawyou commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
Firstsawyou commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548667846



##########
File path: doc/zh-cn/plugins/traffic-split.md
##########
@@ -0,0 +1,413 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [English](../../plugins/traffic-split.md)
+
+# 目录
+
+- [名字](#名字)
+- [属性](#属性)
+- [如何启用](#如何启用)
+  - [灰度发布](#灰度发布)
+  - [蓝绿发布](#蓝绿发布)
+  - [自定义发布](#自定义发布)
+- [测试插件](#测试插件)
+  - [灰度测试](#灰度测试)
+  - [蓝绿测试](#蓝绿测试)
+  - [自定义测试](#自定义测试)
+- [禁用插件](#禁用插件)
+
+## 名字
+
+请求流量分割插件,对流量按指定的比例划分,并将其分流到对应的 upstream ;通过该插件可以实现灰度发布、蓝绿发布和自定义发布功能。
+
+注:由于插件中选择不同上游是根据 roundrobin 算法选择,因此存在算法状态重置的情况下,会对流量分配的比率并不完全精准。
+
+## 属性
+
+| 参数名        | 类型          | 可选项 | 默认值 | 有效值 | 描述                 |
+| ------------ | ------------- | ------ | ------ | ------ | -------------------- |
+| 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] | 可选   |        |        | 上游配置规则列表。 |
+| rules.weighted_upstreams.upstream_id  | string or integer | 可选   |        |        | 通过上游 id 绑定对应上游(暂不支持)。 |
+| rules.weighted_upstreams.upstream     | object | 可选   |        |        | 上游配置信息。 |
+| rules.weighted_upstreams.upstream.type | enum | 可选   |   roundrobin |  [roundrobin, chash]      | roundrobin 支持权重的负载,chash 一致性哈希,两者是二选一的(目前只支持 `roundrobin`)。 |
+| rules.weighted_upstreams.upstream.nodes | object | 可选   |        |        | 哈希表,内部元素的 key 是上游机器地址 列表,格式为地址 + Port,其中地址部 分可以是 IP 也可以是域名,⽐如 192.168.1.100:80、foo.com:80等。 value 则是节点的权重,特别的,当权重 值为 0 有特殊含义,通常代表该上游节点 失效,永远不希望被选中。 |
+| rules.weighted_upstreams.upstream.timeout | object | 可选   |  15     |        | 设置连接、发送消息、接收消息的超时时间(时间单位:秒,都默认为 15 秒)。 |
+| rules.weighted_upstreams.upstream.pass_host  | enum | 可选   | "pass"   | ["pass", "node", "rewrite"]  | pass: 透传客户端请求的 host, node: 不透传客户端请求的 host; 使用 upstream node 配置的 host, rewrite: 使用 upstream_host 配置的值重写 host 。 |
+| rules.weighted_upstreams.upstream.name  | string | 可选   |        |  | 标识上游服务名称、使⽤场景等。 |
+| rules.weighted_upstreams.upstream.upstream_host | string | 可选   |        |        | 只在 pass_host 配置为 rewrite 时有效。 |
+| rules.weighted_upstreams.weight       | integer | 可选   |   weight = 1     |        | 根据 `weight` 值做流量划分,多个 weight 之间使用 roundrobin 算法划分。|
+
+## 如何启用
+
+在插件的 weighted_upstreams 中只有 `weight` 值,表示到达默认 `route` 上的 upstream 流量权重值。
+
+```json
+{
+    "weight": 2
+}
+```
+
+### 灰度发布
+
+根据插件中 weighted_upstreams 配置的 `weight` 值做流量分流(不配置 `match` 的规则,已经默认 `match` 通过)。将请求流量按 4:2 划分,2/3 的流量到达插件中的 `1981` 端口上游, 1/3 的流量到达 route 上默认的 `1980` 端口上游。
+
+```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": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                },
+                                "timeout": {
+                                    "connect": 15,
+                                    "send": 15,
+                                    "read": 15
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### 蓝绿发布
+
+通过请求头获取蓝绿条件(也可以通过请求参数获取或NGINX变量),在 `match` 规则匹配通过后,表示所有请求都命中到插件配置的 upstream ,否则所以请求只命中 `route` 上配置的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["http_new-release","==","blue"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+### 自定义发布
+
+`match` 中可以设置多个匹配规则,`vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 规则之间是 `or` 的关系;只要其中一个 vars 规则通过,则表示 `match` 通过。
+
+示例1:只配置了一个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系。根据 `weight` 值将流量按 4:2 划分。其中只有 `weight` 部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+插件设置了请求的匹配规则并设置端口为`1981`的 upstream,route 上具有端口为`1980`的upstream。
+
+示例2:配置多个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 之间是 `and` 的关系。根据 `weight` 值将流量按 4:2 划分。其中只有 `weight` 部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 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": [
+                {
+                    "match": [
+                        {
+                            "vars": [
+                                ["arg_name","==","jack"],
+                                ["http_user-id",">","23"],
+                                ["http_apisix-key","~~","[a-z]+"]
+                            ],
+                            "vars": [
+                                ["arg_name2","==","rose"],
+                                ["http_user-id2","!",">","33"],
+                                ["http_apisix-key2","~~","[a-z]+"]
+                            ]
+                        }
+                    ],
+                    "weighted_upstreams": [
+                        {
+                            "upstream": {
+                                "name": "upstream_A",
+                                "type": "roundrobin",
+                                "nodes": {
+                                    "127.0.0.1:1981":10
+                                }
+                            },
+                            "weight": 4
+                        },
+                        {
+                            "weight": 2
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "upstream": {
+            "type": "roundrobin",
+            "nodes": {
+                "127.0.0.1:1980": 1
+            }
+    }
+}'
+```
+
+插件设置了请求的匹配规则并设置端口为`1981`的 upstream,route 上具有端口为`1980`的upstream。
+
+## 测试插件
+
+### 灰度测试
+
+**2/3 的请求命中到1981端口的upstream, 1/3 的流量命中到1980端口的upstream。**
+
+```shell
+$ curl http://127.0.0.1:9080/index.html -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+hello 1980
+
+$ curl http://127.0.0.1:9080/index.html -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+world 1981
+```
+
+### 蓝绿测试
+
+```shell
+$ curl 'http://127.0.0.1:9080/index.html?name=jack' -H 'new-release: blue' -i
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=utf-8
+......
+
+world 1981
+```
+
+当 `match` 匹配通过后,所有请求都命中到插件配置的 `upstream`,否则命中 `route` 上配置的 upstream 。

Review comment:
       `match` In the example configuration, the configuration example and the test have been put together.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [apisix] tokers commented on a change in pull request #2935: feat: Implement traffic splitting plugin

Posted by GitBox <gi...@apache.org>.
tokers commented on a change in pull request #2935:
URL: https://github.com/apache/apisix/pull/2935#discussion_r548810397



##########
File path: doc/plugins/traffic-split.md
##########
@@ -0,0 +1,409 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+- [中文](../zh-cn/plugins/traffic-split.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+  - [**Grayscale Release**](#grayscale-release)
+  - [**Blue-green Release**](#blue-green-release)
+  - [**Custom Release**](#custom-release)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The traffic split plugin allows users to incrementally direct percentages of traffic between various upstreams.
+
+Note: The ratio between each upstream may not so accurate since the drawback of weighted round robin algorithm (especially when the wrr state is reset).
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid   | Description                                                                              |
+| ---------------- | ------- | ----------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
+| 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 ==, ~=, ~~, >, <, in, has and !. For specific usage of operators, please see the `operator-list` part of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). |
+| rules.weighted_upstreams  | array[object] | optional    |    |         | List of upstream configuration rules.                                                   |
+| rules.weighted_upstreams.upstream_id  | string or integer | optional    |         |         | The upstream id is bound to the corresponding upstream(not currently supported).            |
+| rules.weighted_upstreams.upstream   | object | optional    |     |      | Upstream configuration information.                                                    |
+| rules.weighted_upstreams.upstream.type | enum | optional    | roundrobin  | [roundrobin, chash] | roundrobin supports weighted load, chash consistent hashing, the two are alternatives.   |
+| rules.weighted_upstreams.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. |
+| rules.weighted_upstreams.upstream.timeout  | object | optional    |  15     |   | Set the timeout period for connecting, sending and receiving messages (time unit: second, all default to 15 seconds).  |
+| rules.weighted_upstreams.upstream.pass_host | enum | optional    | "pass"  | ["pass", "node", "rewrite"]  | pass: pass the host requested by the client, node: pass the host requested by the client; use the host configured with the upstream node, rewrite: rewrite the host with the value configured by the upstream_host. |
+| rules.weighted_upstreams.upstream.name      | string | optional    |        |   | Identify the upstream service name, usage scenario, etc.  |
+| rules.weighted_upstreams.upstream.upstream_host | string | optional    |    |   | Only valid when pass_host is configured as rewrite.    |
+| rules.weighted_upstreams.weight | integer | optional    | weight = 1   |  | The traffic is divided according to the `weight` value, and the roundrobin algorithm is used to divide multiple `weight`. |
+
+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 plug-in will be guided, otherwise, all traffic will be directly Reach the `upstream` configured on `route` or `service`. Of course, you can also configure only the `weighted_upstreams` part, which will directly guide the traffic ratio between each upstream in the plugin based on the `weight` value in `weighted_upstreams`.
+
+>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. There is only a `weight` value in the weighted_upstreams of the plug-in, which means reaching the upstream traffic weight value configured on `route` or `service`. Such as:
+
+```json
+{
+    "weight": 2
+}
+```
+
+## How To Enable
+
+The following provides examples of plugin usage, which will help you understand the use of plugin.

Review comment:
       No! See other plugin documents, we need How to enable section.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org