This is an automated email from the ASF dual-hosted git repository.
spacewander pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new f058b25d1 perf: optimizing performance of router match with lrucache
(#8102)
f058b25d1 is described below
commit f058b25d1bdad4be57298f6da3a287601c32328b
Author: tzssangglass <[email protected]>
AuthorDate: Tue Oct 18 13:29:19 2022 +0800
perf: optimizing performance of router match with lrucache (#8102)
---
apisix/core/ai.lua | 83 +++++++++
apisix/http/route.lua | 2 +
apisix/router.lua | 2 +
t/core/ai.t | 476 ++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 563 insertions(+)
diff --git a/apisix/core/ai.lua b/apisix/core/ai.lua
new file mode 100644
index 000000000..99c38b05b
--- /dev/null
+++ b/apisix/core/ai.lua
@@ -0,0 +1,83 @@
+--
+-- 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 require = require
+local core = require("apisix.core")
+local ipairs = ipairs
+
+local route_lrucache = core.lrucache.new({
+ -- TODO: we need to set the cache size by count of routes
+ -- if we have done this feature, we need to release the origin lrucache
+ count = 512
+})
+
+local _M = {}
+
+local orig_router_match
+local router
+
+local function match_route(ctx)
+ orig_router_match(ctx)
+ return ctx.matched_route or false
+end
+
+
+local function ai_match(ctx)
+ -- TODO: we need to generate cache key dynamically
+ local key = ctx.var.uri .. "-" .. ctx.var.method .. "-" .. ctx.var.host
+ local ver = router.user_routes.conf_version
+ local route_cache = route_lrucache(key, ver,
+ match_route, ctx)
+ -- if the version has not changed, use the cached route
+ if route_cache then
+ ctx.matched_route = route_cache
+ end
+end
+
+
+function _M.routes_analyze(routes)
+ -- TODO: we need to add a option in config.yaml to enable this
feature(default is true)
+ local route_flags = core.table.new(0, 2)
+ for _, route in ipairs(routes) do
+ if route.vars then
+ route_flags["vars"] = true
+ end
+
+ if route.filter_fun then
+ route_flags["filter_fun"] = true
+ end
+
+ if route.remote_addr or route.remote_addrs then
+ route_flags["remote_addr"] = true
+ end
+ end
+
+ if route_flags["vars"] or route_flags["filter_fun"]
+ or route_flags["remote_addr"] then
+ router.match = orig_router_match
+ else
+ core.log.info("use ai plane to match route")
+ router.match = ai_match
+ end
+end
+
+
+function _M.init_worker(router_http)
+ router = router_http
+ orig_router_match = router.match
+end
+
+return _M
diff --git a/apisix/http/route.lua b/apisix/http/route.lua
index 2a8a12b20..28e121708 100644
--- a/apisix/http/route.lua
+++ b/apisix/http/route.lua
@@ -21,6 +21,7 @@ local service_fetch = require("apisix.http.service").get
local core = require("apisix.core")
local expr = require("resty.expr.v1")
local plugin_checker = require("apisix.plugin").plugin_checker
+local ai = require("apisix.core.ai")
local ipairs = ipairs
local type = type
local error = error
@@ -91,6 +92,7 @@ function _M.create_radixtree_uri_router(routes, uri_routes,
with_parameter)
end
end
+ ai.routes_analyze(uri_routes)
core.log.info("route items: ", core.json.delay_encode(uri_routes, true))
if with_parameter then
diff --git a/apisix/router.lua b/apisix/router.lua
index 9bdafebbd..4b6ed6c32 100644
--- a/apisix/router.lua
+++ b/apisix/router.lua
@@ -19,6 +19,7 @@ local http_route = require("apisix.http.route")
local apisix_upstream = require("apisix.upstream")
local core = require("apisix.core")
local plugin_checker = require("apisix.plugin").plugin_checker
+local ai_init = require("apisix.core.ai").init_worker
local str_lower = string.lower
local error = error
local ipairs = ipairs
@@ -83,6 +84,7 @@ function _M.http_init_worker()
local router_http = require("apisix.http.router." .. router_http_name)
attach_http_router_common_methods(router_http)
+ ai_init(router_http)
router_http.init_worker(filter)
_M.router_http = router_http
diff --git a/t/core/ai.t b/t/core/ai.t
new file mode 100644
index 000000000..a54c4ea0b
--- /dev/null
+++ b/t/core/ai.t
@@ -0,0 +1,476 @@
+#
+# 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);
+log_level('info');
+worker_connections(256);
+no_root_location();
+no_shuffle();
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
+ $block->set_value("no_error_log", "[error]");
+ }
+
+ if (!defined $block->request) {
+ $block->set_value("request", "GET /t");
+ }
+
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: enable route cache
+--- 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,
+ [[{
+ "methods": ["GET"],
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+ local t = {}
+ for i = 1, 2 do
+ local th = assert(ngx.thread.spawn(function(i)
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri)
+ assert(res.status == 200)
+ if not res then
+ ngx.log(ngx.ERR, err)
+ return
+ end
+ end, i))
+ table.insert(t, th)
+ end
+ for i, th in ipairs(t) do
+ ngx.thread.wait(th)
+ end
+
+ ngx.say("done")
+ }
+ }
+--- response_body
+done
+--- error_log
+use ai plane to match route
+
+
+
+=== TEST 2: route has vars, disable route cache
+--- 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,
+ [[{
+ "methods": ["GET"],
+ "vars": [ ["arg_k", "~=", "v"] ],
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ local http = require "resty.http"
+ local uri1 = "http://127.0.0.1:" .. ngx.var.server_port ..
"/hello?k=a"
+ local uri2 = "http://127.0.0.1:" .. ngx.var.server_port ..
"/hello?k=v"
+ local threads = {}
+ for i = 1, 2 do
+ local th = assert(ngx.thread.spawn(function(i)
+ local httpc = http.new()
+ local res, err
+ if i == 1 then
+ -- arg_k = a, match route
+ res, err = httpc:request_uri(uri1)
+ ngx.log(ngx.WARN, "res : ", require("inspect")(res))
+ assert(res.status == 200)
+ else
+ -- arg_k = v, not match route
+ res, err = httpc:request_uri(uri2)
+ assert(res.status == 404)
+ end
+ if not res then
+ ngx.log(ngx.ERR, err)
+ return
+ end
+ end, i))
+ table.insert(threads, th)
+ end
+ for i, th in ipairs(threads) do
+ ngx.thread.wait(th)
+ end
+
+ ngx.say("done")
+ }
+ }
+--- response_body
+done
+--- no_error_log
+use ai plane to match route
+
+
+
+=== TEST 3: method changed, create different route cache
+--- 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,
+ [[{
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+ local t = {}
+ for i = 1, 4 do
+ local th = assert(ngx.thread.spawn(function(i)
+ local httpc = http.new()
+ local res, err
+ if i % 2 == 0 then
+ res, err = httpc:request_uri(uri, { method = "POST" })
+ else
+ res, err = httpc:request_uri(uri)
+ end
+ assert(res.status == 200)
+ if not res then
+ ngx.log(ngx.ERR, err)
+ return
+ end
+ end, i))
+ table.insert(t, th)
+ end
+ for i, th in ipairs(t) do
+ ngx.thread.wait(th)
+ end
+
+ ngx.say("done")
+ }
+ }
+--- response_body
+done
+--- error_log
+use ai plane to match route
+
+
+
+=== TEST 4: route with plugins, enable
+--- 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,
+ [[{
+
+ "plugins": {
+ "limit-count": {
+ "count": 9999,
+ "time_window": 60
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+ local t = {}
+ for i = 1, 2 do
+ local th = assert(ngx.thread.spawn(function(i)
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri)
+ assert(res.status == 200)
+ if not res then
+ ngx.log(ngx.ERR, err)
+ return
+ end
+ end, i))
+ table.insert(t, th)
+ end
+ for i, th in ipairs(t) do
+ ngx.thread.wait(th)
+ end
+
+ ngx.say("done")
+ }
+ }
+--- response_body
+done
+--- error_log
+use ai plane to match route
+
+
+
+=== TEST 5: enable -> disable -> enable -> disable
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+ local uri2 = "http://127.0.0.1:" .. ngx.var.server_port ..
"/hello1?k=a"
+ local uri3 = "http://127.0.0.1:" .. ngx.var.server_port ..
"/hello1?k=v"
+
+ -- round 1: all routes without vars or filter_fun, enable route
cache
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "methods": ["GET"],
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ ngx.sleep(0.5)
+
+ local threads1 = {}
+ for i = 1, 2 do
+ local th = assert(ngx.thread.spawn(function(i)
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri)
+ assert(res.status == 200)
+ if not res then
+ ngx.log(ngx.ERR, err)
+ return
+ end
+ end, i))
+ table.insert(threads1, th)
+ end
+
+ for i, th in ipairs(threads1) do
+ ngx.thread.wait(th)
+ end
+
+ -- round 2: routes with vars or filter_fun, disable route cache
+ local code, body = t('/apisix/admin/routes/2',
+ ngx.HTTP_PUT,
+ [[{
+ "methods": ["GET"],
+ "vars": [ ["arg_k", "~=", "v"] ],
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello1"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ ngx.sleep(0.5)
+
+ local threads2 = {}
+ for i = 1, 2 do
+ local th = assert(ngx.thread.spawn(function(i)
+ local httpc = http.new()
+ local res, err
+ if i == 1 then
+ -- arg_k = a, match route 2
+ res, err = httpc:request_uri(uri2)
+ assert(res.status == 200)
+ else
+ -- arg_k = v, not match route 2
+ res, err = httpc:request_uri(uri3)
+ assert(res.status == 404)
+ end
+ if not res then
+ ngx.log(ngx.ERR, err)
+ return
+ end
+ end, i))
+ table.insert(threads2, th)
+ end
+
+ for i, th in ipairs(threads2) do
+ ngx.thread.wait(th)
+ end
+
+ -- round 3: delete route with vars, the remaining route
+ -- has no vars or filter_fun, enable route cache
+ local code, body = t('/apisix/admin/routes/2',
+ ngx.HTTP_DELETE)
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ ngx.sleep(0.5)
+
+ local threads3 = {}
+ for i = 1, 2 do
+ local th = assert(ngx.thread.spawn(function(i)
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri)
+ assert(res.status == 200)
+ if not res then
+ ngx.log(ngx.ERR, err)
+ return
+ end
+ end, i))
+ table.insert(threads3, th)
+ end
+
+ for i, th in ipairs(threads3) do
+ ngx.thread.wait(th)
+ end
+
+ -- round 4: routes with vars or filter_fun, disable route cache
+ local code, body = t('/apisix/admin/routes/2',
+ ngx.HTTP_PUT,
+ [[{
+ "methods": ["GET"],
+ "vars": [ ["arg_k", "~=", "v"] ],
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello1"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ ngx.sleep(0.5)
+
+ local threads4 = {}
+ for i = 1, 2 do
+ local th = assert(ngx.thread.spawn(function(i)
+ local httpc = http.new()
+ local res, err
+ if i == 1 then
+ -- arg_k = a, match route 2
+ res, err = httpc:request_uri(uri2)
+ assert(res.status == 200)
+ else
+ -- arg_k = v, not match route 2
+ res, err = httpc:request_uri(uri3)
+ assert(res.status == 404)
+ end
+ if not res then
+ ngx.log(ngx.ERR, err)
+ return
+ end
+ end, i))
+ table.insert(threads4, th)
+ end
+
+ for i, th in ipairs(threads4) do
+ ngx.thread.wait(th)
+ end
+
+ -- clean route 2
+ local code, body = t('/apisix/admin/routes/2',
+ ngx.HTTP_DELETE)
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ ngx.say("done")
+ }
+ }
+--- response_body
+done
+--- grep_error_log eval
+qr/use ai plane to match route/
+--- grep_error_log_out
+use ai plane to match route
+use ai plane to match route