一、背景
Flink在處理流式任務的時候有很大的優勢,其中windows等操作符可以很方便的完成聚合任務,但是Flink是一套獨立的服務,業務流程中如果想使用需要將資料發到kafka,用Flink處理完再發到kafka,然後再做業務處理,流程很繁瑣。
比如在業務程式碼中想要實現類似Flink的window按時間批量聚合功能,如果純手動寫程式碼比較繁瑣,使用Flink又太重,這種場景下使用響應式程式設計RxJava、Reactor等的window、buffer操作符可以很方便的實現。
響應式程式設計框架也早已有了背壓以及豐富的操作符支援,能不能用響應式程式設計框架處理類似Flink的操作呢,答案是肯定的。
本文使用Reactor來實現Flink的window功能來舉例,其他操作符理論上相同。文中涉及的程式碼:github
二、實現過程
Flink對流式處理做的很好的封裝,使用Flink的時候幾乎不用關心執行緒池、積壓、資料丟失等問題,但是使用Reactor實現類似的功能就必須對Reactor執行原理比較瞭解,並且經過不同場景下測試,否則很容易出問題。
下面列舉出實現過程中的核心點:
1、建立Flux和傳送資料分離
入門Reactor的時候給的示例都是建立Flux的時候同時就把資料賦值了,比如:Flux.just、Flux.range等,從3.4.0版本後先建立Flux,再傳送資料可使用Sinks完成。有兩個比較容易混淆的方法:
- Sinks.many().multicast() 如果沒有訂閱者,那麼接收的訊息直接丟棄
- Sinks.many().unicast() 如果沒有訂閱者,那麼儲存接收的訊息直到第一個訂閱者訂閱
- Sinks.many().replay() 不管有多少訂閱者,都儲存所有訊息
在此示例場景中,選擇的是Sinks.many().unicast()
官方文件:https://projectreactor.io/docs/core/release/reference/#processors
2、背壓支援
上面方法的物件背壓策略支援兩種:BackpressureBuffer、BackpressureError,在此場景肯定是選擇BackpressureBuffer,需要指定快取佇列,初始化方法如下:Queues.
資料提交有兩個方法:
- emitNext 指定提交失敗策略同步提交
- tryEmitNext 非同步提交,返回提交成功、失敗狀態
在此場景我們不希望丟資料,可自定義失敗策略,提交失敗無限重試,當然也可以呼叫非同步方法自己重試。
Sinks.EmitFailureHandler ALWAYS_RETRY_HANDLER = (signalType, emitResult) -> emitResult.isFailure();
在此之後就就可以呼叫Sinks.asFlux開心的使用各種操作符了。
3、視窗函式
Reactor支援兩類視窗聚合函式:
- window類:返回Mono(Flux
) - buffer類:返回List
在此場景中,使用buffer即可滿足需求,bufferTimeout(int maxSize, Duration maxTime)支援最大個數,最大等待時間操作,Flink中的keys操作可以用groupBy、collectMap來實現。
4、消費者處理
Reactor經過buffer後是一個一個的傳送資料,如果使用publishOn或subscribeOn處理的話,只等待下游的subscribe處理完成才會重新request新的資料,buffer操作符才會重新傳送資料。如果此時subscribe消費者耗時較長,資料流會在buffer流程阻塞,顯然並不是我們想要的。
理想的操作是消費者在一個執行緒池裡操作,可多執行緒並行處理,如果執行緒池滿,再阻塞buffer操作符。解決方案是自定義一個執行緒池,並且當然執行緒池如果任務滿submit支援阻塞,可以用自定義RejectedExecutionHandler來實現:
RejectedExecutionHandler executionHandler = (r, executor) -> {
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RejectedExecutionException("Producer thread interrupted", e);
}
};
new ThreadPoolExecutor(poolSize, poolSize,
0L, TimeUnit.MILLISECONDS,
new SynchronousQueue<>(),
executionHandler);
三、總結
1、總結一下整體的執行流程
- 提交任務:提交資料支援同步非同步兩種方式,支援多執行緒提交,正常情況下響應很快,同步的方法如果佇列滿則阻塞。
- 豐富的操作符處理流式資料。
- buffer操作符產生的資料多執行緒處理:同步提交到單獨的消費者執行緒池,執行緒池任務滿則阻塞。
- 消費者執行緒池:支援阻塞提交,保證不丟訊息,同時佇列長度設定成0,因為前面已經有佇列了。
- 背壓:消費者執行緒池阻塞後,會背壓到buffer操作符,並背壓到緩衝佇列,快取佇列滿背壓到資料提交者。
2、和Flink的對比
實現的Flink的功能:
- 不輸Flink的豐富操作符
- 支援背壓,不丟資料
優勢:輕量級,可直接在業務程式碼中使用
劣勢:
- 內部執行流程複雜,容易踩坑,不如Flink傻瓜化
- 沒有watermark功能,也就意味著只支援無序資料處理
- 沒有savepoint功能,雖然我們用背壓解決了部分問題,但是當機後開始會丟失快取佇列和消費者執行緒池裡的資料,補救措施是新增Java Hook功能
- 只支援單機,意味著你的快取佇列不能設定無限大,要考慮執行緒池的大小,且沒有flink globalWindow等功能
- 需考慮對上游資料來源的影響,Flink的上游一般是mq,資料量大時可自動堆積,如果本文的方案上游是http、rpc呼叫,產生的阻塞影響就不能忽略。補償方案是每次提交資料都使用非同步方法,如果失敗則提交到mq中緩衝並消費該mq無限重試。
四、附錄
本文原始碼地址:https://github.com/sofn/reactor-window-like-flink
Reactor官方文件:https://projectreactor.io/docs/core/release/reference/
Flink文件:https://ci.apache.org/projects/flink/flink-docs-stable/
Reactive操作符:http://reactivex.io/documentation/operators.html
本文作者:木小豐,美團Java高階工程師,關注架構、軟體工程、全棧等,不定期分享軟體研發過程中的實踐、思考。歡迎關注公共號:Java研發
本文連結:https://lesofn.com/archives/shi-yong-reactor-wan-cheng-lei-shi-de-flink-de-cao-zuo