# 系统设计 01 - 背压 backpressure

# 0x00 前言

最近看到一篇讲 backpressure 的文章,今天来聊一聊背压问题。

# 0x01 什么是背压

按照百度的说法

指运动流体在密闭容器中沿其路径(譬如管路或风通路)流动时,由于受到障碍物或急转弯道的阻碍而被施加的与运动方向相反的压力

简单来说,水箱入水多,出水少

Image

在正式进入文章之前,务必要注意,

  1. 「背压现象」指的是一种生产者速率大于消费者速率的现象。
  2. 「背压机制」指的是一种预防背压现象出现的机制。

# 0x02 常见的背压场景

Producer 产出多 Consumer 来不及消费完

小到文件的 IO, 在请求到响应的整个生命周期,大到成白上千的数据计算任务,都会存在背压问题。

  1. 读写文件 - 读快写慢 假设读速度 150MB/s 写速度 100MB/s, 那么每一秒就必须缓冲 50MB, 也就是 20 多秒就接近 1GB, 可想而知,如果不注意策略的话,内存很快就撑爆。
  2. 服务间通讯 - 客户端对服务 A qps 为 100, 服务 A 需要请求内部服务 B, B 的 QPS 为 75, 如果不做额外的处理,服务 A 每秒钟就会积压 25 个请求。
  3. 消息队列中瞬时消息过多 - kafka 内瞬间累积了海量的消息。
  4. 爬虫速度过快,解析 worker 无法在相同时间内完成解析入库。
  5. UI 渲染,在比较热闹的直播页面,海量的用户发送弹幕,而用户界面很容易收到并且渲染海量消息而崩溃。
  6. 用户支付完成后跳转到支付成功页面。
  7. 今天花三十分钟写了 300 个 BUG, 导致测试妹纸直接崩溃。

# 0x03 背压解决策略

  1. 减少数据产出速率
  2. 增加数据消费速率
  3. 缓冲数据 use buffer
  4. 丢弃数据

# 场景 1: 读写文件

https://nodejs.org/en/docs/guides/backpressuring-in-streams/

NodeJS 团队分别测试了禁用和启用 backpressure 特性的情况下运行了这段脚本

const gzip = require("zlib").createGzip()
const fs = require("fs")

const inp = fs.createReadStream("The.Matrix.1080p.mkv")
const out = fs.createWriteStream("The.Matrix.1080p.mkv.gz")

inp.pipe(gzip).pipe(out)

不支持 backpressure 的情况首先带来的是内存的激增,其次是

  1. 影响其他速度
  2. GC 频率变高
  3. 内存耗尽

对于 NodeJS 来说,选择了 Buffer 的手段

# 场景 2: 服务间通讯

对于 客户端 -> 服务 A -> 服务 B

往往是在客户端与服务 A 之前加负载均衡,A 与 B 之间也加负载均衡

负载均衡也分为两种,服务端负载均衡以及客户端负载均衡。

  • 负载均衡此时也承担了 Buffer 的作用。
  • 而多个服务 A 和多个服务 B 承担了增加消费速率的作用。

# 场景 3: 爬虫 / 消息队列瞬时消息过多

大多是扩 buffer, 扩消费者。

# 场景 4: UI 渲染

之前写过弹幕类服务。其中对前后端的部分考虑点还是很有趣的。

在弹幕激增的情况下,弹幕列表不能瞬间挂掉。

对于用户发送的弹幕消息,后端做好用户的分组,然后针对每个用户做好弹幕限流。这样确保后端传给前端的弹幕事件是可接受范围内的。

然而,随着弹幕瞬时消息的激增,前端那边还是有点控不住了。

几轮讨论下来,判断出 UI 渲染是瓶颈。后端已经做好用户的分组和弹幕的限流。再做消息的合并必要性不是很大。

比如,瞬间接收 100 次弹幕事件。应该按照每秒进行合并弹幕批量前面 95 条弹幕接着渲染最后 5 条弹幕。

整个界面就看起来顺滑多了。

参考 https://steveholgado.com/rxjs-chat-app/

# 0xEE 参考链接