考慮很久,決定還是寫一下這篇文章,主要是 AJP 技術太老,我只能說 Long long ago ,估計我在用這個技術的時候,很多同學小學還沒有畢業。但是沒有問題,這篇文章只是一個架構啟發,不會浪費你時間讓你學習 20 年前的技術和知識。
Apache JServ Protocol
Apache JServ 協議,簡稱 AJP ,是一種二進制協議,可以將來自 Web 服務器的入站請求代理到位於 Web 服務器後面的應用程序服務器,部署結構如下:
通常我們不希望直接將應用服務暴露到互聯網上,有安全問題,當然還涉及到 DNS,IP等問題,我們會做一個互聯網請求入口的 Gateway,也就是一個Web服務負責入站請求,然後再轉發給內部的Web應用服務器,這樣架構就靈活很多。
為何要使用 AJP 這個二進制協議?我們知道 HTTP 1.1 是文本協議,所以解析協議的工作量還是有的,如果 Gateway 的 Web 服務器已經將 HTTP 協議解析啦,為何不復用解析後的結果,形成一個更高效的二進制結構,然後傳送給後端的 Web 服務器,這樣後端 Web 服務器就會省去解析 HTTP 文本協議這個動作,節約了計算,速度也快啦;
此外 AJP 是長連接,和 HTTP 1.1 的短連接也不一樣,可以避免反覆的 HTTP 短連接創建,也提高了網絡的傳輸效率,這些就是 AJP 的作用。如果 Gateway 直接是反向代理到後端服務器,還是走普通 HTTP 請求,就會涉及大量短連接、 HTTP 協議重新解析的問題。
當然在實際的開發中,進行 AJP 配置非常少的,大家還是採取的標準的 HTTP 協議的反向代理方式,其中一個主要的原因,就是 AJP 還是有些複雜。首先 gateway 上要配置 AJP ,同時應用還需要提供 AJP 接入能力,如果使用 Tomcat 還好,現在基本都是基於 Netty 的嵌入式 Web Server ,幾乎沒有人考慮 AJP 這件事情啦。當然我個人也是這樣的,早期大家都是 Apache + mod_jk + tomcat 部署的,現在也都是 HTTP 協議的。
負載均衡之動態主機問題
假設我們有了下圖的部署結構, Gateway 負責入站請求,然後由 Nginx 進行轉發,可以選擇 HTTP 1.1 或者 AJP ,都還好。
上圖的這個結構,大家會發現要解決一個問題,就是維護後端服務列表的問題,也就是 Nginx 中所說 Upstreams 主機動態維護的問題,看一個典型的 Nginx 配置,如下:
如果 appservers 背後的應用有上線下線的問題,那該怎麼辦?也好辦,就是通知 Nginx ,動態更新 upstream 對應的主機列表,這樣就是可以啦。這個特性,你需要購買 Nginx Plus,當然也有一些 Nginx 開源的方案,都會提供對應的 upstream 主機動態更新的特性。
當然如果你的服務架構中有服務註冊中心,如採用 Spring Cloud Gateway 架構,如果有了 Eureka 等服務註冊中心,那麼就不用擔心這些 upstream 主機動態維護的問題,服務註冊中心就會解決這個問題,如下圖:
RSocket 和 AJP 整合
我們都知道 RSocket 採取是的外連方式,就是我不提供端口監聽,我會連接到一個 Broker ,然後 Broker 來幫助我處理入站請求。藉助 RSocket 這一模式,我們將 Gateway 的模式調整為如下:
首先我們通過 Webflux 對外提供 HTTP 訪問需求,這個是異步化的。當然 Webflux 會默認解析 HTTP 頭, Body 設置為不解析,還是 Netty 的 ByteBuf 。接下來我們將 HTTP 請求轉為換為 AJP 的數據結構,其實就是上面講到那個高效的二進制結構,大概的結構如下:
接下來 Gateway 會將 AJP 的二進制結構體添加到為 RSocket Payload 的 header 中,將 HTTP Body 設置為 Payload 的 data ,然後根據虛擬主機名或服務名從連接到 Gateway 的 RSocket 連接中找到對應的 TCP 連接,然後將這個 RSocket Payload 發送給後端 Web 服務器。
後端 web 服務器在收到 RSocket 請求後,然後讀取出 AJP 數據,構建出內部的 HTTPRequest 對象,然後轉發給對應的 HTTPHandler 完成 HTTP 請求處理,最後將返回的 HTTPResponse 對象再進行 AJP 處理,構建出 Payload ,返回給 Gateway ,然後 Gateway 再解析 AJP ,輸出 HTTP Response ,當然這個也是標準的 AJP 流程。
這裡我們進行了一些調整,傳統的是給 Web 應用配置 AJP 監聽端口,相當於 AJP Server ,接受 AJP 請求,現在調整為 RSocket ,沒有監聽端口,而是直接連接到 Gateway 。
RSocket AJP 這種架構有什麼好處?
無監聽端口:RSocket 採用的是外連的方式,本地並沒有啟動 HTTP 端口和 AJP Server端口,這樣比較安全,同時也節省了系統的資源。
負載均衡簡單:由於後端 web 服務器都主動連接到 Gateway 上啦,而且提供了對應的元信息,如對應的域名等, Gateway內部就建立好路由表啦,不需要服務註冊中心等接入,當然也不需要你手動維護,都只自動化的,只要控制應用上下線就可以。
AJP 序列化方式非常高效,這個前面說過,對比 HTTP 解析,這個性能不用說啦。
部分 Zero Copy 支持:如 HTTP Body 這部分,基於 Netty 的 ByteBuf,這個是完全沒有問題的,不需要反覆的內存 copy ,而且 RSocket 是直接支持 Netty 的 ByteBuf 構建 Payload 的。
長連接支持:RSocket 是長連接的,這個和 AJP 是一致的,不用在擔心 HTTP 短連接搞出的 TIME WAIT 問題啦,而不用搞什麼 TIME WAIT 優化等,默認就可以啦。你不相信 TIME WAIT 問題?你在 ATA 上搜索一下試試,都有 1274 篇文章,不我不知道有多少同學碰到過,反正我不止一次啦。
非常好的靈活性:gateway 已經進行了 http 解析,我們經常說的 session sticky ,也就是根據 cookie 綁定到某一臺 backend server ,這個就非常容易實現。
這種方式非常有靈活性,開發階段打開 HTTP 服務,直接 HTTP REST API 測試,這些都沒有問題,在上線後,只要開啟 RSocket ,然後連接到 Gateway Broker 上就可以,然後這一切都是自動化的。
拓撲擴展延伸
當然這個架構,還可以擴展到各種部署結構上,如 Kubernetes 上,你不需要什麼 ingress ,容器啟動後直接連接得到 gateway broker 就可以啦,只需要提供 web 應用對應的接入域名或者服務名就可以啦,也不需要你創建什麼服務名, DNS 等。這種方式完全對網絡和運維繫統無任何要求,無論你使用任何容器管理系統都可以。
多語言擴展
由於 AJP 是標準的協議,所以同樣可以套用在其他的語言開發上,其實就是減少 HTTP 協議解析,然後從 AJP 中構建出 HTTP 請求,然後安裝標準的 Web 框架處理就可以。如和 JavaScript 結合時, 你完全基於 AJP 構建出 Request 對象,然後交給特定函數處理,其實就是遵循 Service Worker 規範。
其他語言,也都有對應的 webserver interface 規範,如 Ruby的 Rack , Python 的 WSGI 等,主要 AJP 和這些規範對接即可.
總結
藉助 RSocket 的架構提供,我們可以將之前比較複雜的方案簡化,當然最最重要的是性能的提升,即便之前的一些性能提升技術點,可能由於一些約束等,現在和 RSocket 對接,那些問題都不存在啦。
有同學可能問,要實現這個架構複雜否?如果你基於 Spring 架構的話,我可以說任何人都能開發出來。你只需要創建一個 Spring Boot 應用,啟動 RSocket 監聽,然後其他 Spring Boot Web 應用通過 RSocketRequester 連接上來,接下來就是一些AJP相關的編解碼工作,然後調用一下 Spring Web 提供的 HTTPHandler 接口,就這些工作量,Spring RSocket 已經提供對應的功能。