-
Notifications
You must be signed in to change notification settings - Fork 2.9k
feat: Implement traffic splitting plugin #2935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
spacewander
merged 31 commits into
apache:master
from
Firstsawyou:dynamic-upstream-plugin
Dec 25, 2020
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
8a2736e
feat: traffic split plugin.
Firstsawyou 4d49ef5
fix test case global ipairs and pairs.
Firstsawyou fcec69e
update code, docs and test cases.
Firstsawyou 2edd06d
resolve review.
Firstsawyou 4a9cd43
resolve review and update docs.
Firstsawyou 9b200af
resolve conflicts.
Firstsawyou 42d74f9
update docs.
Firstsawyou 0dcecf5
update README docs and resolve review.
Firstsawyou 847fa6b
`weighted_upstreams` field changed to singular form `weighted_upstream`.
Firstsawyou 1f72f67
Merge branch 'master' into dynamic-upstream-plugin
juzhiyuan 3126c9e
fix ci run error.
Firstsawyou 371f806
update README files content.
Firstsawyou b8f18d6
rerun ci.
Firstsawyou e5e0414
upstream nodes support array types.
Firstsawyou b2b6701
code style.
Firstsawyou 727ef58
fix unstable test cases.
Firstsawyou 51ecca7
fix doc desc and code style.
Firstsawyou a37d754
delete `---LAST` of test cases.
Firstsawyou 0b84c2e
fix: the upstream key is not unique
Firstsawyou 13057ae
fix global "type".
Firstsawyou 84399e8
add more test case and update schema field.
Firstsawyou 80683f5
update test case.
Firstsawyou f752da8
update code style and doc
Firstsawyou 9fa9ded
update test.
Firstsawyou f819400
refactored plugin docs.
Firstsawyou 2e9dde5
update plugin docs.
Firstsawyou ce0abab
fix doc typo.
Firstsawyou cc550e0
update docs.
Firstsawyou 2dca214
update doc note content.
Firstsawyou 6f6263b
update docs title.
Firstsawyou 237a4e1
in docs add how to enable content.
Firstsawyou File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,323 @@ | ||
| -- | ||
| -- 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 type = type | ||
| 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, | ||
| weighted_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 | ||
| local upstream_key = up_conf.type .. "#route_" .. | ||
| matched_route.value.id .. "_" ..upstream_info.vid | ||
| core.log.info("upstream_key: ", upstream_key) | ||
| upstream.set(ctx, upstream_key, ctx.conf_version, up_conf, matched_route) | ||
|
|
||
| return | ||
| end | ||
|
|
||
|
|
||
| local function new_rr_obj(weighted_upstreams) | ||
| local server_list = {} | ||
| for i, upstream_obj in ipairs(weighted_upstreams) do | ||
| if not upstream_obj.upstream then | ||
| -- If the `upstream` object has only the `weight` value, it means | ||
| -- that the `upstream` weight value on the default `route` has been reached. | ||
| -- Need to set an identifier to mark the empty upstream. | ||
| upstream_obj.upstream = "empty_upstream" | ||
| end | ||
|
|
||
| if type(upstream_obj.upstream) == "table" then | ||
| -- Add a virtual id field to uniquely identify the upstream `key`. | ||
| upstream_obj.upstream.vid = i | ||
| 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 weighted_upstreams, match_flag | ||
| for _, rule in ipairs(conf.rules) do | ||
| match_flag = true | ||
| for _, single_match in ipairs(rule.match) do | ||
| local expr, err = expr.new(single_match.vars) | ||
|
Firstsawyou marked this conversation as resolved.
|
||
| if err then | ||
|
Firstsawyou marked this conversation as resolved.
|
||
| core.log.error("vars expression does not match: ", err) | ||
| return 500, err | ||
| end | ||
|
|
||
| match_flag = expr:eval() | ||
| if match_flag then | ||
|
Firstsawyou marked this conversation as resolved.
Firstsawyou marked this conversation as resolved.
|
||
| break | ||
| end | ||
| end | ||
|
|
||
| if match_flag then | ||
| weighted_upstreams = rule.weighted_upstreams | ||
| break | ||
| end | ||
| end | ||
| core.log.info("match_flag: ", match_flag) | ||
|
|
||
| if not match_flag then | ||
| return | ||
| end | ||
|
|
||
| local rr_up, err = lrucache(weighted_upstreams, nil, new_rr_obj, weighted_upstreams) | ||
| if not rr_up then | ||
| core.log.error("lrucache roundrobin failed: ", err) | ||
| return 500 | ||
| end | ||
|
|
||
| local upstream = rr_up:find() | ||
|
Firstsawyou marked this conversation as resolved.
tokers marked this conversation as resolved.
|
||
| if upstream and upstream ~= "empty_upstream" then | ||
| core.log.info("upstream: ", core.json.encode(upstream)) | ||
| return set_upstream(upstream, ctx) | ||
| end | ||
|
|
||
| return | ||
| end | ||
|
|
||
|
|
||
| return _M | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.