资讯专栏INFORMATION COLUMN

解析React SSR 中的限流案例

3403771864 / 300人阅读

  本篇文章主要为大家讲述关于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,则水池中的水放空的时间即为激增流量的持续时间。

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




文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/130341.html

相关文章

  • 这个注解一次搞定限流与熔断降级:@SentinelResource

    摘要:实现熔断降级注解除了可以用来做限流控制之外,还能实现与类似的熔断降级策略。函数签名要求返回值类型必须与原函数返回值类型一致方法参数列表需要为空,或者可以额外多一个类型的参数用于接收对应的异常。若未配置和,则被限流降级时会将直接抛出。 在之前的《使用Sentinel实现接口限流》一文中,我们仅依靠引入Spring Cloud Alibaba对Sentinel的整合封装spring-clo...

    Lionad-Morotar 评论0 收藏0
  • spring cloud gateway 之限流

    摘要:常见的限流方式,比如适用线程池隔离,超过线程池的负载,走熔断的逻辑。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。,令牌桶每秒填充平均速率。 转载请标明出处: https://www.fangzhipeng.com本文出自方志朋的博客 在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方面是为了防止网络攻击。 常见的限流方式,...

    joy968 评论0 收藏0
  • Spring Cloud Gateway 扩展支持动态限流

    摘要:以流量为切入点,从流量控制熔断降级系统负载保护等多个维度保护服务的稳定性分布式系统的流量防卫兵。欢迎关注我们获得更多的好玩实践 之前分享过 一篇 《Spring Cloud Gateway 原生的接口限流该怎么玩》, 核心是依赖Spring Cloud Gateway 默认提供的限流过滤器来实现 原生RequestRateLimiter 的不足 配置方式 spring: clou...

    妤锋シ 评论0 收藏0
  • Spring Cloud Gateway 扩展支持动态限流

    摘要:以流量为切入点,从流量控制熔断降级系统负载保护等多个维度保护服务的稳定性分布式系统的流量防卫兵。欢迎关注我们获得更多的好玩实践 之前分享过 一篇 《Spring Cloud Gateway 原生的接口限流该怎么玩》, 核心是依赖Spring Cloud Gateway 默认提供的限流过滤器来实现 原生RequestRateLimiter 的不足 配置方式 spring: clou...

    beanlam 评论0 收藏0

发表评论

0条评论

3403771864

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<