構建api gateway之 http路由實現

2023-01-31 06:02:49

http路由

路由是指路由器從一個介面上收到封包,根據封包的目的地址進行定向並轉發到另一個介面的過程。

而這裡的http路由其實等同於web開發中,根據http相關引數(比如url、http method)分配到對應的處理程式。

借用web框架的示意圖,其作用如下

路由匹配

這裡我們先簡化一下內容,假設我們已有 upstream ip、port,現在只需能區分各種請求怎麼樣對應到這些不同的upstream上,不必關心能否做改寫請求啊、熔斷啊等等複雜情況

那麼怎麼實現路由匹配呢?

通常為以下兩種方式

  1. 字典+正規表示式

    字典用於匹配精確的結果(比如 url == /login 情況),字典的特性保證這類匹配具有超高效能

    正規表示式用於匹配複雜模糊的結果(比如 url 以 .html 為字尾的所有請求), 當然多項正規表示式只能依次遍歷,效能肯定存在問題(為了緩解效能問題,通常會使用快取做優化)

  2. 字首樹

    字首樹,又稱字典樹,是一種有序樹,用於儲存關聯陣列,其中的鍵通常是字串。

    其由於插入和查詢的效率很高,非常適合路由匹配的情況

    雖然理論hash效能最好,字首樹仍需查詢,效率會低一些,

    但畢竟通常開發都會使用比較複雜的路由, 所以效率肯定比上面的 字典+正規表示式 要高很多

路由匹配實踐

這裡由於篇幅關係,只介紹 字典+正規表示式 簡單實現,字首樹 則介紹apisix 中實現的庫,畢竟演演算法要強悍的效能,純lua實現是不太可靠的,必須得上c才行,這裡就避免c的複雜度吧。

字典+正規表示式 簡單實現


worker_processes  1;        #nginx worker 數量
error_log logs/error.log;   #指定錯誤紀錄檔檔案路徑
events {
    worker_connections 1024;
}

http {
    log_format main '$remote_addr [$time_local] $status $request_time $upstream_status $upstream_addr $upstream_response_time';
    access_log logs/access.log main buffer=16384 flush=3;            #access_log 檔案設定

    upstream nature_upstream {
        server 127.0.0.1:6699; #upstream 設定為 hello world 服務

        # 一樣的balancer
        balancer_by_lua_block {
            local balancer = require "ngx.balancer"
            local upstream = ngx.ctx.api_ctx.upstream
            local ok, err = balancer.set_current_peer(upstream.host, upstream.port)
            if not ok then
                ngx.log(ngx.ERR, "failed to set the current peer: ", err)
                return ngx.exit(ngx.ERROR)
            end
        }
    }

    init_by_lua_block {
        local path_exact = {}  -- 精確匹配字典
        local path_reg = {}    -- 正則遍歷集合

        -- 新增路由的方法
        add_router = function(type, path, upstream)
            if type == 'exact' then
                path_exact[path] = upstream
            else
                table.insert(path_reg, {reg = path, upstream = upstream})
            end
        end

        -- 匹配方法,優先精確匹配
        router_match = function()
            local p = ngx.var.uri
            local upstream = path_exact[p]
            if upstream == nil then
                for k, v in pairs(path_reg) do
                    if ngx.re.find(p, v.reg) then
                        return v.upstream
                    end
                end
            end

            return upstream
        end

        -- 新增測試資料
        add_router('exact' , '/aa/d', {host = '127.0.0.1', port = 6698})
        add_router('reg' , '/aa/*', {host = '127.0.0.1', port = 6699})
    }

    server {
		#監聽埠,若你的8699埠已經被佔用,則需要修改
        listen 8699 reuseport;

        location / {

            # 在access階段匹配路由
            access_by_lua_block {
                local upstream = router_match()
                if upstream then
                    ngx.ctx.api_ctx = { upstream = upstream }
                else
                    ngx.exit(404)
                end
            }

            proxy_http_version                  1.1;
            proxy_pass http://nature_upstream; #轉發到 upstream
        }
    }


    #為了大家方便理解和測試,我們引入一個hello world 服務
    server {
		#監聽埠,若你的6699埠已經被佔用,則需要修改
        listen 6699;
        location / {
            default_type text/html;

            content_by_lua_block {
                ngx.say("HelloWorld")
            }
        }
    }
}

啟動服務並測試

$ openresty -p ~/openresty-test -c openresty.conf #啟動
$ curl --request GET 'http://127.0.0.1:8699/aa/d'  #測試精確匹配
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
</body>
</html>
$ curl --request GET 'http://127.0.0.1:8699/aa/xxx'  #測試正則匹配
HelloWorld
$ curl --request GET 'http://127.0.0.1:8699/dd/xxx'  #測試不存的路由
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
</body>
</html>

核心原理其實就是 router_match 這個函數的內容,

不過上述簡單實現肯定無法支援以下的一些複雜場景

  1. 正則衝突 (一般會引入優先順序順序支援,或者預設加入的順序)
  2. host隔離
  3. 引數匹配
  4. 自定義條件匹配
  5. ……

所以一個完整的路由實現都很複雜,畢竟支援的場景挺多的

使用 lua-resty-radixtree 路由庫

引入庫

以下內容更新到 openresty-dev-1.rockspec

-- 依賴包
dependencies = {
    "lua-resty-radixtree >= 2.8.2",
}

然後執行

luarocks install openresty-dev-1.rockspec --tree=deps --only-deps --local
程式碼調整

這裡只列舉變動的部分

http {

    init_by_lua_block {

        -- 初始化路由
        local radix = require("resty.radixtree")
        local r = radix.new({
            {paths = {'/aa/d'}, metadata = {host = '127.0.0.1', port = 6698}},
            {paths = {'/aa/*'}, metadata = {host = '127.0.0.1', port = 6699}}
        })

        -- 匹配路由
        router_match = function()
            local upstream, err = r:match(ngx.var.uri, {})
            if err then
                log.error(err)
            end
            return upstream
        end
    }

}

啟動服務並測試

$ openresty -p ~/openresty-test -c openresty.conf #啟動
$ curl --request GET 'http://127.0.0.1:8699/aa/d'  #測試精確匹配
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
</body>
</html>
$ curl --request GET 'http://127.0.0.1:8699/aa/xxx'  #測試正則匹配
HelloWorld
$ curl --request GET 'http://127.0.0.1:8699/dd/xxx'  #測試不存的路由
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
</body>
</html>

可以看到效果一樣

更多複雜使用請參閱 https://github.com/api7/lua-resty-radixtree

目錄