Java程式設計方法論-Spring WebFlux篇 01 為什麼需要Spring WebFlux 下

知秋z發表於2019-02-25

前言

本系列為本人Java程式設計方法論 響應式解讀系列的Webflux部分,現分享出來,前置知識Rxjava2 ,Reactor的相關解讀已經錄製分享視訊,併發布在b站,地址如下:

Rxjava原始碼解讀與分享:www.bilibili.com/video/av345…

Reactor原始碼解讀與分享:www.bilibili.com/video/av353…

NIO原始碼解讀相關視訊分享: www.bilibili.com/video/av432…

NIO原始碼解讀視訊相關配套文章:

BIO到NIO原始碼的一些事兒之BIO

BIO到NIO原始碼的一些事兒之NIO 上

BIO到NIO原始碼的一些事兒之NIO 中

BIO到NIO原始碼的一些事兒之NIO 下 之 Selector BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上 BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 下

Java程式設計方法論-Spring WebFlux篇 01 為什麼需要Spring WebFlux 上

其中,Rxjava與Reactor作為本人書中內容將不對外開放,大家感興趣可以花點時間來觀看視訊,本人對著兩個庫進行了全面徹底細緻的解讀,包括其中的設計理念和相關的方法論,也希望大家可以留言糾正我其中的錯誤。

Servlet 3.1與Spring MVC

隨著Servlet 3.1的引入,通過Spring MVC即可以實現非阻塞行為。 但是,由於Servlet API依然包含幾個阻塞的介面。同樣,我們在應用程式設計的API中也可能會使用到阻塞,而該API本來是被設定為非阻塞。 在這種情況下,相關阻塞API的使用肯定會降低應用程式效能。 我們來看下面這段程式碼:

@GetMapping
void onResponse(){
  try{
     //some logic here
  }catch(Exception e){
      //sendError() is a blocking API
     response.sendError(500);
  }
}
複製程式碼

這段程式碼使用在Spring MVC中,Spring容器針對這個錯誤而對相應頁面的渲染則是阻塞的。如下:

@Controller
public class MyCustomErrorController implements ErrorController {
    @RequestMapping(path = "/error")
    public String greeting() {
        return "myerror";
    }
    @Override
    public String getErrorPath() {
        return "/error";
    }
}
複製程式碼

此處渲染的頁面為myerror.jsp,具體程式碼就不貼了。當然,我們肯定有辦法來非同步解決這個錯誤處理問題,但我們出錯的可能性就會變大,要知道,我們最終還是要經過Servlet物件的,而Servlet相關api有阻塞的也有非阻塞的,我們來通過一張圖來方便理解。

Java程式設計方法論-Spring WebFlux篇 01 為什麼需要Spring WebFlux 下

當產生請求訪問時事件時,則該事件處理流向如上圖所示(我們只關注進入到Servlet容器的處理階段),可以知道,這個過程尤其是Filter鏈這裡,都是可以發生IO阻塞的,再根據上一節所講內容,我們可以使用一張圖來展示我們可以確定的非阻塞IO。

Java程式設計方法論-Spring WebFlux篇 01 為什麼需要Spring WebFlux 下
也就是說,即使我們在Spring MVC中在所寫程式碼邏輯中做到完美的無阻塞,我們依然無法改變與避免Servlet 3.1+中那些架構設計層面的缺陷,Servlet的相關阻塞API我們依然會用到。那麼我們是不是可以使用netty來避免這樣的情形?於是我們就可以將目光放到Spring WebFlux之上。

業務層面非同步處理難易分析

我們業務端來講,絕大多數程式設計師對於併發的操作並不在行的,也就很難寫出效能很好而且符合規範的程式碼,這也造成了在Spring web MVC下,我們很難針對自己的業務進行合理的非同步化操作。比如,我們往往會將I/O操作與當前執行執行緒進行繫結到一起,也就是生產和消費兩種業務繫結在一起,這樣,即便我們非同步,兩者也是在同一個執行緒中進行,這樣,假如併發量很大的情況下,非同步化會產生大量的執行緒,CPU會在切換執行緒上消耗更多的效能,這是我們所不願看到的,而RxJavaReactor給我們提供了很好的排程API,如Reactor中的publishOnRxJava中的observeOn,可以保證我們將生產和消費分離,同時,作為生產或消費執行緒所在的執行緒池,其往往是針對於使用了這個執行緒池的多個訂閱服務,這樣,每一個執行緒都可能同時為多個訂閱關係服務,一個單獨的訂閱關係並不會一直佔有這個執行緒,當有元素下發時,將會根據訂閱者請求數量和元素產生的速度以及是否有多個執行緒在處理此訂閱關係的下發元素,使用排程器的話,這裡拿Reactor中的publishOn來講,當上遊只支援同步的話(FluxPublishOn.PublishOnSubscriber#onSubscribe內呼叫源的requestFusion方法判斷),那就始終在同一個執行緒內消費(FluxPublishOn.PublishOnSubscriber#trySchedule內進行判斷,通過WIP控制),當我們定義好publishOn中佇列大小後,每當佇列內元素消耗完畢,然後上游元素產生太慢,就會跳出當前消費執行緒,直到有新元素下發時,就再次從執行緒池中拿到一個執行緒消費。讀者假如此處有疑問,請回顧本書之前內容(因書並未出版,可回顧本人相關分享視訊)。 這樣伺服器的效能就可以得到最大程度的利用。這個我們在Spring MVC中確實很難自行實現,比較複雜。 另外,通過Reactor對於背壓的實現,我們可以做到類似訊息中介軟體對於訊息的積壓,不至於資料在網路傳輸的過程中丟失,這樣就可以更好的應對高併發場景下的訪問需求。 接下來,我們就來對Webflux下的背壓使用進行一波大致的說明。

Webflux中的背壓的使用

為了幫助理解BackpressureWebFlux使用時底層的工作原理,我們有必要回顧一下預設使用的TCP/IP傳輸層。我們知道,瀏覽器和伺服器之間的正常通訊(伺服器到伺服器之間的通訊通常也是一樣)是通過TCP連線完成的(同樣包括WebFlux中的WebClient和伺服器之間的通訊)。同時,我們會從Reactive Streams規範的角度來回顧一下背壓的含義,以便更好的針對背壓進行控制。

Reactive Streams中,背壓包括兩部分,一部分是接收端的訊息積壓,另一部分是消費者可以通過發出通知來表達該消費者可以消耗多少元素,以此來進行需求調節。整個過程是操作的元素物件,那麼,在這裡,我們就碰到一個棘手的問題:TCP是針對位元組抽象而不是邏輯元素抽象。 我們通常所說的背壓控制是指制向或者從網路傳送或接收的邏輯元素的數量。而TCP自己的流程控制是基於位元組而不是邏輯元素。

由上,可知道,在WebFlux的實現中,背壓通過資料傳輸流程控制來調節,但它不會暴露接收方的實際需求。 我們可以通過下圖來觀察其中的互動流程:

Java程式設計方法論-Spring WebFlux篇 01 為什麼需要Spring WebFlux 下

上圖顯示了兩個微服務之間的通訊,其中左側傳送資料流,右側對該流進行消費。接下來對上圖整個過程進行簡要說明:

  1. WebFlux中,它將邏輯物件元素轉換為位元組流並將它們傳輸到TCP網路或從TCP網路接收位元組流並轉換為邏輯物件元素。
  2. 此處開始進行一段時間長度的元素處理,在該元素處理完成後請求下一個元素。
  3. 這裡,雖然沒有來自業務邏輯的需求,但WebFlux會對來自TCP網路的位元組排隊,這裡,就會涉及到背壓策略,關於背壓策略我們在之前Reactor的相關章節已經涉及過。
  4. 由於TCP自身資料流程控制的性質,服務A仍然可以向網路傳送資料。

正如我們從上圖中可以看到的那樣,接收者的需求與傳送者的需求不同(這裡指圖中的request請求的邏輯元素)。這也就意味著兩者的需求是相互獨立的,也就是說,在WebFlux中,我們可以通過業務邏輯(服務)互動來展現需求,但很少會暴露服務A與服務B互動的相關背壓細節。 也就是說,webflux中的背壓設計並沒有對資料傳送服務端進行按需設計,這點可能與我們所期望的有所出入,不是那麼完美,顯得有失公平。

自定義背壓控制

如果我們想很簡單的對背壓進行控制,我們可以通過Reactor的相關操作來控制請求數量,也可以在自定義訂閱者的時候進行限定,這裡我們通過Flux下的limitRate(n)來實現。首先我們先來看下其實現思路,其實就是一個排程操作,只不過我們之前有講,publishOn自己是一箇中間儲存站,它將上下游進行分離下游的請求數量在這裡進行管理,publishOn自己有一個每次向上遊請求的數量限制,關於publishOn操作原始碼細節,可以回顧之前相關章節內容(因書並未出版,可回顧本人相關分享視訊)。也就是說,我們只需要在publishOn之上封裝一個API來實現即可:

//reactor.core.publisher.Flux#limitRate(int)
public final Flux<T> limitRate(int prefetchRate) {
    return onAssembly(this.publishOn(Schedulers.immediate(), prefetchRate));
}
複製程式碼

假如我們有一個包含questions的源,因為解決問題的能力有限,想要對其進行限流,於是我們就可以進行如下操作:

@PostMapping("/questions")
public Mono<Void> postAllQuestions(Flux<Question> questionsFlux) {

    return questionService.process(questionsFlux.limitRate(10))
                       .then();
}
複製程式碼

我們熟悉publishOn後,可以知道limitRate()操作會首先從上游獲取10個元素存到其內定義的佇列中。這意味著即使我們定義的訂閱者所設定的請求元素數量為Long.MAX_VALUElimitRate操作也會將此需求拆分為一塊一塊去請求下發。此處涉及的原始碼如下,大家可對照理解:

//reactor.core.publisher.FluxPublishOn.PublishOnSubscriber#runAsync
if (e == limit) {
    if (r != Long.MAX_VALUE) {
        r = REQUESTED.addAndGet(this, -e);
    }
    s.request(e);
    e = 0L;
}
複製程式碼

上面是提交的資料的分塊處理,我們有時候會涉及到資料庫請求資料的處理,比如查詢,同時將所傳送資料進行限流逐步傳送,可以進行如下操作:

@GetMapping("/questions")
public Flux<Question> getAllQuestions() {

    return questionService.retreiveAll()
                       .limitRate(10);
}
複製程式碼

由此,我們也能理解背壓在webflux中的作用機制了。對於這些特性,Spring MVC也就很難提供了。

小結

相信大家也明確感受到了使用Spring WebFlux的好處了,也知道為何會要求使用Servlet 3.1+,同時對於webflux中背壓的作用有了更清晰的認知。不過,我們需要注意的是,通過官方文件可知,Spring Webflux可以在Servlet ContainerNetty上執行,而本書更關心Spring Webflux基於Netty伺服器的執行。那麼,接下來,我們將接觸Reactor-netty的內在細節。

相關文章