我目前正在探索解决方案,以按每月请求总数限制对NodeJS上的API端点的访问。
例如,我希望免费计划用户访问/api
端点,每个月最多可以访问100个请求,高级计划用户每月可以访问5000个请求。
解决这个问题的天真的方法是实施护照中间件来获取用户的计划,然后跟踪计数:
app.get("/api", requireAuth, async (req, res, next) => {
try {
// Check if user ran out of requests
if (req.user.apiRequestsLeft === 0) {
res.send("You ran out of API requests!")
} else {
// Decrement the allocated requests
req.user.apiRequestsLeft--;
await req.user.save();
res.send(user)
}
} catch (err) {
next(err);
}
});
我担心的是:
答案 0 :(得分:2)
必须更新MongoDB文档的性能/可伸缩性问题 每次有请求时-这可行还是我遇到问题 应用程序何时增长?
当然。您很快就会遇到大量的mongoDB流量,它将遇到性能瓶颈。我认为,您应该使用速度更快的内存数据库,例如Redis来处理这种情况。您甚至可以将Redis用作session-store
,这将减少MongoDB上的负载。这样,MongoDB可以用于其他业务查询。
重置计数-如果这是每天的cronjob, 每个用户的“注册”时间戳,计算一个月 已经通过并相应地重置了分配的请求,或者是否存在 设计这样的更好的方法?
更好的方法是在中间件本身中实现重置部分。
这是一些解释我解决方案的代码。
Quota
对象的示例设计为:
{
type: "FREE_USER", /** or "PREMIUM_USER" */
access_limit: 100, /** or 5000 */
exhausted_requests: 42 /** How many requests the user has made so far this month */
last_reset_timestamp: 1547796508728 /** When was the exhausted_requests set to 0 last time */
}
采用这种设计。您的检查配额的中间件看起来像:
const checkQuota = async (req, res, next) => {
const user = req.user;
const userQuotaStr = await redis.getAsync(user.id)
let userQuota;
/** Check if we have quota information about user */
if (userQuotaStr != null) {
/** We have previously saved quota information */
userQuota = JSON.parse(userQuotaStr);
/**
* Check if we should reset the exhausted_requests
* Assuming that all the requests are reset on the First Day of each month.
*/
if ( isStartOfMonth() ) {
/**
* It is First Day of the month. We might need to reset the `exhausted_requests`
* Check the difference between `Date.now()` and `userQuota.last_reset_timestamp`
* to determine whether we should reset or not
*/
if ( shouldResetTimeStamp(userQuota.last_reset_timestamp) ) {
userQuota.exhausted_requests = 0
userQuota.last_reset_timestamp = Date.now()
}
}
} else {
/** We do not have previously saved quota information. Prepare one */
userQuota = {
type: user.type,
access_limit: user.access_limit,
exhausted_requests: 0,
last_reset_timestamp: Date.now()
}
}
/** Incredement the counter to account the current request */
userQuota.exhausted_requests++
/** Update in database */
redis.set(user.id, JSON.stringify(userQuota))
if ( userQuota.exhausted_requests >= userQuota.access_limit ) {
/** User has reached the quota limit. Deny the request. set with 401 or 403 status code */
} else {
/** User can access the API. call next() */
}
}
当然,该代码段是不完整的。它只是为您提供有关编写中间件的想法。
以下是您可以将中间件用于API的方式:
/** If requests to routes are under the quota */
app.get("/api/quota-routes", requireAuth, checkQuota, /** Mount the actual middleware here */)
/** If requests to routes are unlimited, just remove the checkQuota middleware */
app.get("/api/unlimited-routes", requireAuth, /** Mount the actual middleware here */)
答案 1 :(得分:0)
rate-limiter-flexible软件包可帮助您创建计数器并自动使计数器过期。
const opts = {
storeClient: mongoConn,
points: 5000, // Number of points
duration: 60 * 60 * 24 * 30, // Per month
};
const rateLimiterMongo = new RateLimiterMongo(opts);
const rateLimiterMiddleware = (req, res, next) => {
// req.userId should be set before this middleware
const key = req.userId ? req.userId : req.ip;
const pointsToConsume = req.userId ? 1 : 50;
rateLimiterMongo.consume(key, pointsToConsume)
.then(() => {
next();
})
.catch(_ => {
res.status(429).send('Too Many Requests');
});
};
app.use(rateLimiterMiddleware);
请注意,此示例未绑定到日历月,而是计算了下一个月内第一个事件的事件。您可以使用 block严格将计数器的到期日与日历月份联系起来。
在基本服务器上,此代码应该可以轻松地每秒处理大约1k-2k请求。您也可以使用Redis limiter或Mongo limiter with sharding options。
此外,它提供In-memory Block strategy以避免对MongoDB / Redis /任何商店的请求过多。
或者,使用get中的rate-limiter-flexible方法来减少不必要的计数器更新量。 get
方法比增量方法快得多。