背压

当上下游的流操作处于不同的线程时,如果上游弹射数据的速度快于下游接收处理数据的速度,对于那些没来得及处理的数据就会造成积压,这些数据既不会丢失,又不会被垃圾回收机制回收,而是存放在一个异步缓存池中,如果缓存池中的数据一直得不到处理,越积越多,最后就会造成内存溢出,这便是响应式编程中的背压问题。
 
在请求访问、消息处理中,通常有两种模式,一种是Pull拉模式,一种是Push推模式。两者各有优缺点,为此响应式编程Reactive Streams提出了结合两者各自优点的Pull-Push模式,即所谓的背压(Back-Pressure)处理。

什么是背压

背压,有时也叫反压、回压,这个词对于大多数人都很陌生,笔者也不例外。原因是这个词主要是来自对英文Back-Pressure的翻译,该词在维基百科的解释:
Back pressure (or backpressure) is a resistance or force opposing the desired flow of fluid through pipes, leading to friction loss and pressure drop. The term back pressure is a misnomer, as pressure is a scalar quantity, so it has a magnitude but no direction. The fluid is what is directed, tending to flow away from high-pressure regions and toward low-pressure regions. If the low-pressure space is more high-pressure than intended (e.g. due to obstructions or tight bends in an exhaust pipe) or the high-pressure space is more low-pressure than intended, this opposes the desired flow and reduces the discharge. Similarly, bending or other operations on a pipe (such as a stock car exhaust system with a particularly high number of twists and bends[1]) can reduce flow rate.[2]
以水管与水龙头举例,上游开始放水,当水龙头的出水速度小于上游放水速度时,水管就会慢慢被水充满,直至在上游源头上外溢,这种反向流动其实就是背压。通过这种反向压力,上游就能知道下游的处理速度以及是否产生了阻塞,从而进一步从源头上进行流量控制,所以背压是实现流控的一种方式
那为什么back-pressure要翻译成背压呢?因为back有后背的意思,可以较为形象的理解就是,当人群通过一个入口时,大家不断地推动前面人的后背往前走,当推不动时,就会从后背传来压力,从而告知后面的人不要再往前推了。
 

流控策略

如上图,系统中存在三方:生产者(Producer)产生数据,通过管道(Pipeline)传输给消费者(Consumer)。
notion image
Producer Consumer 此时生产的速率(100/s)大于消费的速率(75/s),多余的流量无处可去。于是自然地衍生出三种策略:
控制(Control)。降低生产速率,从源头减少流量缓冲(Buffer)。管道将多余的流量存储起来丢弃(Drop)。消费者将无暇处理的流量丢弃

无限缓冲不可行

缓冲不应该是无限的(unbounded)。一方面如果生产者的速率长期大于消费者的速率,那么多余的流量将无限增加,即使流量可以用某种方式存储,这些流量预期被消费的时间也无限增加,满足不了业务需求。另一方面事实上无法实现真正的“无限”缓冲,它们最终都将受限于物理资源(内存、硬盘等),资源耗尽时,就不仅仅是流量丢失的问题了。
如果是有限的缓冲,则当缓冲满了以后,又回到了背压和丢弃策略了。而丢弃可不可行通常得看业务需求,于是早晚我们又得实现背压策略。

Pull v.s. Push概念

在详细介绍Pull与Push的各自细节前,先介绍下两者在编程方面的使用差别,准确的说是编程范式的差别。
  • Pull: 在逻辑上是一个主动地同步阻塞的编程方式(与命令式编程相同),其逻辑范式是:
  • Push:在逻辑上是一个被动的异步非阻塞的编程方式(与声明式相同),是一种事件通知的方式,其范式是:
用更形象的例子举例,就是java中Iterator和Stream的区别,Iterator就是pull-based的,使用方式是iterator.next()这种命令式的方式,而Stream就是push-based的,使用的是map等操作的声明式方式。

Pull模式

Pull模式——通常是同步、阻塞等待响应时间长,因为是同步的,所以编程相对简单,debug也方便。在网络通信方面,每次获取数据都需要先发送request,才能获取到response数据,增加了二分之一的round trip的网络开销,例如http协议。
 
显式背压是指在业务逻辑中显式地实现生产者和消费者间的沟通达到流量控制的目的。例如 TCP 协议中通过交换当前接收窗口的大小来完成流量控制。
其中拉取(pull)模式则是比较通用且重要的一种,即任务的趋动是由消费者发起的,而不是生产者。例如 Reactive Stream 里的 API 规定是由订阅者(消费者)调用 request(n) 方法向生产者请求 n 个消息,生产者再调用 onNext() 将 n 个消息提供给消费者。消费者可以按需要获取,生产者也可以按需生产,从而实现背压。

Push模式

Push模式——通常是异步的、非阻塞的基于消息驱动的方式。由于是异步非阻塞,所以性能要比pull模式高不少。同时由于获取数据不需要发送request,也减少了发送request的二分之一的round trip的网络开销,例如websocket协议。
Push模式从性能上看起来处处要比Pull模式优秀,但Push模式有个问题是,如果消息发布者producer的生产能力大于消息消费者consumer的能力时,会导致压垮consumer,其原因就是缺少了consumer对producer的控制。而反观Pull模式,所有的消息消费都是由consumer发送request给producer才可以,所以相对Pull模式,Push模式中consumer丧失了对producer的控制能力。

Pull-Push模式

为了使Push模式中consumer也能具有对producer的流量控制能力(即背压back-pressure),Reactive Streams推出了Pull-Push模式。那么,什么是Pull-Push模式呢?
顾名思义,Pull-Push中Pull在Push之前,也就是要获取数据(Push),需要先发送一个请求(Pull)。与Pull模式不同的是,每次发送请求request时,会携带一个请求数据量大小n的值,用于流量控制。producer接收到请求大小后,会以Push的方式,不断地将数据发送给consumer,直到达到请求的数据量结束为止。
  • n==1时,Pull-Push模式就会降级为Pull模式。
  • n==Long.MAX_VALUE时,Pull-Push模式会升级为Push模式,因为假设每个消息发送间隔是一纳秒,那Long.MAX_VALUE的数据量则至少需要几百万年,所以可以近似的理解为请求的数据量是无限的,所以也就升级为了Push模式。
 
Loading...
目录
文章列表
王小扬博客
产品
Think
Git
软件开发
计算机网络
CI
DB
设计
缓存
Docker
Node
操作系统
Java
大前端
Nestjs
其他
PHP