API开发使用限速应对大规模访问

image

想要开发牢固的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: 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
        }
      }
    }
  },
}

响应体首部X-RateLimit-Reset表示可以恢复访问的时间,同时也会带着Retry-After首部,它的值是距离恢复时间的秒数。

总结

在ThinkJS开发的Web应用中,可以使用中间件然后添加配置实现简单的限速,如果你提供的web API服务访问量比较大或者需要付费访问等功能,就需要在真正的逻辑前加一层来做限速相关的事情,在ThinkJS中可以实现一个services/ratelimit.js,然后在项目的base controller中实现限速等逻辑。

参考链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容