OpenResty中如何實現,按QPS、時間範圍、來源IP進行限流

OpenResty是一個基於Nginx與Lua的高性能Web平臺,它通過LuaJIT在Nginx中運行高效的Lua腳本和模塊,可以用來處理複雜的網絡請求,並且支持各種流量控制和限制的功能。

近期研究在OpenResty中如何實現,按QPS、時間範圍、來源IP進行限流,以及動態更新限流策略。今天將實現方案分享給大家。

一、在OpenResty中如何實現,按QPS、時間範圍、來源IP進行限流

使用OpenResty進行限流的幾種常見方法:

  1. 按QPS(每秒查詢率)限流:
    使用ngx_http_limit_req_module模塊,可以限制每個客戶端的請求速率。這個模塊使用漏桶算法來控制請求的速率。

    在Nginx配置文件中,你可以這樣設置:

    http {
        limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1r/s;
    
        server {
            location / {
                limit_req zone=mylimit burst=5 nodelay;
            }
        }
    }
    

    上面的配置定義了一個名爲mylimit的區域,它根據客戶端的IP地址來限流,並且設置了每秒可以處理的請求數(rate)爲1。burst參數定義了可以累積的最大請求數,而nodelay表示不對超出速率的請求進行延遲處理。

  2. 按時間範圍限流:
    如果你想在特定時間範圍內限流,你可能需要編寫一些Lua腳本來實現這個邏輯。例如,你可以使用lua-resty-limit-traffic庫的limit.req模塊,並結合時間判斷邏輯:

    local limit_req = require "resty.limit.req"
    local lim, err = limit_req.new("my_limit_req_store", 2, 0)
    if not lim then
        ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
        return ngx.exit(500)
    end
    
    local key = ngx.var.binary_remote_addr
    local delay, err = lim:incoming(key, true)
    
    -- 檢查當前時間是否在限流時間範圍內
    local current_hour = os.date("%H")
    if current_hour >= "09" and current_hour <= "18" then
        -- 在工作時間進行限流
        if delay then
            if delay >= 0.001 then
                -- 延遲處理
                ngx.sleep(delay)
            end
        else
            if err == "rejected" then
                -- 請求超出速率限制
                return ngx.exit(503)
            end
            ngx.log(ngx.ERR, "failed to limit req: ", err)
            return ngx.exit(500)
        end
    end
    
  3. 按來源IP限流:
    使用ngx_http_limit_conn_module模塊,可以限制同時處理的連接數。如果你想根據來源IP地址進行限流,可以像這樣配置:

    http {
        limit_conn_zone $binary_remote_addr zone=addr:10m;
    
        server {
            location / {
                limit_conn addr 3;
            }
        }
    }
    

    這個配置限制了每個IP地址同時只能有3個活躍連接。

        在實際部署時,需要根據自己的業務需求調整這些配置參數。需要注意的是,對於複雜的限流規則,可能需要結合多個Nginx模塊和Lua腳本來實現。

        而且,由於限流策略可能會影響用戶體驗,應謹慎設計限流規則,確保它們既能保護後端服務,又不會對合法用戶造成不必要的麻煩。

二、限流後提示信息處理和請求狀態

在OpenResty中,如果你使用了內置的限流模塊(如ngx_http_limit_req_modulengx_http_limit_conn_module)並且請求被限流,你可以通過返回特定的狀態碼和錯誤頁面來通知用戶。

例如,如果使用limit_reqlimit_conn指令,你可以設置返回503狀態碼(服務不可用),然後定義一個自定義的錯誤頁面:

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
    limit_conn_zone $binary_remote_addr zone=addr:10m;

    server {
        location / {
            limit_req zone=one burst=5 nodelay;
            limit_conn addr 3;

            error_page 503 /custom_503.html;
        }

        location = /custom_503.html {
            root /path/to/your/error/pages;
            internal;
        }
    }
}

在上面的配置中,當請求被限流並返回503狀態碼時,Nginx將會發送/path/to/your/error/pages/custom_503.html文件的內容作爲響應。

如果你使用Lua腳本來處理限流,你可以更加靈活地設置返回的內容。例如,你可以使用ngx.exit來返回狀態碼,同時使用ngx.sayngx.send_headers來發送自定義的響應體或者響應頭。

access_by_lua_block {
    -- 假設你已經進行了一些限流判斷...
    if should_limit then
        ngx.status = ngx.HTTP_SERVICE_UNAVAILABLE
        ngx.header.content_type = 'text/html'
        ngx.say("<html><body>Sorry, we are currently receiving too many requests. Please try again later.</body></html>")
        ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
    end
}

在這個Lua代碼塊中,如果should_limit變量爲true,則返回503狀態碼,並顯示一個自定義的HTML錯誤消息。

要注意的是,返回給用戶的信息應該既明確又友好,以確保用戶理解爲什麼他們的請求沒有成功,並且知道下一步該做什麼。對於API服務,通常返回一個JSON對象,包含錯誤碼和錯誤信息會更加合適:

access_by_lua_block {
    -- 假設你已經進行了一些限流判斷...
    if should_limit then
        ngx.status = ngx.HTTP_TOO_MANY_REQUESTS -- 429 Too Many Requests
        ngx.header.content_type = 'application/json'
        ngx.say([[{"error": "rate_limit", "error_description": "Too many requests. Please try again later."}]])
        ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
    end
}

在這個例子中,我們使用了429狀態碼(太多請求),這是一個更具體的狀態碼,用來表示客戶端發送的請求已經超過了服務器願意處理的頻率。

三、如何動態更新限流策略,實時生效,不需要重啓Nginx

動態更新限流策略而不重啓Nginx服務,可以通過以下幾種方式實現:

  1. Lua共享字典(shared dictionaries):
    OpenResty提供了共享內存字典,這是一種在Nginx工作進程之間共享數據的機制。你可以使用共享字典來存儲限流配置,並且在Lua代碼中動態讀取這些配置。這樣,當你更新了共享字典中的配置時,不需要重啓Nginx,新的請求將會使用新的限流配置。

    例如,你可以定義一個共享字典來存儲限流速率:

    http {
        lua_shared_dict my_limit_req_store 10m;
    
        init_by_lua_block {
            local dict = ngx.shared.my_limit_req_store
            dict:set("rate", 1) -- 設置每秒請求數爲1
        }
    
        server {
            location / {
                access_by_lua_block {
                    local dict = ngx.shared.my_limit_req_store
                    local rate = dict:get("rate") -- 動態獲取當前的限流速率
                    -- 接下來使用這個rate值來進行限流...
                }
            }
        }
    }
    

    當你需要更新限流策略時,只需修改共享字典中的值即可。

  2. OpenResty的控制API:
    OpenResty提供了一個控制API,可以用來動態地調整運行時的Nginx配置。這個API可以通過Lua代碼來調用,從而實現不重啓服務的情況下更新配置。

  3. 外部配置服務:
    你可以將限流配置存儲在外部服務中,比如數據庫、配置文件或者分佈式配置系統(如etcd、Consul)。然後在Nginx的Lua代碼中定期輪詢這些服務,獲取最新的限流配置。

    access_by_lua_block {
        local http = require "resty.http"
        local httpc = http.new()
        local res, err = httpc:request_uri("http://config-service/get_rate_limit", {
            method = "GET",
            headers = {
                ["Content-Type"] = "application/json",
            }
        })
    
        if not res then
            ngx.log(ngx.ERR, "failed to request: ", err)
            return
        end
    
        local rate_limit = tonumber(res.body)
        if rate_limit then
            -- 應用新的限流策略...
        end
    }
    
  4. 信號控制:
    Nginx支持通過信號來進行控制,例如重新加載配置文件(nginx -s reload)。雖然這不是實時的,但是它不需要完全重啓Nginx進程,只是重新加載配置文件。如果限流策略是通過Nginx配置文件中的參數來控制的,這是一個可行的方法。

        選擇哪種方法取決於你的具體需求和環境。如果你需要非常快速地更新配置,並且配置更新操作非常頻繁,那麼使用Lua共享字典或者外部配置服務可能是更好的選擇。如果配置更新不是很頻繁,使用信號控制來重新加載Nginx配置可能就足夠了。

 

周國慶

2024/2/25

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章