Java 理論與實踐 Web 層的狀態複製

有多種用於群集 Web 應用程序的方法

不管正在構建的是 J2EE 還是 J2SE 服務器應用程序,都有可能以某種方式使用 Java Servlet —— 可能是直接地通過像 JSP 技術、Velocity 或者 WebMacro 這樣的表示層,也可能通過一個基於 servlet 的 Web 服務實現,如 Axis 或者 Glue。Servlet API 提供的一個最重要的功能是會話管理 —— 通過 HttpSession接口進行用戶狀態的認證、失效和維護。

會話狀態

幾乎每一個 Web 應用程序都有一些會話狀態,這些狀態有可能像記住您是否已登錄這麼簡單,也可能是您的會話的更詳細的歷史,如購物車的內容、以前查詢結果的緩存或者 20 頁動態問卷表的完整響應歷史。因為 HTTP 協議本身是無狀態的,所以需要將會話狀態存儲在某處並與瀏覽會話以某種方式相關聯,使得下次請求同一 Web 應用程序的頁面時可以容易地獲取。幸運的是,J2EE 提供了幾種管理會話狀態的方法 —— 狀態可以存儲在數據層,用 Servlet API 的 HttpSession 接口存儲在 Web 層,用有狀態會話 bean 存儲在 Enterprise JavaBeans(EJB)層,甚至用 cookie 或者隱藏表單字段將狀態存儲在客戶層。不幸的是,會話狀態管理不當會帶來嚴重的性能問題。

如果應用程序能夠在 HttpSession 中存儲用戶狀態,這種方法通常比其他方法更好。在客戶端用 HTTP cookie 或者隱藏表單字段存儲會話狀態有很大的安全風險 —— 它將應用程序的一部分內部內容暴露給了非受信任的客戶層。(一個早期的電子商務網站將購物車內容(包括價格)存儲在隱藏表單字段中,從而可以很容易被非法利用,讓任何瞭解 HTML 和 HTTP 的用戶可以以 0.01 美元購買任何商品。噢)此外,使用 cookie 或者隱藏表單字段很混亂,容易出錯,並且脆弱(如果用戶禁止在瀏覽器中使用 cookie,那麼基於 cookie 的方法就完全不能工作)。

在 J2EE 應用程序中存儲服務器端狀態的其他方法是使用有狀態會話 bean,或者在數據庫中存儲會話狀態。雖然有狀態會話 bean 在會話狀態管理方面有更大的靈活性,但是在可能的情況下,將會話狀態存儲在 Web 層仍然有好處。如果業務對象是無狀態的,那麼通常可以僅僅添加更多 Web 服務器來擴展應用程序,而不用添加更多 Web 服務器和更多 EJB 容器, 這樣的成本一般要低一些並且容易完成。使用HttpSession 存儲會話狀態的另一個好處是 Servlet API 提供了一種會話失效時通知的容易方法。在數據庫中存儲會話狀態的成本可能難以承受。

servlet 規範沒有要求 servlet 容器進行某種類型的會話複製或者持久性,但是它建議將狀態複製作為 servlet 首要 存在理由(raison d'etre) 的重要部分,並且它對作為進行會話複製的容器提出了一些要求。會話複製可以提供大量好處 —— 負載平衡、伸縮性、容錯和高可用性。相應地,大多數 servlet 容器支持某種形式的 HttpSession 複製,但是複製的機制、配置和時間是由實現決定的。

HttpSession API

簡單地說, HttpSession 接口支持幾種方法,servlet、JSP 頁或者其他表示層組件可以用這些方法來跨多個 HTTP 請求維護會話信息。會話綁定到特定的用戶,但是在 Web 應用程序的所有 servlet 中共享 —— 不特定於某一個 servlet。一種考慮會話的有用方法是,會話像一個在會話期間存儲對象的 Map —— 可以用 setAttribute 按名字存儲會話屬性,並用 getAttribute 提取它們。 HttpSession 接口還包含會話生存週期方法,如 invalidate() (它通知容器應丟棄會話)。清單 1 顯示 HttpSession 接口最常用的元素:

清單 1. HttpSession API

public interface HttpSession {
 Object getAttribute(String s);
 Enumeration getAttributeNames();
 void setAttribute(String s, Object o);
 void removeAttribute(String s);
 boolean isNew();
 void invalidate();
 void setMaxInactiveInterval(int i);
 int getMaxInactiveInterval();
 ...
}

理論上,可以跨群集一致性地完全複製會話狀態,這樣群集中的所有節點都可以服務任何請求,一個簡單的負載平衡器可以以輪詢方式傳送請求,避開有故障的主機。不過,這種緊密的複製有很高的性能成本,並且難於實現,當群集接近某一規模時,還會有伸縮性的問題。

一種更常用的方式是將負載平衡與會話 相似性(affinity) 結合起來 —— 負載平衡器可以將會話與連接相關聯,並將會話中以後的請求發送給同一服務器。有很多硬件和軟件負載平衡器支持這個功能,並且這意味著只有主連接主機和會話需要故障轉移到另一臺服務器時才訪問複製的會話信息。

複製方式

複製提供了一些可能的好處,包括可用性、容錯和伸縮性。此外,有大量會話複製的方法可用:方法的選擇取決於應用程序群集的規模、複製的目標和 servlet 容器支持的複製設施。複製有性能成本,包括 CPU 週期(存儲在會話中的序列化對象)、網絡帶寬(廣播更新),以及基於磁盤的方案中寫入到磁盤或者數據庫的成本。

幾乎所有 servlet 容器都通過存儲在 HttpSession 中的序列化對象進行 HttpSession 複製,所以如果是創建一個分佈式應用程序,應當確保只將可序列化對象放到會話中。(一些容器對像 EJB 引用、事務上下文、還有其他非可序列化的 J2EE 對象類型有特殊的處理。)

基於 JDBC 的複製

一種會話複製的方法是序列化會話內容並將它寫入數據庫。這種方法相當直觀,其優點是不僅會話可以故障轉移到其他主機,而且即使整個群集失效,會話數據也可以保存下來。基於數據庫的複製的缺點是性能成本 —— 數據庫事務是昂貴的。雖然它可以在 Web 層很好地伸縮,但是它可能在數據層產生伸縮問題 —— 如果群集增長大到一定程度,擴展數據層以容納會話數據會很困難或者成本無法接受。

基於文件的複製

基於文件的複製類似於使用數據庫存儲序列化的會話,只不過是使用共享文件服務器而不是數據庫來存儲會話數據。這種方式的成本一般比使用數據庫的成本(硬件成本、軟件許可證和計算開銷)低,其代價則是可靠性(數據庫可提供比文件系統更強的持久化保證)。

基於內存的複製

另一種複製方式是與群集中的一個或者多個其他服務器共享序列化的會話數據副本。複製所有會話到所有主機中提供了最大的可用性,並且負載平衡最容易,但是因為複製消息所消耗的每個節點的內存和網絡帶寬,最終會限制群集的規模。一些應用服務器支持與“夥伴(buddy)”節點的基於內存的複製,其中每一個會話存在於 主服務器上和一臺(或更多)備份服務器上。這種方案比將所有會話複製到所有服務器的伸縮性更好,但是當需要將會話故障轉移到另一臺服務器上時會使負載平衡任務複雜化,因為它必須找出另外哪一臺(幾臺)服務器有這個會話。

時間考慮

除了決定如何存儲複製會話數據,還有什麼時候複製數據的問題。最可靠但也最昂貴的方法是每次數據改變時複製它(如每次 servlet 調用結束)。不那麼昂貴、但是在故障時會有丟失一些數據的風險的方法是在每超過 N 秒時複製數據。

與時間問題有關的問題是,是複製整個會話還是隻試嘗複製會話中改變了的屬性(它包含的數據會少得多)。這些都需要在可靠性和性能之間進行取捨。Servlet 開發人員應當認識到在故障轉移時,會話狀態可能變得“過時”(是幾次請求前的複製),並應當準備處理不是最新的會話內容。(例如,如果一個interview 的第 3 步產生一個會話屬性,而用戶在第 4 步時,請求被故障轉移到一個具有兩次請求之前的會話狀態複製的系統上,那麼第 4 步的 servlet 代碼應預備在會話中找不到這個屬性,並採取相應的行動 —— 如重定向,而不是認定它會在那裡、並在找不到它時拋出一個 NullPointerException 。)

容器支持

Servlet 容器的 HttpSession 複製選項以及如何配置這些選項是各不相同的。IBM WebSphere ®提供的複製選項是最多的,它提供了在內存中複製或者基於數據庫的複製、在 servlet 末尾或者基於時間的複製時間、傳播全部會話快照(JBoss 3.2 或以後版本)或者只傳播改變了的屬性等選擇。基於內存的複製基於 JMS 發佈-訂閱,它可以複製到所有克隆、一個“夥伴”複製品或者一個專門的複製服務器。

WebLogic 還提供了一組選擇,包括內存中(使用一個夥伴複製品)、基於文件的或者基於數據庫的。JBoss 與 Tomcat 或者 Jetty servlet 容器一同使用時,進行基於內存的複製,可以選擇 servlet 末尾或者基於時間的複製時間,而快照選項(在 JBoss 3.2 或以後版本)是隻複製改變了的屬性。Tomcat 5.0 為所有群集節點提供了基於內存的複製。此外,通過像 WADI 這樣的項目,可以用 servlet 過濾機制將會話複製添加到像 Tomcat 或者 Jetty 這樣的 servlet 容器中。

改進分佈式 Web 應用程序的性能

不管決定使用什麼機制進行會話複製,可以用幾種方式改進 Web 應用程序的性能和伸縮性。首先記住,為了獲得會話複製的好處,需要在部署描述符中將 Web 應用程序標記為 distributable,並保證在會話中的所有內容都是可序列化的。

保持會話最小

因為複製會話有隨著會話中的對象圖(object graph) 的變大而增加成本,所以應當儘可能地在會話中少放置數據。這樣做會減少複製的序列化的開銷、網絡帶寬要求和磁盤要求。特別地,將共享對象存儲在會話中一般不是好主意,因為它們需要複製到它們所屬的 每一個會話中。

不要繞過 setAttribute

在改變會話的屬性時,要知道即使 servlet 容器只是試圖做最小的更新(只傳播改變了的屬性),如果沒有調用 setAttribute ,容器也可能沒有注意到已經改變的屬性。(想像在會話中有一個 Vector ,表示購物車中的商品 —— 如果調用 getAttribute() 獲取 Vector 、然後向它添加一些內容,並且不再次調用setAttribute ,容器可能不會意識到 Vector 已經改變了。)

使用細化的會話屬性

對於支持最小更新的容器,可以通過將多個細化的對象而不是一個大塊頭放到會話中而降低會話複製的成本。這樣,對快速改變的數據的改變也不會迫使容器去序列化並傳播慢速改變的數據。

完成後使之失效

如果知道用戶完成了會話的使用(如,用戶選擇註銷登錄),確保調用 HttpSession.invalidate() 。否則,會話將持久化直到它失效,這會消耗內存,並且可能是長時間的(取決於會話超時時間)。許多 servlet 容器對可以跨所有會話使用的內存的數量有一個限制,達到這個限制時,會序列化最先使用的會話並將它寫到磁盤上。如果知道用戶使用完了會話,可以使容器不再處理它並使它作廢。

保持會話乾淨

如果在會話中有大的項,並且只在會話的一部分中使用,那麼當不再需要時應刪除它們。刪除它們會減少會話複製的成本。(這種做法類似於使用顯式 nulling 以幫助垃圾收集器,老讀者知道我一般不建議這樣做,但是在這種情況下,因為有複製,在會話中保持垃圾的成本要高得多,因此值得以這種方式幫助容器。)

結束語

通過 HttpSession 複製,Servlet 容器可以在構建複製的、高可用性的 Web 應用程序方面給您減輕很多負擔。不過,對於複製有一些配置選項,每個容器都不一樣,複製策略的選擇對於應用程序的容錯、性能和伸縮性有影響。複製策略的選擇不應當是事後的 —— 您應當在構建 Web 應用程序時就考慮它。並且,一定不要忘記進行負載測試以確定應用程序的伸縮性 —— 在客戶替您做之前。

Java 理論與實踐  Web 層的狀態複製


分享到:


相關文章: