博客 / 詳情

返回

解鎖網絡性能優化利器HTTP/2C

我總要言説一些東西,因為我的心始終在喋喋不休。

前言

HTTP的發展現狀

最近腦海裏面始終活躍着一些想法,一部分是對過去錯誤認知的糾正,比如HTTP/2。在《HTTP學習筆記(三) HTTP/2》,這裏已經提過了,HTTP 1.0的性能缺點是每一個連接都對應一個TCP連接,到HTTP 1.1對這個問題進行了解決,也就是keep-alive和流水線,所謂keep-alive, 也就是説客户端和服務端請求維持這個TCP連接一段時間,這樣有效的減少了頻繁建立TCP連接的開銷。

而流水線則是允許客户端在收到上一個響應之前,連續發送其他請求,這看起來是個不錯的設計,有效的將請求報文傳輸並行化。但這一般是一個誤解,英文原文是:

HTTP pipelining is a way to send another request while waiting for the response to a previous request.

但其實表達的真實意思應該是將多個HTTP請求放到一個TCP連接中一一發送,而在發送過程中不需要等待服務器對前一個請求的響應。但是遺憾的是HTTP 1.1 要求,服務器必須嚴格按照接收到請求的相同順序來回送HTTP響應。但就像在超市排隊一樣,如果隊頭的人買了很多東西,那麼後續排隊人都要在這裏等待。

img

當然你也可以和超市協商再起一個新隊伍,即新建一個TCP連接。但不管怎麼樣,你總歸得選擇一個隊伍,而且一旦選定之後,就不能更換隊伍。但是新隊伍也會導致資源耗費和性能損失。

我們分析一下HTTP 1.1 為什麼要這麼要求,原因在於如果不強制要求順序,那接收響應的時候怎麼知道對應的是哪個請求的呢? 於是這些HTTP請求看起來還是串行處理,在一個TCP連接上。

管線化的問題

管線化的思路沒什麼問題,我們在RFC-2616,也就是參考文檔[2]可以看到對管線化的論述:

Clients which assume persistent connections and pipeline immediately after connection establishment SHOULD be prepared to retry their connection if the first pipelined attempt fails.

If a client does such a retry, it MUST NOT pipeline before it knows the connection is persistent. Clients MUST also be prepared to resend their requests if the server closes the connection before sending all of the corresponding responses

那些假定連接是持久的、並且在連接建立後立即使用流水線的客户端,應該準備好在第一次流水線嘗試失敗之後,重試他們與服務器之間的連接。如果客户端進行了這樣的重試,那麼在它確認該連接是持久的之前,客户端必須禁止再次使用流水線。如果客户端在發送完所有的響應之前就關閉了連接,客户端必須準備重發它們的請求。

注意這個持久連接,默認情況下,HTTP/1.0會在每次請求/響應交互關閉連接,這個連接是TCP連接,因此HTTP/1.0的持久連接必須經過明確協商。也就是請求頭裏面加入Connection: keep-alive來保持連接,連接的其他參數可以通過 keep-alive來指定,如果希望關閉連接,則是在請求標頭裏面加入Connection:close。這是http/1.0請求的默認值。如果在Http/1.1下面將會自動維持長連接,自動啓用keep-alive。

注意這裏的話,我認為這個假設有點脆弱,原因在於沒有經過假設,客户端只能通過猜測的方式來判斷服務器是否支持這一特性,為什麼這麼説呢? 原因在於我們考慮服務端早期對 http 1.0的支持,許多Http 1.1web服務器是從 1.0演變過來的,由於無法判斷這一特性是否被這些服務器支持,客户端必須猜測這一特性是否被服務端支持。在參考資料可以看到,火狐瀏覽器為了支持這個特性做出的努力,通過嘗試和維護黑名單(網站不支持加入黑名單,黑名單裏面的網站默認不會開啓這個特性),最終還是發現風險大於收益。

舉個例子,請求A、B、C依次到達代理服務器,假設代理服務器不支持這一特性,返回順序是B、C、A,對於一些頁面渲染就會出現問題。由此就引出來了HTTP/2的多路複用。

多路複用解決了這個問題

觀測流水線

我們在這裏再度明確一下,我們希望在發送請求的時候儘可能的降低延遲,但是遇到有依賴的網絡資源的話,HTTP 1.1給出的方案是可以向服務端發送多個請求而不等待響應。一般我們用HttpClient發送請求的偽代碼如下所示:

Request request = new Request():
response = httpclient.send(request);

在Http 1.1下面我們可以寫成下面這樣:

// 注意這裏是偽代碼
Request requestOne = new Request();
Request requestTwo = new Request():
List<Request> listRequest = new  ArrayList<>();
listRequest.add(requestOne);
listRequest.add(requestTwo);
List<Response> response = httpclient.send(request);

注意這裏的核心問題在於,如何知道請求和響應之間的對應關係,HTTP /1.1的設計響應順序即為發送請求的順序。我們不妨看看一些Http Client是怎麼實現你這個特性的,這裏以Vertx為例我們來做個簡單的分析, 首先我們需要引入Vertx:

<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-web-client</artifactId>
    <version>5.0.4</version>
</dependency>
<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-web</artifactId>
    <version>5.0.4</version>
</dependency>

注意如果你在Spring Boot 中寫Vertx相關的代碼,會有依賴衝突的問題,原因在於Spring Boot 鎖定了Netty的版本,Vertx也依賴了Netty:

img

而Vertx 依賴Netty的版本是4.2.5,所以這裏要注意對齊Netty的版本, 所以這裏要對齊Netty的版本,在maven裏面聲明一下:

<properties>
    <java.version>17</java.version>
    <netty.version>4.2.5.Final</netty.version>
</properties>

首先我們用Vertx 寫一個簡單的WebServer:

public class SimpleWebServer extends AbstractVerticle {
    @Override
    public void start(Promise<Void> startPromise) throws Exception {
        HttpServer server = vertx.createHttpServer();
        Router router = Router.router(vertx);
        router.get("/test-1").handler(ctx -> {
            try {
                // 注意這裏的延時是為了測試隊頭阻塞問題,
                // 為了模擬隊頭阻塞問題,看響應是否按順序返回
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            HttpServerResponse response = ctx.response();
            response.putHeader("content-type", "text/plain"); // 設置響應頭
            response.end("你好,這裏是 /test-1 的響應!");
        });

        router.get("/test-2").handler(ctx -> {
            HttpServerResponse response = ctx.response();
            response.putHeader("content-type", "application/json");
            JsonObject myJson = new JsonObject()
                    .put("message", "成功訪問 /test-2")
                    .put("timestamp", System.currentTimeMillis());
            response.end(myJson.encodePrettily());
        });
        router.get("/test-3").handler(this::handleTest3);
        server.requestHandler(router);
        server.listen(8080,"localhost");
        super.start(startPromise);
    }

    private void handleTest3(RoutingContext ctx) {
        HttpServerResponse response = ctx.response();
        response.putHeader("content-type", "text/plain; charset=utf-8");
        response.end("你好, " + "! 歡迎來到 /test-3。");
    }

    public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();
        vertx.deployVerticle(new SimpleWebServer());
    }
}

下面是客户端的代碼:

HttpClientOptions options = new HttpClientOptions()
        .setProtocolVersion(HttpVersion.HTTP_1_1)
        .setPipelining(true)
        .setPipeliningLimit(4);
Vertx vertx = Vertx.vertx(new VertxOptions().setWorkerPoolSize(40));
HttpClientAgent client = vertx.createHttpClient(options);
List<Future<HttpClientResponse>> futureList = new ArrayList<>();
RequestOptions requestOptionsOne = new RequestOptions()
        .setMethod(HttpMethod.GET)
        .setHost("localhost")
        .setPort(8080)
        .setURI("/test-1");

RequestOptions requestOptionsTwo = new RequestOptions()
        .setMethod(HttpMethod.GET)
        .setHost("localhost")
        .setPort(8080)
        .setURI("/test-2");
RequestOptions requestOptionsThree = new RequestOptions()
        .setMethod(HttpMethod.GET)
        .setHost("localhost")
        .setPort(8080)
        .setURI("/test-3");
List<RequestOptions> requestOptionsList = new ArrayList<>();
requestOptionsList.add(requestOptionsOne);
requestOptionsList.add(requestOptionsTwo);
requestOptionsList.add(requestOptionsThree);
for (int i = 0; i < 3; i++) {
    Future<HttpClientResponse> responseFuture = client.request(requestOptionsList.get(i)).compose(HttpClientRequest::send);
    futureList.add(responseFuture);
}
for (Future<HttpClientResponse> responseFuture : futureList) {
    HttpClientResponse clientResponse = responseFuture.await();
    Buffer bodyBuf = null;
    try {
        bodyBuf = clientResponse.body().toCompletionStage()
                .toCompletableFuture()
                .get();
        String body = bodyBuf.toString("UTF-8");
        System.out.println(body);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } catch (ExecutionException e) {
        throw new RuntimeException(e);
    }
}

最終輸出結果為:

你好,這裏是 /test-1 的響應!
{
  "message" : "成功訪問 /test-2",
  "timestamp" : 1759046207167
}
你好, ! 歡迎來到 /test-3。

可以看到Vertx的思路和我們想象的是一致的。 到現在我們總結一下,Http 1.0面臨的問題,雖然在一個TCP連接上可以併發的發送報文,但是由於沒有報文標識,報文請求和響應沒辦法形成對應關係,所以就只能要求請求報文排隊被處理,然後按請求順序返回。

那Http/2的解藥就是改造Http 1.1的報文,為了解決請求和響應之間的映射關係,Http/2 為報文引入了標識符的概念:

Streams are identified with an unsigned 31-bit integer.

流由一個無符號的31位整數來標識

那什麼是流? 在RFC 7540 我們可以看到對應的描述:

A "stream" is an independent, bidirectional sequence of frames exchanged between the client and server within an HTTP/2 connection.

一個“流”(Stream)是存在於一個HTTP/2連接內部的,客户端與服務器之間交換的一個獨立的、雙向的幀(Frame)序列

因為是服務器和客户端之間交換,所以是雙向的,那幀是什麼? 幀是HTTP/2的基本單元,HEADERS(頭部)幀DATA(數據)幀構成了HTTP請求和響應的基礎,其他類型的幀,如SETTINGSWINDOW_UPDATE*和*PUSH_PROMISE,則用於支持HTTP/2的其他各項功能。這也就是HTTP/2的另一個重要特性: 基於二進制的協議。

img

Tomcat對流水線的支持

按道理測試應該結束了,但是我還想測試一下Tomcat對流水線的支持是怎麼樣的,Tomcat對流水線的支持見參考鏈接[5]關於maxKeepAliveRequests的説明:

The maximum number of HTTP requests which can be pipelined until the connection is closed by the server.

直到連接被關閉之前,這個連接可以被管線化發送HTTP請求的最大數量。

Setting this attribute to 1 will disable HTTP/1.0 keep-alive, as well as HTTP/1.1 keep-alive and pipelining.

將此屬性設置為1將會禁用HTTP/1.0的keep-alive功能,以及Http/1.1的keep-alive和流水線功能。

Setting this to -1 will allow an unlimited amount of pipelined or keep-alive HTTP requests. If not specified, this attribute is set to 100.

將這個值設置為-1表示在流水線上不限制最大請求數量,如果沒有具體設置,那麼這個屬性是100。

遺憾的是我用vertx向Tomcat發流水線支持

二進制的協議

熟悉HTTP協議的同學可能會有點印象,HTTP/1.1是基於文本的,那這個基於文本的是什麼意思? 底層不都是二進制嘛? 在Java裏面,我們可以通過String,將字符串轉成字節數組。本質上就是二進制式的。那HTTP/2的二進制式是什麼意思?我們在RFC-7540裏面可以看到,相同的報文在1.1和2.0格式之間的區別:

// request line
GET /resouce HTTP/1.1           HEADERS
// request header 
Host: example.org          ==>     + END_STREAM
Accept: image/jpeg                 + END_HEADERS
                                       :method = GET
                                       :scheme = https
                                       :path = /resource
                                       host = example.org
                                       accept = image/jpeg

注意我們在解析HTTP/1.1的時候是通過\r\n,分別拿出請求頭不同類型比如host、accept。 然後我們可以用正則表達式分別取出報文裏面的各個類型的字段,但正則表達式性能很差,Tomcat是一個字符一個字符讀取,讀完請求行, 讀請求頭, 讀請求體。·在Oracle 給的JDK 8示例中,有一個用新特性寫的HTTP Server,用的是正則來分割:

img

Tomcat則是一個部分,一個部分的截取,在Http11Processor中我們可以看到這一點:

img

等等,你講了這麼多,還是沒講清楚這個二進制協議是個什麼意思, 只是看起來換了一種格式,原先的字符串變換成了Map(存儲k-v對的集合)一樣。是的二進制格式的語義就是將原先基於文本的報文變成了更加緊湊的報文格式,在解析HTTP/2報文的時候我們根據type就能知道當前的幀是哪種類型,是header 還是 data。

由於HTTP是無狀態的,所以就算是相同的請求地址,我們每次請求都得帶上和上一次請求相同的請求頭,這無疑也有些浪費帶寬,由此就引出了壓縮對象頭。

壓縮請求頭

HTTP/2 引入了一個靜態表,這個靜態表是預定義的頭字段靜態列表組成,比如method、status等等。靜態表是隻讀的。原本我以為動態表維護的是若干key-value對,在一開始value是沒有值的,在第一次通信之後,請求方式填值,後面發送請求的時候,發送列表的索引即可。實際的是靜態表已經維護了這些key-value對,通信的過程中只需要發送對應的下標即可:

img

注意到不是所有的請求頭的值都是預定義好的,比如cookie,HTTP/2還引入了霍夫曼編碼對請求值的字符串進行壓縮。動態表這是一個連接期間由客户端和服務端共同維護的表,動態表由一個"先進先出"順序維護的頭部字段列表組成。動態表中最先加入位於最低的索引位置,動態表中最老的位於最高的位置。

多路複用

於是現在我們就可以解決隊頭阻塞問題了,因為有了流的ID,我們甚至可以做到更進一步,將一個HTTP請求拆成多個流, 也就是請求頭流、數據流、其他流,我們可以認為流就是帶上了streamID的幀。

img

那該如何兼容從前

現在我們是客户端要和服務端通信,由於HTTP/2和HTTP/1.1的報文格式都發生了改變,所以就需要確定通信的時候使用哪個版本進行通信。除此之外,在標準制定的時候,大家都將TLS標記為HTTP/2可選的組件,這一點是有點出人意料的,原因在於HTTP/2脱胎於google的SPDY,而SPDY又強依賴TLS。但是Firefox和Chrome都明確地表示,他們只會實現基於TLS的http2. 選擇TLS的原因是希望保護以及尊重用户的隱私。

所以HTTP/2就有兩個版本,一個是HTTP/2 Cleartext 也就是明文版的HTTP/2, 簡稱為H2C,另一個是基於TLS的HTTP/2。 但是引入TLS,就會有額外加密解密的成本,這往往對於服務間調用的不必要的,這也就是本篇文章的主題,在服務之間的調用使用H2C來提升性能。

這在日常生活中也很常見,假設我們有一個支持120W的充電頭,那給手機充電的時候,充電頭會直接上120w的充電功率嘛?當然不會他們會首先進行握手,握手的一刻,充電頭會進行上報自己支持的充電協議,手機會從中選擇自己支持的協議回覆給充電頭,然後開始充電。

對此客户端會先會首先給出自己支持的協議版本,這也就是Application-Layer Protocol Negotiation,客户端先發送一個協議優先級列表給服務器,由服務器最終選擇一個合適的協議版本。這個協商的基礎是建立在TLS之上的,現在是一個HTTP1.1的網站,沒有TLS,TCP連接建立之後,那該發送什麼格式的報文? 對此的解決方案是客户端先使用HTTP/1.1的報文格式,然後發送一個請求升級協議的請求頭:Upgrade: h2c

GET / HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>

這表明客户端希望升級到H2C,如果服務器支持,就響應101,連接升級到h2c。如果一個老舊的服務器不認識這個請求頭,它會根據規範忽略這個它不認識的頭部,返回一個標準的響應200。這樣保證即使升級失敗,也不影響後續的通信。

但如果你先驗的確認服務端支持H2C,就可以避免這個升級的過程,這也被稱為先驗的方式支持h2c(prior knowledge)。

走向HTTP/3簡介

HTTP/1.1 連接上的隊頭阻塞問題被HTTP/2解決之後,下一個問題就是TCP層的隊頭阻塞問題,所謂TCP層的隊頭阻塞問題。TCP處理數據時有嚴格的前後順序,先發送的要先被處理。舉個例子: 在一個TCP連接上,我們發送了四個Stream,Stream1、Stream3、Stream4都到了,但是Stream2的第三個frame丟失了,於是接收方要求發送方重傳,Stream3和Stream4雖然到達但是不能被處理。那麼這時整條TCP連接上排在Stream2之後的報文都被阻塞。

HTTP/3的解藥是加強了UDP,也就是QUIC ,UDP 的數據包在接收端沒有處理順序,即使中間丟失一個包,也不會阻塞整條連接,其他的資源會被正常處理。

現在大型網站的架構

現在我們可以來聊一下網站的架構演進,一般我們會有若干微服務,微服務前面會有一個網關,這個網關是流量的入口。但是我們為了保護我們的內網IP,我們一般會用Nginx做反向代理,同時做一個靜態服務器資源服務器。

img

但是往往單機不太牢靠,為了提升整體的可靠性,我們採取了主從的結構,我們期待的是假設主節點掛掉之後,從節點升級為主節點。這也就引出了Keepalived。有了keepAlived之後, 主節點因為意外掛掉之後,從節點就能接管流量。但是很快我們的流量越來越大,單台Nginx已經沒有辦法再滿足我們的要求了,於是我們想到了Nginx集羣。

那誰對Nginx進行負載均衡? 這也就引出了HAProxy,HAProxy可以對流量進行轉發, 做健康檢查。 可能有同學還是會問,如果單台HAProxy還是不夠用怎麼辦? 這裏就引出了LVS還有ECMP來分發流量,當然也可以直接向雲廠商購買此類服務。

這不是我們本篇的重點,我們不做過多介紹。現在流量涌入之後走HTTP/2, Nginx轉發到GateWay這裏又降級成了HTTP/1.1, 這無疑是一個性能瓶頸,因此我們的設想是在流量進入到內部服務的時候走h2c。服務之間的調用也儘可能的走h2c, 如果內部服務的調用是Feign的話。本篇我們演示的是在服務調用之間啓用H2C,服務從負載均衡到網關這一塊,需要不少組件,有些龐大,這裏只是做一個理論的推測。目前看Nginx轉發的時候是不支持HTTP 2C的, proxy_version 最高只到1.1。

我們只能將目光轉向別的反向代理服務器,比如caddy。或者是envoy,在參考文檔[10] 可以看到envoy是支持代理到上游的時候支持HTTP/2C的。這裏我並沒有做實際的驗證,僅僅論證的是理論上的可行性。

如何在Spring Boot 中啓用HTTP/2c

如何觀測和驗證

現在我們有了理論,現在我們來驗證我們的猜想,首先我們要知道Tomcat對HTTP/2C的支持,這點我們可以在Tomcat的文檔裏面可以看到:

img

也就是説Tomcat是支持H2C的,但是JDK 8 不支持 ALPN機制意味着不能使用基於TLS的HTTP/2協議。 那怎麼知道我們成功啓用了HTTP/2C呢? 我們通過觀測Tomcat的日誌來驗證。在Spring Boot的配置文件裏面加上下面的配置:

logging.level.org.apache.coyote=TRACE

我們就能觀測到Tomcat解析報文的過程。注意在Spring Boot 2.0的時候我們採用curl來發送HTTP/2的請求報文來驗證。 注意在Spring Boot 2.0的文檔裏面指出 Spring Boot 2.x的文檔指出 不支持h2c, 見參考文檔[7] , 但是實際是支持的,我們可以在參考文檔[8] 裏面可以看到這一點:

The documentation is probably worded a bit too strongly at the moment. What it really means is that there's no support for enabling h2c via configuration properties.

You can still do so by adding a little bit of your own configuration. For example, the following customiser will enable h2c with Tomcat

@Bean
public TomcatConnectorCustomizer customizer() {
    return (connector) -> connector.addUpgradeProtocol(new Http2Protocol());
}

根據我實際的測試,Spring Boot 2.7.14也是能夠支持直接開啓H2c的,不需要通過這個Bean配置。

Spring Boot 啓用H2C

通過一行配置啓用即可:

server.http2.enabled=true
server.ssl.enabled=false

然後我們使用curl來驗證請求:

curl --http2-prior-knowledge  http://localhost:9090/test

注意這裏我電腦的curl版本有點問題,所以我這裏用Vertx來驗證:

Vertx vertx = Vertx.vertx();
WebClientOptions options = new WebClientOptions()
        .setSsl(false)
        // 這裏直接用H2的報文,不用從Http/1.1升級到H2C。
        .setHttp2ClearTextUpgrade(false)
        .setSslEngineOptions(new JdkSSLEngineOptions())
        .setUseAlpn(false)
        .setProtocolVersion(HttpVersion.HTTP_2);
WebClient client = WebClient.create(vertx, options);
client.get(9090, "localhost", "/test")
        .send()
        .onSuccess(response ->
                System.out.println(response.bodyAsString()))
        .onFailure(err ->
                System.out.println(err.getMessage()));

然後我們觀測日誌:

img

常見的HTTP Client 啓用H2C

我們這裏只講OkHttp,首先我們引入依賴:

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version>
</dependency>

下面是代碼:

OkHttpClient client = new OkHttpClient.Builder()
        .protocols(Collections.singletonList(Protocol.H2_PRIOR_KNOWLEDGE))
        .build();
Request request = new Request.Builder()
        .url("http://localhost:8080/test")
        .build();
Response response = client.newCall(request).execute();
System.out.println("Protocol used: " + response.protocol());

實測通過,Tomcat裏面輸出了Http/2的標誌。然後我們就能在一般的Spring Cloud 項目中讓服務之間的調用使用H2C了。

總結一下

本篇的思路是先從HTTP/1.1的隊頭阻塞問題開始,所謂隊頭阻塞説的是HTTP/1.1引入的流水線特性,流水線特性從字面上特性來説就是允許客户端在收到上一個請求對應的響應之前發送下一個響應,其實表達的應當是將多個HTTP請求報文在一條TCP連接上批量發送。但是由於沒有報文ID,不知道請求和響應的對應關係,所以HTTP協議標準規定響應按順序返回。

但如果第一個請求處理時間比較長,那麼就算是其他請求已經被處理,也需要讓第一個請求處理完,然後按順序返回。對此HTTP/2的解藥是,為報文加上了ID。注意到HTTP是無狀態的,這意味着相同的接口,每次都會帶上相同的請求頭,比如請求方式等等。對此HTTP/2的解藥是引入靜態表和動態表,靜態表統計了常用的請求頭,在發送的時候只需要發送索引就可以了。而如果你有不在靜態表的請求頭,可以用動態表,比如cookie,但有時候cookie的內容比較大,於是HTTP/2引入了哈夫曼編碼進行壓縮。

除此之外,由於HTTP/1.1 是基於文本的,基於文本的我們姑且就可以理解為字符串,我們解析的時候就要按規則去截取,但是用正則又比較影響性能,主流的HTTP服務器都是從字節數組挨個解析。HTTP/2引入了更換了報文的格式,我們通過frame的類型和長度就能知道該讀多少,確定的格式解析效率更高。

但是HTTP/2在指定過程中,為了保護隱私,主流的服務器廠商實現的都是基於TLS的HTTP/2, 儘管這在標準中是可選的,沒有TLS的HTTP/2 也被稱之為HTTP/2C。

但如何兼容從前呢,HTTP標準規定,從HTTP/1.1升級到HTTP/2, 頭一次先放鬆一個請求升級的報文,也就是報文裏面帶上, upgrade: h2c。但如果你知道對面的服務器是支持H2C的,就可以省掉這個加密過程。我們希望流量從負載均衡走到我們服務的過程中,都是H2C,這是我們本篇的目標。我們在探索的過程中發現,Spring Boot 的官方文檔有點問題,其實Spring Boot 2.x是可以通過配置開啓的。

如果你的服務端是微服務,且用的是Spring Cloud Open Feign來實現遠程調用,那麼我們可以通過替換HTTP Client, 來實現服務之間調用用HTTP/2C。但Nginx做反向代理的時候是不支持轉發到服務上是H2C的,我們不得已只能換別的反向代理服務器。

參考資料

[1] HTTP的現狀 https://http2-explained.haxx.se/zh/part2

[2] https://datatracker.ietf.org/doc/html/rfc2616#section-19.2

[3] https://bugzilla.mozilla.org/show_bug.cgi?id=264354

[4] https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/H...

[5] https://tomcat.apache.org/tomcat-8.0-doc/config/http.html#Pro...

[6] 一文帶你淺入淺出Keepalived https://zhuanlan.zhihu.com/p/566166393

[7] https://docs.spring.io/spring-boot/docs/2.0.0.M6/reference/ht...

[8] https://github.com/spring-projects/spring-boot/issues/21997

[9] https://caddyserver.com/docs/caddyfile/directives/reverse_proxy

[10] https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/clu...

[11] HTTP/3 原理實戰 https://zhuanlan.zhihu.com/p/143464334

user avatar lankerens 頭像 mo_or 頭像 knifeblade 頭像 91cyz 頭像
4 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.