文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

React SSR 中的限流案例详解

2024-04-02 19:55

关注

当对 React 应用进行页面加载或 SEO 优化时,我们一般绕不开 React SSR。但 React SSR 毕竟涉及到了服务端,有很多服务端特有的问题需要考虑,而限流就是其中之一。

所谓限流,就是当我们的服务资源有限、处理能力有限时,通过对请求或并发数进行限制从而保障系统正常运行的一种策略。本文会通过一个简单的案例来说明,为什么服务端需要进行限流。

为什么要限流

如下所示是一个简单的 nodejs 服务端项目:

const express = require('express')
const app = express()
app.get('/', async (req, res) => {
  // 模拟 SSR 会大量的占用内存
  const buf = Buffer.alloc(1024 * 1024 * 200, 'a')
  console.log(buf)
  res.end('end')
})
app.get('/another', async (req, res) => {
  res.end('another api')
})
const listener = app.listen(process.env.PORT || 2048, () => {
  console.log('Your app is listening on port ' + listener.address().port)
})

其中,我们通过 Buffer 来模拟 SSR 过程会大量的占用内存的情况。

然后,通过 docker build -t ssr . 指定将我们的项目打包成一个镜像,并通过以下命令运行一个容器:

docker run \
-it \
-m 512m \ # 限制容器的内存
--rm \
-p 2048:2048 \
--name ssr \
--oom-kill-disable \
ssr

我们将容器内存限制在 512m,并通过 --oom-kill-disable 指定容器内存不足时不关闭容器。

接下来,我们通过 autocannon 来进行一下压测:

autocannon -c 10 -d 1000 http://localhost:2048

通过, docker stats 可以看到容器的运行情况:

CONTAINER ID   NAME      CPU %     MEM USAGE / LIMIT   MEM %     NET I/O           BLOCK I/O         PIDS
d9c0189e2b56    ssr     0.00%     512MiB / 512MiB     99.99%    14.6kB / 8.65kB   41.9MB / 2.81MB   40

此时,容器内存已经全部被占用,服务对外失去了响应,通过 curl -m 5 http://localhost:2048 访问,收到了超时的错误提示:

curl: (28) Operation timed out after 5001 milliseconds with 0 bytes received

我们改造一下代码,使用 counter.js 来统计 QPS,并限制为 2:

const express = require('express')
const counter = require('./counter.js')
const app = express()
const limit = 2
let cnt = counter()
app.get(
  '/',
  (req, res, next) => {
    cnt(1)
    if (cnt() > limit) {
      res.writeHead(500, {
        'content-type': 'text/pain',
      })
      res.end('exceed limit')
      return
    }
    next()
  },
  async (req, res) => {
    const buf = Buffer.alloc(1024 * 1024 * 200, 'a')
    console.log(buf)
    res.end('end')
  }
)
app.get('/another', async (req, res) => {
  res.end('another api')
})
const listener = app.listen(process.env.PORT || 2048, () => {
  console.log('Your app is listening on port ' + listener.address().port)
})
// counter.js
module.exports = function counter(interval = 1000) {
  let arr = []
  return function cnt(number) {
    const now = Date.now()
    if (number > 0) {
      arr.push({
        time: now,
        value: number,
      })
      const newArr = []
      // 删除超出一秒的数据
      for (let i = 0, len = arr.length; i < len; i++) {
        if (now - arr[i].time > interval) continue
        newArr.push(arr[i])
      }
      arr = newArr
      return
    }
    // 计算前一秒的数据和
    let sum = 0
    for (let i = arr.length - 1; i >= 0; i--) {
      const {time, value} = arr[i]
      if (now - time <= interval) {
        sum += value
        continue
      }
      break
    }
    return sum
  }
}

此时,容器运行正常:

CONTAINER ID   NAME      CPU %     MEM USAGE / LIMIT   MEM %     NET I/O           BLOCK I/O        PIDS
3bd5aa07a3a7   ssr     88.29%    203.1MiB / 512MiB   39.67%    24.5MB / 48.6MB   122MB / 2.81MB   40

虽然此时访问 / 路由会收到错误:

curl -m 5  http://localhost:2048
exceed limit

但是 /another 却不受影响:

curl -m 5  http://localhost:2048/another
another api

由此可见,限流确实是系统进行自我保护的一个比较好的方法。

令牌桶算法

常见的限流算法有“滑动窗口算法”、“令牌桶算法”,我们这里讨论 “令牌桶算法” 。在令牌桶算法中,存在一个桶,容量为 burst 。该算法以一定的速率(设为 rate )往桶中放入令牌,超过桶容量会丢弃。每次请求需要先获取到桶中的令牌才能继续执行,否则拒绝。

根据令牌桶的定义,我们实现令牌桶算法如下:

export default class TokenBucket {
  private burst: number
  private rate: number
  private lastFilled: number
  private tokens: number
  constructor(burst: number, rate: number) {
    this.burst = burst
    this.rate = rate
    this.lastFilled = Date.now()
    this.tokens = burst
  }
  setBurst(burst: number) {
    this.burst = burst
    return this
  }
  setRate(rate: number) {
    this.rate = rate
    return this
  }
  take() {
    this.refill()
    if (this.tokens > 0) {
      this.tokens -= 1
      return true
    }
    return false
  }
  refill() {
    const now = Date.now()
    const elapse = now - this.lastFilled
    this.tokens = Math.min(this.burst, this.tokens + elapse * (this.rate / 1000))
    this.lastFilled = now
  }
}

然后,按照如下方式使用:

const tokenBucket = new TokenBucket(5, 10)
if (tokenBucket.take()) {
  // Do something
} else {
  // refuse
}

简单解释一下这个算法,调用 take 时,会先执行 refill 先往桶中进行填充。填充的方式也很简单,首先计算出与上次填充的时间间隔 elapse 毫秒,然后计算出这段时间内应该补充的令牌数,因为令牌补充速率是 rate 个/秒,所以需要补充的令牌数为:

elapse * (this.rate / 1000)

又因为令牌数不能超过桶的容量,所以补充后桶中的令牌数为:

Math.min(this.burst, this.tokens + elapse * (this.rate / 1000))

注意,这个令牌数是可以为小数的。

令牌桶算法具有以下两个特点:

T = burst / (M - rate) // rate < M

可以理解为一个水池里面有 burst 的水量,进水的速率为 rate ,出水的速率为 M ,则净出水速率为 M-rate ,则水池中的水放空的时间即为激增流量的持续时间。

到此这篇关于React SSR 之限流的文章就介绍到这了,更多相关React SSR内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-前端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯