# 解析React SSR 中的限流案例

> 作者/来源: UCloud 运营管理员
> 发布时间: 2023-03-17T00:28:00.000Z
> 分类: 云主机
> 标签: AI, Docker
> 原文链接: http://117.50.162.249:3000/yun/articles/569

---

# 解析React SSR 中的限流案例

> 来源: https://www.ucloud.cn/yun/130341.html
> 作者: 3403771864
> 发布日期: 发布于2023-03-17 08:28

本篇文章主要为大家讲述关于ReactSSR之限流，其实我们都知道React SSR是涉及到服务端的，因此，我们先需要考虑到很多的服务器端问题，下面就为大家举例说明。

　　当简单来说， React 的应用进行页面加载或 SEO 优化时，都会想到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))
```

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

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

　　当外部请求的 QPSM大于令牌补充的速率rate时，长期来看，最终有效的 QPS 会趋向于rate。这个很好理解，拉的总不可能比吃的多吧。

　　因为令牌桶可以存下burst个令牌，所以可以允许短时间的激增流量，持续的时间为：

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

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

     本文内容到此都讲述完毕，欢迎大家关注后续更多精彩内容。