
考慮很久,決定還是寫一下這篇文章,主要是 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 已經提供對應的功能。