從Dcard題目實作RATE LIMITING

剛好在網路上看到人家分享Dcard題目要實作Rate limiting,就讓我們來挑戰看看

Dcard 每天午夜都有大量使用者湧入抽卡,為了不讓伺服器過載,請設計一個 middleware:

  • 限制每小時來自同一個 IP 的請求數量不得超過 1000
  • 在 response headers 中加入剩餘的請求數量 (X-RateLimit-Remaining) 以及 rate limit 歸零的時間 (X-RateLimit-Reset)
  • 如果超過限制的話就回傳 429 (Too Many Requests)
  • 可以使用各種資料庫達成

題目來源:https://tech-blog.jameshsu.csie.org/post/programming-dcard-homework-2020/

初步想法

因為沒有特別限制語言,所以就使用我們最熟悉的Node.js來開發。

資料庫選擇,限制流量這件事沒有需要用到SQL的關聯性,所以選擇用NoSQL。考量到限流這件事必須要加快存取速度,故採用儲存在記憶體的主流NoSQL:Redis或Mencached。Mencached算是Redis的閹割版本,聽說FB早期也是使用Mencached。在以上題目中,基本上只要可以存取Key-Value就可以實踐,所以兩者皆可。Redis雖然比Mencached多了持久化,但在此也並非必要。在這邊文章會以Redis做為練習。

步驟1:建立基本的middleware

直接就先依照Express初步開發方式,建立一個新的Express

npm init // create new app
npm install redis --save // import redis
npm install express --save // import express
npm install dotenv // import .env

其餘教學可參考:https://expressjs.com/en/starter/hello-world.html

步驟2:安裝Redis

這邊我直接在本機端用Docker啟Redis,並使用npm redis套件,實際Run看看有沒有成功set/get。

const client = createClient(
{url: `redis://${process.env.CACHE_USER}:${process.env.CACHE_PASSWORD}@${process.env.CACHE_HOST}:${process.env.CACHE_PORT}`}
);

client.on('error', (err) => console.log('Redis Client Error', err));

await client.connect();

await client.set('key', 'value');
const value = await client.get('key');

console.log(value); // value

步驟3:使用static time window實作Rate limit

題目:限制每小時來自同一個 IP 的請求數量不得超過 1000

我們預計使用這樣的資料格式存取

{
 IP:req times
}

{
 127.0.0.1:1
}

而我們要如何取得使用者的IP呢?可以藉由req.header的方法得到使用者的IP。在拿取使用者IP的時候要注意前端來的資料都是不可信的!所以會有header被篡改的可能,詳細可以參考這一篇https://devco.re/blog/2014/06/19/client-ip-detection/

app.get('/',async (req, res) => {
  let user = req.headers.host || 'none';
  let checkUser = await client.exists(user)

  // User not exist
  if(checkUser == 0){
    let body = {
        'count': 1,
        'startTime': moment().unix()
      }
    client.set(user,JSON.stringify(body))
  }

  if(checkUser == 1){
    // user exists
    let reply = await client.get(user)
    let data = JSON.parse(reply)
    let currentTime = moment().unix()
    console.log(data.startTime);
    let difference = (currentTime - data.startTime)/60
    if(difference >= 1) {
      let body = {
        'count': 1,
        'startTime': moment().unix()
      }
      client.set(user,JSON.stringify(body))
      // allow the request
      res.send('new')
    }
    if(difference < 1) {
      if(data.count > 3) {
        return res.json({"error": 1, "message": "throttled limit exceeded..."})
      }
      // update the count and allow the request
      data.count++

      client.set(user,JSON.stringify(data))
      res.send(String(data.count))
    }
  }
})

參考網址:https://codeforgeek.com/building-api-rate-limiter-using-nodejs-redis/

https://engineering.classdojo.com/blog/2015/02/06/rolling-rate-limiter/

https://developer.redis.com/howtos/ratelimiting/stati

https://redis.com/redis-best-practices/basic-rate-limiting/

這邊過期的機制也可以使用redis內建的TTL

https://redis.io/docs/manual/cli/

Leave a Comment

發佈留言必須填寫的電子郵件地址不會公開。