编者注:俗话说的好 “并发不够,机器来凑”,当我们面对高并发请求的时候增加机器是最简单也是最土豪的做法。不过在资源有限的情况除了去优化代码我们又该怎么办呢?今天我们请来了 @有马 同学为我们分享一下他在这方面的经验,希望能帮助到大家。
———
想要开发牢固的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
中实现限速等逻辑。
参考资料:
因为用egg.js开发有段时间了,此前也有关注过thinkjs,所以基于楼主的think-ratelimiter 实现了egg版的egg-ratelimiter
- 支持egg [js] [ts]
- 支持Controller控制器内分别对请求速率进行配置
详情查看egg-ratelimiter