My ideas for blog posts inevitably start to dry up after over two years at Apache APISIX. Hence, I did some triage on the APISIX repo. I stumbled upon this one question:
We have a requirement to use a plugin, where we need to route the traffic on percentage basis. I'll give an example for better understanding.
We have an URL https://xyz.com/ca/fr/index.html where ca is country (canada) and fr is french language. Now the traffic needs to routed 10% to https://xyz.com/ca/en/index.html and the remaining 90% to https://xyz.com/ca/fr/index.html. And whenever we're routing the traffic to https://xyz.com/ca/en/index.html we need to set a cookie. So for next call, if the cookie is there, it should directly go to https://xyz.com/ca/en/index.html else it should go via a 10:90 traffic split. What is the best possible way to achieve this ??
The use case is interesting, and I decided to tackle it.
I'll rephrase the requirements first:
- If no cookie is set, randomly forward the request to one of the upstreams
- If a cookie has been set, forward the request to the correct upstream.
For easier testing:
- I change the odds from 10:90 to 50:50
- I use the root instead of a host plus a path
Finally, I assume that the upstream sets the cookie.
Newcomers to Apache APISIX understand the matching algorithm very quickly: if a request matches a route's host, method, and path, forward it to the upstream set.
routes:
- id: 1
uri: /hello
host: foo.com
methods:
- GET
- PUT
- POST
upstream_id: 1
curl --resolve foo.com:127.0.0.1 http://foo.com/hello #1
curl -X POST --resolve foo.com:127.0.0.1 http://foo.com/hello #2
curl -X PUT --resolve foo.com:127.0.0.1 http://foo.com/hello #2
curl --resolve bar.com:127.0.0.1 http://bar.com/hello #3
curl --resolve foo.com:127.0.0.1 http://foo.com/hello/john #4
- Matches host, method as
curl
defaults toGET
, and path - Matches host, method, and path
- Doesn't match host
- Doesn't match path as the configured path doesn't hold a
*
character
path
is the only required parameter; neither host
nor methods
are. host
defaults to any host and methods
to any method.
Beyond these three main widespread matching parameters, others are available, e.g., remote_addrs
or vars
. Let's focus on the latter. The documentation on the Route API is pretty concise:
Matches based on the specified variables consistent with variables in Nginx. Takes the form
[[var, operator, val], [var, operator, val], ...]]
. Note that this is case sensitive when matching a cookie name. See lua-resty-expr for more details.-- Route API
One can only understand vars
in the Router Radix Tree documentation. The Router Radix Tree powers the Apache APISIX's matching engine.
Nginx provides a variety of built-in variables that can be used to filter routes based on certain criteria. Here is an example of how to filter routes by Nginx built-in variables:
-- How to filter route by Nginx built-in variable?
$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' { "uri": "/index.html", "vars": [ ["http_host", "==", "iresty.com"], ["cookie_device_id", "==", "a66f0cdc4ba2df8c096f74c9110163a9"], ["arg_name", "==", "json"], ["arg_age", ">", "18"], ["arg_address", "~~", "China.*"] ], "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:1980": 1 } } }'
This route will require the request header
host
equaliresty.com
, request cookie key_device_id
equala66f0cdc4ba2df8c096f74c9110163a9
, etc. You can learn more at radixtree-new.
Among all Nginx variables, we can find $cookie_xxx
. Hence, we can come up with the following configuration:
routes:
- name: Check for French cookie
uri: /
vars: [[ "cookie_site", "==", "fr" ]] #1
upstream_id: 1
- name: Check for English cookie
uri: /
vars: [[ "cookie_site", "==", "en" ]] #2
upstream_id: 2
- Match if a cookie named
site
has valuefr
- Match if a cookie named
site
has valueen
We need to configure the final route, the one used when no cookie is set. We use the traffic-split
plugin to assign a route randomly.
The
traffic-split
Plugin can be used to dynamically direct portions of traffic to various Upstream services.This is done by configuring
match
, which are custom rules for splitting traffic, andweighted_upstreams
which is a set of Upstreams to direct traffic to.When a request is matched based on the
match
attribute configuration, it will be directed to the Upstreams based on their configuredweights
. You can also omit using thematch
attribute and direct all traffic based onweighted_upstreams
.
The third route is the following:
- name: Let the fate decide
uri: /
upstream_id: 1 #1
plugins:
traffic-split:
rules:
- weighted_upstreams:
- weight: 50 #1
- upstream_id: 2 #2
weight: 50 #2
- The weight of the upstream
1
is50
- The upstream
2
weight is also50
out of the total weight sum. It's a half-half chance of APISIX forwarding it to either upstream
At this point, we need to solve one remaining issue: the order in which APISIX will evaluate the routes. When routes' paths are disjoint, the order plays no role; when they are overlapping, it does.
For example, if APISIX evaluates the last route first, it will forward the request to a random upstream, even though a cookie might have been set. We need to force the evaluation of the first two routes first. For that, APISIX offers the priority
parameter; its value is 0
by default. It evaluates routes matching by order of decreasing priority. We need to override it to evaluate the random route last.
- name: Let the fate decide
uri: /
upstream_id: 1
priority: -1
#...
You can try the setup in a browser or with curl
. With curl, we can set the "first" request like this:
curl -v localhost:9080
If the upstream sets the cookie correctly, you should see the following line among the different response headers:
Set-Cookie: site=fr
Since curl doesn't store cookies by default, the value should change across several calls. If we set the cookie, the value stays constant:
curl -v --cookie 'site=en' localhost:9080 #1
- The cookie name is case-sensitive; beware
The browser keeps the cookie, so it's even simpler. Just go to http:localhost:9080 and refresh several times: the content is the same as well. The content will change if you change the cookie to another possible value and request again.
The complete source code for this post can be found on GitHub:
To go further:
Originally published at A Java Geek on June 9th, 2024