天天看點

海量請求下的接口并發解決方案

作者:浮生若夢3

設定一個場景,假如一個商品接口在某段時間突然上升,會怎麼辦?

對于這個問題,在電商高并發系統中,對接口的保護一般采用:緩存、限流、降級 來操作。

假設該接口已經接受過風控的處理,過濾掉一半的機器人腳本請求,剩下都是人為的下單請求。

服務限流

限流 主要的目的是通過對并發通路/請求進行限速,或者對一個時間視窗内的請求進行限速,一旦達到限制速率則可以拒絕服務、排隊或等待、降級等處理。

限流算法

1. 漏鬥算法

漏桶算法 是當請求到達時直接放入漏桶,如果目前容量已達到上限(限流值),則進行丢棄或其他政策(觸發限流政策)。漏桶以固定的速率(根據服務吞吐量)進行釋放通路請求(即請求通過),直到漏桶為空。

漏鬥算法的思想就是,不管你來多少請求,我的接口消費速度一定是小于等于流出速率的門檻值的。

海量請求下的接口并發解決方案

可以基于消息隊列來實作。

2. 令牌桶算法

令牌桶算法 是程式以v(v = 時間周期 / 限流值)的速度向令牌桶中增加令牌,直到令牌桶滿,請求到達時向令牌桶請求令牌,如果擷取成功則通過請求,如果擷取失敗觸發限流政策。

令牌桶算法和漏鬥算法的思想差别在于,前者可以允許突發請求的發生。

海量請求下的接口并發解決方案

3. 滑窗算法

滑窗算法 是将一個時間周期分為N個小周期,分别記錄每個小周期内通路次數,并且根據時間滑動删除過期的小周期。

如下圖所示,假設時間周期為1分鐘,将1分鐘再分為2個小周期,統計每個小周期的通路數量,則可以看到,第一個時間周期内,通路數量為75,第二個時間周期内,通路數量為100,如果一個時間周期内所有的小周期總和超過100的話,則會觸發限流政策。

海量請求下的接口并發解決方案

Sentinel的實作 和 TCP滑窗。

接入層限流

Nginx限流

Nginx 限流采用的是漏桶算法。

它可以根據用戶端特征,限制其通路頻率,用戶端特征主要指 IP、UserAgent等。使用 IP 比 UserAgent 更可靠,因為 IP 無法造假,UserAgent 可随意僞造。

limit_req子產品基于IP:

http://nginx.org/en/docs/http/ngx_http_limit_req_module.html

tgngine:

http://tengine.taobao.org/document_cn/http_limit_req_cn.html

本地接口限流

Semaphore

Java 并發庫 的 Semaphore 可以很輕松完成信号量控制,Semaphore 可以控制某個資源可被同時通路的個數,通過 acquire() 擷取一個許可,如果沒有就等待,而 release() 釋放一個許可。

假如我們對外提供一個服務接口,允許最大并發數為40,我們可以這樣:

private final Semaphore permit = new Semaphore(40, true);

public void process(){

    try{
        permit.acquire();
        //TODO 處理業務邏輯

    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        permit.release();
    }
}           

具體的 Semaphore 實作參考源碼。

分布式接口限流

使用消息隊列

不管是用MQ中間件,或是Redis的List實作的消息隊列,都可以作為一個 緩沖隊列 來使用。思想就是基于漏鬥算法。

當對于一個接口請求達到一定門檻值時,就可以啟用消息隊列來進行接口資料的緩沖,并根據服務的吞吐量來消費資料。

海量請求下的接口并發解決方案

服務降級

在接口做好風控的前提下,發現了接口請求的并發量迅速上升,我們可以啟用兜底方案,進行服務降級。

一般服務降級應該用來對一些 不重要 或 不緊急 的服務或任務進行服務的 延遲使用 或 暫停使用。

降級方案

停止邊緣業務

比如淘寶雙11前,就不可以查詢三個月前的訂單,對邊緣業務進行降級,保證核心業務的高可用。

拒絕請求

在接口請求并發量大于門檻值,或是接口出現大量失敗請求等等突發情況,可以拒絕一些通路請求。

拒絕政策

  • 随機拒絕:随機拒絕超過門檻值的請求 。
  • 拒絕舊請求:按照請求的時間,優先拒絕更早收到的請求。
  • 拒絕非核心請求:根據系統業務設定核心請求清單,将非核心清單内的請求拒絕掉。

恢複方案

在實作服務降級之後,對于突增流量我們可以繼續注冊多個消費者服務來應對并發量,之後我們再對一些伺服器進行慢加載。

降級具體實作參考其他文章。

資料緩存

在接口做好風控的前提下,發現了接口請求的并發量迅速上升,我們可以分以下幾個操作執行:

  • 對通路請求使用分布式鎖進行阻塞。
  • 在這個短時間中,我們可以将對應操作行的熱點資料,緩存在緩存中間件中。
  • 放行請求後,讓所有請求優先操作緩存資料。
  • 再将操作的結果通過消息隊列發送給消費接口慢慢消費。
海量請求下的接口并發解決方案

緩存問題

假設我們操作的是一個庫存接口,此時資料庫中隻有100個庫存。

那假如此時我們将一條資料放入緩存中,如果所有的請求都來通路這個緩存,那它還是被打挂,我們該怎麼操作?

讀寫分離

第一種想法,讀寫分離。

使用Redis的哨兵叢集模式來進行主從複制的讀寫分離操作。讀的操作肯定大于寫操作,等庫存被消費到0時,讀操作直接快速失敗。

海量請求下的接口并發解決方案

負載均衡

第二種想法,負載均衡。

在緩存資料後,如果所有請求都來緩存中操作這個庫存,不管是加悲觀鎖還是樂觀鎖,并發率都很低,此時我們可以對這個庫存進行拆分。

我們可以參照 ConcurrentHashMap 中的 counterCells 變量的設計思想,将100個庫存拆分到10個緩存服務中,每個緩存服務有10個緩存,然後我們再對請求進行負載均衡到各個緩存服務上。

但是這種方式會有問題,如果大部分使用者被hash到同一個緩存上,導緻其他緩存沒有被消費,卻傳回沒有庫存,這是不合理的。

海量請求下的接口并發解決方案

page cache

第三種想法,page cache。

大部分軟體架構其實都用到了這種方法,比如linux核心的硬碟寫入、mysql的刷盤等等,即将短時間内的寫操作聚合結果寫入,所有的寫操作在緩存内完成。