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

Reply via email to