熵增學院-Anders-SpringBoot中的Http應用---WebFlux 頂 原 薦

我們今天開始進入Spring WebFlux.WebFlux是Spring5.0開始引入的.有別於SpringMVC的Servlet實現,它是完全支持異步和非阻塞的.在正式使用Spring WebFlux之前,我們首先得了解他和Servlet的區別,以及他們各自的優勢,這樣我們才能夠給合適的場景選擇合適的開發工具.

首先我們要問幾個問題,爲什麼要有異步?在異步之前,軟件行業做過哪些努力,他們的優勢是什麼?基於這幾個問題,我們今天分享以下三個知識點:

  1. 從Http1.X 到Http2.0

  2. 從Servlet2.x到Servlet3.x

  3. WebFlux的出場

     

 

1. 從Http1.x到Http2.0

異步和同步是無法分開的.他們對性能的理解和處理也是各有千秋.傳統的web項目因爲是基於阻塞I/O模型而建立的,所以他們只能通過對整個鏈路的優化來提升性能,而這裏的性能就包括了伸縮性和響應速度.這裏面比較重要的一個環節就是網絡傳輸.相對而言,這也是距離我們的用戶最近的一個環節,因此他們對併發的處理以及對響應速度的處理就比其他的會更直接地影響我們的用戶.

1.1 Http/1.x

在http1.x中,我們都知道,http會先進行三次握手,握手成功之後,開始傳遞數據,服務器響應完畢,就進行四次揮手,最後關閉鏈接.剛開始應用這個概念的時候,是非常受歡迎的,因爲在那時候傳遞的還是靜態頁面或者動態數據比較少的資源,因此無論是客戶端還是服務器端,他都節省了更多的資源.但隨着互聯網的飛速發展,這種方式就遇到了問題.如果每次傳遞數據都需要三次握手四次揮手的話,那麼隨着數據訪問量的增加,那麼三次握手四次揮手帶來的資源消耗就會成爲影響系統的瓶頸.這就好像一根針重量可以忽略,但當我們聚集上億根針的時候,那麼他的重量和所佔用的空間,就成了必須要考慮的問題了.

那能不能建立好一次鏈接之後,我多傳遞幾次數據,然後在關閉呢?當然可以,這就是長鏈接,也就是大家常說的"Keep-Alive".而HTTP1.1則是默認就開啓了Keep-Alive.Keep-Alive雖然暫時性的解決了建立鏈接所帶來的開銷,也一定程度的提高了響應速度,但後來又凸顯了另外兩個問題:

  1. 首先,因爲http是串行文件傳輸.所以當客戶端請求a文件時,b文件只能等待.等待a鏈接到服務器,服務器處理文件,服務器返回文件這三個步驟完成後,b才能接着處理.我們假設,鏈接服務器,服務器處理,服務器返回各需要1秒,那麼b處理完的時候就需要6秒,以此類推.(當然,這裏有個前提,服務器和瀏覽器都是單通道的.)這就是我們說的阻塞.

  2. 其次,鏈接數的問題.我們都知道服務器的鏈接數是有限的.並且瀏覽器也對鏈接數有限制.這樣能接入進來的服務就是有個數限制的,當達到這個限制的時候,其他的就需要等待鏈接被斷開,然後新的請求才能夠進入.這個比較容易理解.

之所以http1.x會使用串行文件傳輸,是因爲http傳輸的無論是request還是response都是基於文本的,所以接收端無法知道數據的順序,因此必須按着順序傳輸.這也就限制了只要請求就必須新建立一個鏈接,這也就導致了第二個問題的出現.

1.2 Http/2

爲了從根本上行解決http1.x所遺留的這兩個問題,http2引入了二進制數據幀和流的概念.其中幀的作用就是對數據進行順序標識,這樣的話,接收端就可以根據順序標識來進行數據合併了.同時,因爲數據有了順序,服務器和客戶端就可以並行的傳輸數據,而這就是流所作的事情.

這樣,因爲服務器和客戶端可以藉助流進行並行的傳遞數據,那麼同一臺客戶端就可以使用一個鏈接來進行傳輸,此時服務器能處理的併發數就有了質的飛躍.

http/2的這個新特性,就是多路複用.我們可以看到,多路複用的本質就是並行傳輸.那web對請求的處理是否可以使用這個思路呢?

2.Servlet

現在我們來討論Servlet與Netty.這兩個一個主要是以同步阻塞的方式服務的,另一個是異步非阻塞的.這也就造成了他們適用的場景是不同的.

2.1 Servlet

做JavaWeb研發的幾乎沒有不知道Servlet的.在Servlet 3.0之前,Servlet採用Thread-Per-Request的方式處理請求,即每一次Http請求都由某一個線程從頭到尾負責處理。如果一個請求需要進行IO操作,比如訪問數據庫、調用第三方服務接口等,那麼其所對應的線程將同步地等待IO操作完成, 而IO操作是非常慢的,所以此時的線程並不能及時地釋放回線程池以供後續使用,在併發量越來越大的情況下,這將帶來嚴重的性能問題。爲了解決這一的問題,Servlet3.0引入了異步處理.

在Servlet 3.0中,我們可以從HttpServletRequest對象中獲得一個AsyncContext對象,該對象構成了異步處理的上下文,Request和Response對象都可從中獲取。AsyncContext可以從當前線程傳給另外的線程,並在新的線程中完成對請求的處理並返回結果給客戶端,初始線程便可以還回給容器線程池以處理更多的請求。如此,通過將請求從一個線程傳給另一個線程處理的過程便構成了Servlet 3.0中的異步處理。

這裏舉個例子,對於一個需要完成長時處理的Servlet來說,其實現通常爲:

 

package top.lianmengtu.testjson.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

//@WebServlet("/syncHello"),因爲使用的SpringBoot模擬,所以註釋掉該註解
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,       IOException {
        super.doGet(req, resp);
        new LongRunningProcess().run();
        System.out.println("HelloWorld");
    }
}

LongRunningProcess實現如下:

package top.lianmengtu.testjson.servlet;

import java.util.concurrent.ThreadLocalRandom;

public class LongRunningProcess {
    public void run(){
        try {
            int millis = ThreadLocalRandom.current().nextInt(2000);
            String currentThread = Thread.currentThread().getName();
            System.out.println(currentThread + " sleep for " + millis + " milliseconds.");
            Thread.sleep(millis);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我們現在將MyServlet注入到Spring容器中:

@Bean
public ServletRegistrationBean servletRegistrationBean(){
    return new ServletRegistrationBean(new MyServlet(),"/syncHello");
}

此時的SyncHelloServlet將順序地先執行LongRunningProcess的run()方法,然後在控制檯打印HelloWorld.而3.0則提供了對異步的支持,因此在Servlet3.0中我們可以這麼寫:

 

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    AsyncContext asyncContext=req.startAsync();
    asyncContext.start(()->{
        new LongRunningProcess().run();
        try {
            asyncContext.getResponse().getWriter().print("HelloWorld");
        } catch (IOException e) {
            e.printStackTrace();
        }
        asyncContext.complete();
    });

}

此時,我們先通過request.startAsync()獲取到該請求對應的AsyncContext,然後調用AsyncContext的start()方法進行異步處理,處理完畢後需要調用complete()方法告知Servlet容器。start()方法會向Servlet容器另外申請一個新的線程(可以是從Servlet容器中已有的主線程池獲取,也可以另外維護一個線程池,不同容器實現可能不一樣),然後在這個新的線程中繼續處理請求,而原先的線程將被回收到主線程池中。事實上,這種方式對性能的改進不大,因爲如果新的線程和初始線程共享同一個線程池的話,相當於閒置下了一個線程,但同時又佔用了另一個線程。

Servlet 3.0對請求的處理雖然是異步的,但是對InputStream和OutputStream的IO操作卻依然是阻塞的,對於數據量大的請求體或者返回體,阻塞IO也將導致不必要的等待。因此在Servlet 3.1中引入了非阻塞IO,通過在HttpServletRequest和HttpServletResponse中分別添加ReadListener和WriterListener方式,只有在IO數據滿足一定條件時(比如數據準備好時),才進行後續的操作。

雖然Servlet3.1提供了異步的方式,並且做的也比Servlet3.0更徹底,但是如果我們使用了Servlet3.1提供的異步接口,像剛剛的代碼演示的那樣,那麼我們在之後的處理中就沒有辦法再使用他原來的接口了.這就讓我們處於了一種非此即彼的狀況中.如果是這樣,Servlet系列的技術,如SpringMVC也就是這樣了.那怎麼辦呢?

 

3. WebFlux的出場

現在我們會從以下幾個層面來探討WebFlux

  1. 爲什麼要有WebFlux?

  2. Reactive定義與ReactiveAPI

  3. WebFlux中的性能問題

  4. WebFlux的併發模型

  5. WebFlux的適用性

3.1爲什麼要有WebFlux

首先,爲什麼要有webFlux?

在前面兩部分,我們一直在探討併發問題.爲了解決併發,我們需要使用非阻塞的web技術棧.因爲非阻塞的web棧使用的線程數更少,對硬件資源的要求更低.雖然Servlet3.1爲非阻塞I/O提供了一些支持,但剛剛我們提到了,如果我們使用Servlet3.1裏的非阻塞API,會導致我們無法再使用它原來的API.並且,自從非阻塞I/O以及異步概念出現之後,就誕生了一批專爲異步和非阻塞I/O設計的服務器,比如Netty,這就催生了新的能服務於各種非阻塞I/O服務器的統一的API.

WebFlux誕生的另一個重要原因是函數式程序設計.隨着腳本型語言(Nodejs,Angular等)的擴張,函數式程序設計以及後繼式API也相繼火起來.以至於Java也在Java8中引入了Lambda來對函數式程序設計進行支持,又引入了StreamAPI來對後繼式程序進行支持.由此,對具備函數式編程和後繼式程序設計的Web框架的需求也越來越大了。

3.2Reactive的定義與API

Reactive的定義

我們接觸了"非阻塞"和"函數式",那reactive是什麼意思呢?

 "reactive"這個術語指的是:圍繞着對改變做出響應的程序設計模型---網絡組件對IO事件做出響應,UIController對鼠標事件做出響應等等.在那種情況下,非阻塞取代了阻塞是響應式的,我們正處於響應模式中,當操作完成和數據變得可用的時候發起通知.

還有另一個重要的機制那就是我們在spring team裏整合"reactive"以及非阻塞式背壓機制.在同步裏,命令式的代碼,阻塞式地調用服務爲普通的表單充當背壓機制強迫調用者等待.在非阻塞式編程中,控制事件的頻率就變得很重要防止快速的生產者不會壓垮他的目的地.

Reactive Streams 是一個定義了使用背壓機制的異步組件之間交互設計的小型說明書(在Java9中也採納了).例如,一個數據倉庫(可以看做Publisher)可以生產數據,然後HTTP Server(看做訂閱者)可以寫入到響應裏.Reactive Streams的主要目的是讓訂閱者可以控制生產者產生數據的速度有多快或有多慢.

Reactive API

Reactive Streams 在互操作性上扮演了一個很重要的角色.類庫和基礎設施組件雖然有趣,但對於應用程序API來說卻用處甚少,因爲他們太底層了.應用程序需要一個更高級別更豐富的函數式API來編寫異步邏輯---和Java8裏的StreamAPI很類似,不過不僅僅是爲集合做準備的.

Reactor 是爲SpringWebFlux選擇的一個reactive類庫.它提供了Mono和Flux類型的API來處理0..1(Mono)和0..N(Flux)數據序列化通過一組豐富的操作集和ReactiveX vocabulary of operators對齊.Reactor 是一個Reactive Streams類庫,所以他所有的操作都支持非阻塞背壓機制.Reactor強烈地聚焦於Server端的Java.他在發展上和Spring有着緊密的協作.

WebFlux要求Reactor作爲一個核心依賴,但憑藉Reactive Streams也可以和其他的reactive libraries一起使用.一般來說,一個WebFlux API 接收一個Publisher作爲輸入,轉換給一個內置的Reactor類型來使用,最後返回一個Flux或一個Mono作爲輸出.所以,你可以批准任何的Publisher作爲輸入,你可以應用操作在輸出上,但你因爲你使用了其他的reactive library所以你需要進行轉換.只要可行(例如,註解controllers),WebFlux可以在使用RXJava和另一個reactive library之間透明的改變.看Reactive Libraries獲取更多地細節.

3.3 性能

性能這個詞有很多特徵和含義.Reactive 和非阻塞通常不會使應用程序運行地更快.在某些場景下,他們也可以.(例如,在並行條件下使用WebClient來執行遠程調用的話).整體來說,非阻塞方式可能需要做更多的工作並且他也會稍微增加請求處理的時間.

對reactive和非阻塞好處的預期關鍵在於使用小,固定的線程數和更少的內存來擴展的能力.這使應用程序在加載的時候更加有彈性,因爲他們以一種更可以預測的方式擴展.然而爲了看到這些好處,你需要一些延遲(包括比較慢的不可預知的網絡I/O).那是響應式堆棧開始顯示他力量的地方,並且這些不同是非常吸引人的.

3.4併發模型

Spring MVC和Spring WebFlux都支持註解Controllers,但他們在併發模型和對阻塞和線程的默認呈現(assumptions)上是非常不同的.在Spring MVC(和通用的servlet應用)中,都假設應用程序是阻塞當前線程的(例如,遠程調用),並且出於這個原因,servlet容器處理請求的期間使用一個巨大的線程池來吸收潛在的阻塞.

在Spring WebFlux(和非阻塞服務器)中,假設應用程序是非阻塞的,所以,非阻塞服務器使用小的,固定代銷的線程池(event loop workders)來處理請求.

 "彈性伸縮"和"小數量的線程"或許聽起來矛盾,但是對於不會阻塞當前線程(用依賴回調來取代)意味着你不需要額外的線程,因爲非阻塞調用給處理了.

 

調用一個阻塞API

      要是你需要使用阻塞庫怎麼辦?Reactor和RxJava都提供了publishOn操作用一個不同的線程來繼續處理.那意味着有一個簡單的脫離艙口(一個可以離開非阻塞的出口).然而,請牢記,阻塞API對於併發模型來說不太合適.

易變的狀態

        在Reactor和RxJava裏,你通過操作符生命邏輯,在運行時在不同的階段裏,都會形成一個進行數據序列化處理的管道.這樣做的一個主要好處就是把應用程序從不同的狀態保護中解放了出來,因爲管道中的應用代碼是絕不會被同時調用的.

線程模型

在運行了一個使用Spring WebFlux的服務器上,你期望看到什麼線程呢?

  • 在一個"vanilla"Spring WebFlux服務器上(例如,沒有數據訪問也沒有其他可選的依賴),你能夠看到一個服務器線程和幾個其他的用來處理請求的線程(一般來說,線程的數目和CPU的核數是一樣的).然而,Servlet容器在啓動的時候就使用了更多的線程(例如,tomcat是10個),來支持servlet(阻塞)I/O和servlet3.1(非阻塞)I/O的用法.

  • 響應式的WebClient操作是用Event Loop方式.所以你可以看到少量的固定數量的線程和他關聯.(例如,使用了Reactor Netty連接的reactor-http-nio).然而,如果Reactor Netty在客戶端和服務端都被使用了,這兩者之間的event loop資源默認是被共享的.

  • Reactor和RxJava提供了抽象化的線程池,調度器目的是結合publishOn操作符在不同的線程池之間切換操作.調度器有一個名字,建議這個名字是一個具體的併發策略--例如,"parallel"(因爲CPU-bound使用有限的線程數來工作)或者"elastic"(因爲I/O-bound使用大量的線程來工作).如果你看到這類的線程,這就意味着一些代碼正在使用一個具體的使用了Scheduler策略的線程池.

  • 數據訪問庫和其他第三方庫依賴也創建和使用了他們自己的線程.

下次我們來分享Spring WebFlux的使用.

本文相關視頻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章