API開發中如何使用限速應對大規模訪問

ThinkJS發表於2018-11-12

編者注:俗話說的好 “併發不夠,機器來湊”,當我們面對高併發請求的時候增加機器是最簡單也是最土豪的做法。不過在資源有限的情況除了去優化程式碼我們又該怎麼辦呢?今天我們請來了 @有馬 同學為我們分享一下他在這方面的經驗,希望能幫助到大家。

———

想要開發牢固的Web API只考慮安全是不夠的,還有一點我們需要考慮,那就是應對大規模訪問的對策。不僅是Web API服務,任何在網路上公開的服務都會時不時地遇到來自外部的大規模訪問,比如“鹿晗關曉彤公佈戀情”這種實時熱點。當伺服器遇到大規模訪問時,為了處理這些訪問會耗盡資源,進而無法提供服務。這時不僅是這些大規模訪問,任何人都無法和伺服器端建立連線。

我們可以通過程式毫不費力的訪問Web API,所以API伺服器更容易遇到訪問負載高的情況,針對這個問題,和普通的Web應用一樣,我們可以對API服務進行擴容,這是正確的做法,但本文不對擴容方案展開討論。接下來會討論限速在應對大規模訪問時一些重要的點,以及在ThinkJS開發的專案中應該怎樣做。

限制使用者的訪問

為了解決突然出現大規模訪問的問題,最現實的方法是對每個使用者的訪問次數進行限制。也就是確定單個使用者在單位時間裡最大的訪問次數,如果使用者已經超過了最大訪問次數,使用者再次訪問時,服務端將會直接拒絕並返回錯誤資訊。比如設定一個使用者10分鐘內只允許呼叫20次獲取簡訊驗證碼的介面,那麼當使用者在10分鐘內發起第21次請求時,伺服器端便會返回錯誤資訊,10分鐘之後才會恢復訪問。如果進行訪問限速,就要先解決下面三個問題:

  • 如何確定限速的數值
  • 如何確定限速時間單位
  • 在什麼時候重置限速的數值

確定限速數值

對資料頻繁更新的查詢類API而言,使用者需要頻繁的訪問的到最新的資料,如果設定1小時只能訪問10次的話,使用者肯定不滿意,轉而去用可以替代的服務。訪問限速的初衷是為了應對伺服器短時間內遭遇大規模訪問不堪重負從而無法提供服務,但如果讓使用者用起來不方便就得不償失了,所以要儘可能的瞭解提供的API在什麼情況下被使用,然後決定限速的數值。

確定限速時間單位

根據線上服務的不同,有些會以一天作為訪問次數的時間單位,不過這對很多API來說有點長了,假設使用者正在寫指令碼訪問API,開始並不清楚訪問次數的時間單位,那就可能需要讓他等24個小時才能繼續訪問API,或者換一個賬號。如果我們以10分鐘作為訪問次數的時間單位,如果超出訪問次數限制,也只需要等10分鐘就能繼續訪問了。雖然單位時間的設定和API返回的資料密切相關,但大部分已公開的API都設定了都設定了1小時左右的單位時間。

確定重置限速數值的時間

當使用者超出訪問上限值時,服務端該如何返回響應訊息呢?這種情況下可以返回HTTP協議中備好的“429 Too Many Request”狀態碼。429狀態碼在2012年4月釋出的RFC 6585中定義,當特定使用者在一定時間內發起的請求次數過多時,伺服器端可以返回該狀態碼錶示出錯。RFC 文件中對該狀態碼描述如下:

429 Too Many Requests

   The 429 status code indicates that the user has sent too many
   requests in a given amount of time ("rate limiting").

   The response representations SHOULD include details explaining the
   condition, and MAY include a Retry-After header indicating how long
複製程式碼

通過上面的描述可以知道,響應訊息中應該包含錯誤的詳細資訊,並且可以通過Retry-After告知使用者需要等待多長時間才能訪問API。Retry-After首部表示客戶端需要等待多長時間才能再次訪問。RFC文件中用 MAY 標記該首部,表示即使不傳送該首部也不會有什麼問題,只是在響應體加上該首部會顯得更加友好。

另外,Retry-After並不是 429 狀態碼專用的響應首部。該首部在HTTP 1.1的RFC 7231中定義,它也同樣包含在帶有503和3xx系列的響應體中。而且Retry-After首部用秒數來指定時間,還可以使用詳細的日期資訊,可以看一下RFC文件中的描述:

Retry-After

   Servers send the "Retry-After" header field to indicate how long the
   user agent ought to wait before making a follow-up request.  When
   sent with a 503 (Service Unavailable) response, Retry-After indicates
   how long the service is expected to be unavailable to the client.
   When sent with any 3xx (Redirection) response, Retry-After indicates
   the minimum time that the user agent is asked to wait before issuing
   the redirected request.

   The value of this field can be either an HTTP-date or a number of
   seconds to delay after the response is received.

     Retry-After = HTTP-date / delay-seconds

   A delay-seconds value is a non-negative decimal integer, representing
   time in seconds.

     delay-seconds  = 1*DIGIT
複製程式碼

通過HTTP響應傳遞限速資訊

在實施訪問限速的過程中,如果能將當前使用者訪問次數限制、已使用的訪問次數以及何時重置訪問限速等資訊告訴使用者,會顯得非常友好。如果不返回這些資訊的話,使用者可能為了確定限速是否解除而多次嘗試訪問介面API,這樣一來無疑又增加了伺服器的壓力。

限速資訊可以放在響應訊息首部,另一種是作為響應訊息體資料的一部分,目前將限速資訊放在響應訊息首部的方式成為事實上的標準。

首部名 說明 型別
X-RateLimit-Limit 單位時間的訪問上限 Integer
X-RateLimit-Remaining 剩餘的訪問次數 Integer
X-RateLimit-Reset 訪問次數重置時間 UTC epoch seconds

看一下GitHub的限速策略,GitHub就使用了上面三個響應首部,沒有帶Retry-After首部。對於認證的請求每小時可以訪問5000次,沒有認證的請求每小時訪問60次。

Twitter限速策略的時間視窗是15分鐘,比GitHub的時間視窗小很多,因為Twitter的資料更新的相對較較快,時間視窗設定小一些才能滿足使用者獲取最新資料的需求。Twitter使用類似上面三個的響應首部傳達限速資訊x-rate-limit-limit,x-rate-limit-remaining,x-rate-limit-reset。對於GET請求有兩種初始方案,一種是15分鐘15次請求,另一種是15分鐘180次請求,並且只允許認證訪問。

通過對比GitHub和Twitter的限速策略,可以知道只要準確傳達限速資訊,響應頭部完全可以自己定義,重點是語義明確,且不能和其他標準首部衝突。

在ThinkJS中實現API限速控制

要實現API訪問限速,需要對每個使用者及應用訪問API的次數進行計數,一般會使用Redis等鍵值對儲存來記錄。ThinkJS 結合自己的路由對映方式實現了think-ratelimiter中介軟體對action進行限速,你需要在middleware.js裡進行如下配置,就可以實現簡單的限速策略。

// in middleware.js
const redis = require('redis');
const { port, host, password } = think.config('redis');
const db = redis.createClient(port, host, { password });
const ratelimiter = require('think-ratelimiter');

module.exports = {
  // after router middleware
  {
    handle: ratelimiter,
    options: {
      db,
      errorMessage: 'Sometimes You Just Have To Slow Down',
      headers: {
        remaining: 'X-RateLimit-Remaining',
        reset: 'X-RateLimit-Reset',
        total: 'X-RateLimit-Limit'
      },
      resources: {
        'test/test': { // key 是 controller/action 的拼接
          id: ctx => ctx.ip,
          max: 5,
          duration: 7000 // ms
        }
      }
    }
  },
}
複製程式碼

響應體首部X-RateLimit-Reset表示可以恢復訪問的時間,同時也會帶著Retry-After首部,它的值是距離恢復時間的秒數。

總結

在ThinkJS開發的Web應用中,可以使用中介軟體然後新增配置實現簡單的限速,如果你提供的web API服務訪問量比較大或者需要付費訪問等功能,就需要在真正的邏輯前加一層來做限速相關的事情,在ThinkJS中可以實現一個services/ratelimit.js,然後在專案的base controller中實現限速等邏輯。

參考資料:

相關文章