背壓問題
背壓是指在異步場景中,被觀察者發送事件速度遠快於觀察者的處理速度的情況下,一種告訴上游的被觀察者降低發送速度的策略
簡而言之,背壓是流速控制的一種策略。
需要強調兩點:
- 背壓策略的一個前提是異步環境,也就是說,被觀察者和觀察者處在不同的線程環境中。
- 背壓(Backpressure)並不是一個像flatMap一樣可以在程序中直接使用的操作符,他只是一種控制事件流速的策略。
響應式拉取(reactive pull)
首先我們回憶之前那篇《關於Rxjava最友好的文章》,裏面其實提到,在RxJava的觀察者模型中,被觀察者是主動的推送數據給觀察者,觀察者是被動接收的。而響應式拉取則反過來,觀察者主動從被觀察者那裏去拉取數據,而被觀察者變成被動的等待通知再發送數據。
結構示意圖如下:
觀察者可以根據自身實際情況按需拉取數據,而不是被動接收(也就相當於告訴上游觀察者把速度慢下來),最終實現了上游被觀察者發送事件的速度的控制,實現了背壓的策略。
源碼
public class FlowableOnBackpressureBufferStategy{
...
@Override
public void onNext(T t) {
if (done) {
return;
}
boolean callOnOverflow = false;
boolean callError = false;
Deque<T> dq = deque;
synchronized (dq) {
if (dq.size() == bufferSize) {
switch (strategy) {
case DROP_LATEST:
dq.pollLast();
dq.offer(t);
callOnOverflow = true;
break;
case DROP_OLDEST:
dq.poll();
dq.offer(t);
callOnOverflow = true;
break;
default:
// signal error
callError = true;
break;
}
} else {
dq.offer(t);
}
}
if (callOnOverflow) {
if (onOverflow != null) {
try {
onOverflow.run();
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
s.cancel();
onError(ex);
}
}
} else if (callError) {
s.cancel();
onError(new MissingBackpressureException());
} else {
drain();
}
}
...
}
在這段源碼中,根據不同的背壓策略進行了不同的處理措施,當然這只是列舉了一段關於buffer背壓策略的例子。
根源
產生背壓問題的根源就是上游發送速度與下游的處理速度不均導致的,所以如果想要解決這個問題就需要通過匹配兩個速率達到解決這個背壓根源的措施。
通常有兩個策略可供使用:
- 從數量上解決,對數據進行採樣
- 從速度上解決,降低發送事件的速率
- 利用flowable和subscriber
使用Flowable
Flowable<Integer> upstream = Flowable.create(new FlowableOnSubscribe<Integer>() {
@Override
public void subscribe(FlowableEmitter<Integer> emitter) throws Exception {
Log.d(TAG, "emit 1");
emitter.onNext(1);
Log.d(TAG, "emit 2");
emitter.onNext(2);
Log.d(TAG, "emit 3");
emitter.onNext(3);
Log.d(TAG, "emit complete");
emitter.onComplete();
}
}, BackpressureStrategy.ERROR); //增加了一個參數
Subscriber<Integer> downstream = new Subscriber<Integer>() {
@Override
public void onSubscribe(Subscription s) {
Log.d(TAG, "onSubscribe");
s.request(Long.MAX_VALUE); //注意這句代碼
}
@Override
public void onNext(Integer integer) {
Log.d(TAG, "onNext: " + integer);
}
@Override
public void onError(Throwable t) {
Log.w(TAG, "onError: ", t);
}
@Override
public void onComplete() {
Log.d(TAG, "onComplete");
}
};
upstream.subscribe(downstream);
我們注意到這次和Observable
有些不同. 首先是創建Flowable
的時候增加了一個參數, 這個參數是用來選擇背壓,也就是出現上下游流速不均衡的時候應該怎麼處理的辦法, 這裏我們直接用BackpressureStrategy.ERROR
這種方式, 這種方式會在出現上下游流速不均衡的時候直接拋出一個異常,這個異常就是著名的MissingBackpressureException
. 其餘的策略後面再來講解.
另外的一個區別是在下游的onSubscribe
方法中傳給我們的不再是Disposable
了, 而是Subscription
, 它倆有什麼區別呢, 首先它們都是上下游中間的一個開關, 之前我們說調用Disposable.dispose()
方法可以切斷水管, 同樣的調用Subscription.cancel()
也可以切斷水管, 不同的地方在於Subscription
增加了一個void request(long n)
方法, 這個方法有什麼用呢, 在上面的代碼中也有這麼一句代碼:
s.request(Long.MAX_VALUE);
這是因爲Flowable
在設計的時候採用了一種新的思路也就是響應式拉取
的方式來更好的解決上下游流速不均衡的問題, 與我們之前所講的控制數量
和控制速度
不太一樣, 這種方式用通俗易懂的話來說就好比是葉問打鬼子
, 我們把上游
看成小日本
, 把下游
當作葉問
, 當調用Subscription.request(1)
時, 葉問
就說我要打一個!
然後小日本
就拿出一個鬼子
給葉問, 讓他打, 等葉問打死這個鬼子之後, 再次調用request(10)
, 葉問就又說我要打十個!
然後小日本又派出十個鬼子
給葉問, 然後就在邊上看熱鬧, 看葉問能不能打死十個鬼子, 等葉問打死十個鬼子後再繼續要鬼子接着打…
所以我們把request當做是一種能力, 當成下游處理事件
的能力, 下游能處理幾個就告訴上游我要幾個, 這樣只要上游根據下游的處理能力來決定發送多少事件, 就不會造成一窩蜂的發出一堆事件來, 從而導致OOM. 這也就完美的解決之前我們所學到的兩種方式的缺陷, 過濾事件會導致事件丟失, 減速又可能導致性能損失. 而這種方式既解決了事件丟失的問題, 又解決了速度的問題, 完美 !
同步情況
Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
for (int i = 0; ; i++) { //無限循環發事件
emitter.onNext(i);
}
}
}).subscribe(new Consumer<Integer>() {
@Override
public void accept(Integer integer) throws Exception {
Thread.sleep(2000);
Log.d(TAG, "" + integer);
}
});
當上下游工作在同一個線程
中時, 這時候是一個同步
的訂閱關係, 也就是說上游
每發送一個事件必須
等到下游
接收處理完了以後才能接着發送下一個事件.
同步與異步的區別就在於有沒有緩存發送事件的緩衝區。
異步情況
通過subscribeOn和observeOn來確定對應的線程,達到異步的效果,異步時會有一個對應的緩存區來換從從上游發送的事件。
public enum BackpressureStrategy {
/**
* OnNext events are written without any buffering or dropping.
* Downstream has to deal with any overflow.
* <p>Useful when one applies one of the custom-parameter onBackpressureXXX operators.
*/
MISSING,
/**
* Signals a MissingBackpressureException in case the downstream can't keep up.
*/
ERROR,
/**
* Buffers <em>all</em> onNext values until the downstream consumes it.
*/
BUFFER,
/**
* Drops the most recent onNext value if the downstream can't keep up.
*/
DROP,
/**
* Keeps only the latest onNext value, overwriting any previous value if the
* downstream can't keep up.
*/
LATEST
}
背壓策略:
- error, 緩衝區大概在128
- buffer, 緩衝區在1000左右
- drop, 把存不下的事件丟棄
- latest, 只保留最新的
- missing, 缺省設置,不做任何操作
上游從哪裏得知下游的處理能力呢?我們來看看上游最重要的部分,肯定就是FlowableEmitter
了啊,我們就是通過它來發送事件的啊,來看看它的源碼吧(別緊張,它的代碼灰常簡單):
public interface FlowableEmitter<T> extends Emitter<T> {
void setDisposable(Disposable s);
void setCancellable(Cancellable c);
/**
* The current outstanding request amount.
* <p>This method is thread-safe.
* @return the current outstanding request amount
*/
long requested();
boolean isCancelled();
FlowableEmitter<T> serialize();
}
FlowableEmitter是個接口,繼承Emitter,Emitter裏面就是我們的onNext(),onComplete()和onError()三個方法。我們看到FlowableEmitter中有這麼一個方法:
long requested();
同步request.png
這張圖的意思就是當上下游在同一個線程中的時候,在下游
調用request(n)就會直接改變上游
中的requested的值,多次調用便會疊加這個值,而上游每發送一個事件之後便會去減少這個值,當這個值減少至0的時候,繼續發送事件便會拋異常了。
異步request.png
可以看到,當上下游工作在不同的線程裏時,每一個線程裏都有一個requested,而我們調用request(1000)時,實際上改變的是下游主線程中的requested,而上游中的requested的值是由RxJava內部調用request(n)去設置的,這個調用會在合適的時候自動觸發。