API開(kāi)發(fā)使用限速應(yīng)對(duì)大規(guī)模訪問(wèn)

image

想要開(kāi)發(fā)牢固的Web API只考慮安全是不夠的,還有一點(diǎn)我們需要考慮,那就是應(yīng)對(duì)大規(guī)模訪問(wèn)的對(duì)策。不僅是Web API服務(wù),任何在網(wǎng)絡(luò)上公開(kāi)的服務(wù)都會(huì)時(shí)不時(shí)地遇到來(lái)自外部的大規(guī)模訪問(wèn),比如“鹿晗關(guān)曉彤公布戀情”這種實(shí)時(shí)熱點(diǎn)。當(dāng)服務(wù)器遇到大規(guī)模訪問(wèn)時(shí),為了處理這些訪問(wèn)會(huì)耗盡資源,進(jìn)而無(wú)法提供服務(wù)。這時(shí)不僅是這些大規(guī)模訪問(wèn),任何人都無(wú)法和服務(wù)器端建立連接。

我們可以通過(guò)程序毫不費(fèi)力的訪問(wèn)Web API,所以API服務(wù)器更容易遇到訪問(wèn)負(fù)載高的情況,針對(duì)這個(gè)問(wèn)題,和普通的Web應(yīng)用一樣,我們可以對(duì)API服務(wù)進(jìn)行擴(kuò)容,這是正確的做法,但本文不對(duì)擴(kuò)容方案展開(kāi)討論。接下來(lái)會(huì)討論限速在應(yīng)對(duì)大規(guī)模訪問(wèn)時(shí)一些重要的點(diǎn),以及在ThinkJS開(kāi)發(fā)的項(xiàng)目中應(yīng)該怎樣做。

限制用戶的訪問(wèn)

為了解決突然出現(xiàn)大規(guī)模訪問(wèn)的問(wèn)題,最現(xiàn)實(shí)的方法是對(duì)每個(gè)用戶的訪問(wèn)次數(shù)進(jìn)行限制。也就是確定單個(gè)用戶在單位時(shí)間里最大的訪問(wèn)次數(shù),如果用戶已經(jīng)超過(guò)了最大訪問(wèn)次數(shù),用戶再次訪問(wèn)時(shí),服務(wù)端將會(huì)直接拒絕并返回錯(cuò)誤信息。比如設(shè)置一個(gè)用戶10分鐘內(nèi)只允許調(diào)用20次獲取短信驗(yàn)證碼的接口,那么當(dāng)用戶在10分鐘內(nèi)發(fā)起第21次請(qǐng)求時(shí),服務(wù)器端便會(huì)返回錯(cuò)誤信息,10分鐘之后才會(huì)恢復(fù)訪問(wèn)。如果進(jìn)行訪問(wèn)限速,就要先解決下面三個(gè)問(wèn)題:

  • 如何確定限速的數(shù)值
  • 如何確定限速時(shí)間單位
  • 在什么時(shí)候重置限速的數(shù)值

確定限速數(shù)值

對(duì)數(shù)據(jù)頻繁更新的查詢類API而言,用戶需要頻繁的訪問(wèn)的到最新的數(shù)據(jù),如果設(shè)置1小時(shí)只能訪問(wèn)10次的話,用戶肯定不滿意,轉(zhuǎn)而去用可以替代的服務(wù)。訪問(wèn)限速的初衷是為了應(yīng)對(duì)服務(wù)器短時(shí)間內(nèi)遭遇大規(guī)模訪問(wèn)不堪重負(fù)從而無(wú)法提供服務(wù),但如果讓用戶用起來(lái)不方便就得不償失了,所以要盡可能的了解提供的API在什么情況下被使用,然后決定限速的數(shù)值。

確定限速時(shí)間單位

根據(jù)在線服務(wù)的不同,有些會(huì)以一天作為訪問(wèn)次數(shù)的時(shí)間單位,不過(guò)這對(duì)很多API來(lái)說(shuō)有點(diǎn)長(zhǎng)了,假設(shè)使用者正在寫腳本訪問(wèn)API,開(kāi)始并不清楚訪問(wèn)次數(shù)的時(shí)間單位,那就可能需要讓他等24個(gè)小時(shí)才能繼續(xù)訪問(wèn)API,或者換一個(gè)賬號(hào)。如果我們以10分鐘作為訪問(wèn)次數(shù)的時(shí)間單位,如果超出訪問(wèn)次數(shù)限制,也只需要等10分鐘就能繼續(xù)訪問(wèn)了。雖然單位時(shí)間的設(shè)定和API返回的數(shù)據(jù)密切相關(guān),但大部分已公開(kāi)的API都設(shè)置了都設(shè)置了1小時(shí)左右的單位時(shí)間。

確定重置限速數(shù)值的時(shí)間

當(dāng)用戶超出訪問(wèn)上限值時(shí),服務(wù)端該如何返回響應(yīng)消息呢?這種情況下可以返回HTTP協(xié)議中備好的“429 Too Many Request”狀態(tài)碼。429狀態(tài)碼在2012年4月發(fā)布的RFC 6585中定義,當(dāng)特定用戶在一定時(shí)間內(nèi)發(fā)起的請(qǐng)求次數(shù)過(guò)多時(shí),服務(wù)器端可以返回該狀態(tài)碼表示出錯(cuò)。RFC 文檔中對(duì)該狀態(tài)碼描述如下:

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

通過(guò)上面的描述可以知道,響應(yīng)消息中應(yīng)該包含錯(cuò)誤的詳細(xì)信息,并且可以通過(guò)Retry-After告知用戶需要等待多長(zhǎng)時(shí)間才能訪問(wèn)API。Retry-After首部表示客戶端需要等待多長(zhǎng)時(shí)間才能再次訪問(wèn)。RFC文檔中用 MAY 標(biāo)記該首部,表示即使不發(fā)送該首部也不會(huì)有什么問(wèn)題,只是在響應(yīng)體加上該首部會(huì)顯得更加友好。

另外,Retry-After并不是 429 狀態(tài)碼專用的響應(yīng)首部。該首部在HTTP 1.1的RFC 7231中定義,它也同樣包含在帶有503和3xx系列的響應(yīng)體中。而且Retry-After首部用秒數(shù)來(lái)指定時(shí)間,還可以使用詳細(xì)的日期信息,可以看一下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

通過(guò)HTTP響應(yīng)傳遞限速信息

在實(shí)施訪問(wèn)限速的過(guò)程中,如果能將當(dāng)前用戶訪問(wèn)次數(shù)限制、已使用的訪問(wèn)次數(shù)以及何時(shí)重置訪問(wèn)限速等信息告訴用戶,會(huì)顯得非常友好。如果不返回這些信息的話,用戶可能為了確定限速是否解除而多次嘗試訪問(wèn)接口API,這樣一來(lái)無(wú)疑又增加了服務(wù)器的壓力。

限速信息可以放在響應(yīng)消息首部,另一種是作為響應(yīng)消息體數(shù)據(jù)的一部分,目前將限速信息放在響應(yīng)消息首部的方式成為事實(shí)上的標(biāo)準(zhǔn)。

首部名 說(shuō)明 類型
X-RateLimit-Limit 單位時(shí)間的訪問(wèn)上限 Integer
X-RateLimit-Remaining 剩余的訪問(wèn)次數(shù) Integer
X-RateLimit-Reset 訪問(wèn)次數(shù)重置時(shí)間 UTC epoch seconds

看一下GitHub的限速策略,GitHub就使用了上面三個(gè)響應(yīng)首部,沒(méi)有帶Retry-After首部。對(duì)于認(rèn)證的請(qǐng)求每小時(shí)可以訪問(wèn)5000次,沒(méi)有認(rèn)證的請(qǐng)求每小時(shí)訪問(wèn)60次。

Twitter限速策略的時(shí)間窗口是15分鐘,比GitHub的時(shí)間窗口小很多,因?yàn)門witter的數(shù)據(jù)更新的相對(duì)較較快,時(shí)間窗口設(shè)置小一些才能滿足使用者獲取最新數(shù)據(jù)的需求。Twitter使用類似上面三個(gè)的響應(yīng)首部傳達(dá)限速信息x-rate-limit-limit,x-rate-limit-remaining,x-rate-limit-reset。對(duì)于GET請(qǐng)求有兩種初始方案,一種是15分鐘15次請(qǐng)求,另一種是15分鐘180次請(qǐng)求,并且只允許認(rèn)證訪問(wèn)。

通過(guò)對(duì)比GitHub和Twitter的限速策略,可以知道只要準(zhǔn)確傳達(dá)限速信息,響應(yīng)頭部完全可以自己定義,重點(diǎn)是語(yǔ)義明確,且不能和其他標(biāo)準(zhǔn)首部沖突。

在ThinkJS中實(shí)現(xiàn)API限速控制

要實(shí)現(xiàn)API訪問(wèn)限速,需要對(duì)每個(gè)用戶及應(yīng)用訪問(wèn)API的次數(shù)進(jìn)行計(jì)數(shù),一般會(huì)使用Redis等鍵值對(duì)存儲(chǔ)來(lái)記錄。ThinkJS 結(jié)合自己的路由映射方式實(shí)現(xiàn)了think-ratelimiter中間件對(duì)action進(jìn)行限速,你需要在middleware.js里進(jìn)行如下配置,就可以實(shí)現(xiàn)簡(jiǎn)單的限速策略。

// 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: ratelimit,
    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
        },
        'admin/api/user': { // 多模塊 key 是 module/controller/action 的拼接
            id: ctx => ctx.ip,
            max: 5,
            duration: 5000
        }
      }
    }
  },
}

響應(yīng)體首部X-RateLimit-Reset表示可以恢復(fù)訪問(wèn)的時(shí)間,同時(shí)也會(huì)帶著Retry-After首部,它的值是距離恢復(fù)時(shí)間的秒數(shù)。

總結(jié)

在ThinkJS開(kāi)發(fā)的Web應(yīng)用中,可以使用中間件然后添加配置實(shí)現(xiàn)簡(jiǎn)單的限速,如果你提供的web API服務(wù)訪問(wèn)量比較大或者需要付費(fèi)訪問(wèn)等功能,就需要在真正的邏輯前加一層來(lái)做限速相關(guān)的事情,在ThinkJS中可以實(shí)現(xiàn)一個(gè)services/ratelimit.js,然后在項(xiàng)目的base controller中實(shí)現(xiàn)限速等邏輯。

參考鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容