J2EE應用程序的Web層狀態(tài)復制
大多數具有一定重要性的 Web 應用程序都要求維護某種會(huì )話(huà)狀態(tài),如用戶(hù)購物車(chē)的內容。如何在群集服務(wù)器應用程序中治理和復制狀態(tài)對應用程序的可伸縮性有顯著(zhù)影響。許多 J2SE 和 J2EE 應用程序將狀態(tài)存儲在由 Servlet API 提供的 HttpSession 中。本文作者分析了狀態(tài)復制的一些選項以及如何最有效地使用 HttpSession 以提供好的伸縮性和性能。
不管正在構建的是 J2EE 還是 J2SE 服務(wù)器應用程序,都有可能以某種方式使用 Java Servlet —— 可能是直接地通過(guò)像 jsp 技術(shù)、Velocity 或者 WebMacro 這樣的表示層,也可能通過(guò)一個(gè)基于 servlet 的 Web 服務(wù)實(shí)現,如 Axis 或者 Glue。Servlet API 提供的一個(gè)最重要的功能是會(huì )話(huà)治理 —— 通過(guò) HttpSession 接口進(jìn)行用戶(hù)狀態(tài)的認證、失效和維護。
會(huì )話(huà)狀態(tài)
幾乎每一個(gè) Web 應用程序都有一些會(huì )話(huà)狀態(tài),這些狀態(tài)有可能像記住您是否已登錄這么簡(jiǎn)單,也可能是您的會(huì )話(huà)的更具體的歷史,如購物車(chē)的內容、以前查詢(xún)結果的緩存或者 20 頁(yè)動(dòng)態(tài)問(wèn)卷表的完整響應歷史。因為 HTTP 協(xié)議本身是無(wú)狀態(tài)的,所以需要將會(huì )話(huà)狀態(tài)存儲在某處并與瀏覽會(huì )話(huà)以某種方式相關(guān)聯(lián),使得下次請求同一 Web 應用程序的頁(yè)面時(shí)可以輕易地獲取。幸運的是,J2EE 提供了幾種治理會(huì )話(huà)狀態(tài)的方法 —— 狀態(tài)可以存儲在數據層,用 Servlet API 的 HttpSession 接口存儲在 Web 層,用有狀態(tài)會(huì )話(huà) bean 存儲在 Enterprise JavaBeans(EJB)層,甚至用 cookie 或者隱藏表單字段將狀態(tài)存儲在客戶(hù)層。不幸的是,會(huì )話(huà)狀態(tài)治理不當會(huì )帶來(lái)嚴重的性能問(wèn)題。
假如應用程序能夠在 HttpSession 中存儲用戶(hù)狀態(tài),這種方法通常比其他方法更好。在客戶(hù)端用 HTTP cookie 或者隱藏表單字段存儲會(huì )話(huà)狀態(tài)有很大的安全風(fēng)險 —— 它將應用程序的一部分內部?jì)热荼┞督o了非受信任的客戶(hù)層。(一個(gè)早期的電子商務(wù)網(wǎng)站將購物車(chē)內容(包括價(jià)格)存儲在隱藏表單字段中,從而可以很輕易被非法利用,讓任何了解 Html 和 HTTP 的用戶(hù)可以以 0.01 美元購買(mǎi)任何商品。噢)此外,使用 cookie 或者隱藏表單字段很混亂,輕易出錯,并且脆弱(假如用戶(hù)禁止在瀏覽器中使用 cookie,那么基于 cookie 的方法就完全不能工作)。
在 J2EE 應用程序中存儲服務(wù)器端狀態(tài)的其他方法是使用有狀態(tài)會(huì )話(huà) bean,或者在數據庫中存儲會(huì )話(huà)狀態(tài)。雖然有狀態(tài)會(huì )話(huà) bean 在會(huì )話(huà)狀態(tài)治理方面有更大的靈活性,但是在可能的情況下,將會(huì )話(huà)狀態(tài)存儲在 Web 層仍然有好處。假如業(yè)務(wù)對象是無(wú)狀態(tài)的,那么通??梢?xún)H僅添加更多 Web 服務(wù)器來(lái)擴展應用程序,而不用添加更多 Web 服務(wù)器和更多 EJB 容器, 這樣的成本一般要低一些并且輕易完成。使用 HttpSession 存儲會(huì )話(huà)狀態(tài)的另一個(gè)好處是 Servlet API 提供了一種會(huì )話(huà)失效時(shí)通知的輕易方法。在數據庫中存儲會(huì )話(huà)狀態(tài)的成本可能難以承受。
servlet 規范沒(méi)有要求 servlet 容器進(jìn)行某種類(lèi)型的會(huì )話(huà)復制或者持久性,但是它建議將狀態(tài)復制作為 servlet 首要 存在理由(raison d'etre) 的重要部分,并且它對作為進(jìn)行會(huì )話(huà)復制的容器提出了一些要求。會(huì )話(huà)復制可以提供大量好處 —— 負載平衡、伸縮性、容錯和高可用性。相應地,大多數 servlet 容器支持某種形式的 HttpSession 復制,但是復制的機制、配置和時(shí)間是由實(shí)現決定的。 HttpSession API
簡(jiǎn)單地說(shuō),HttpSession 接口支持幾種方法,servlet、JSP 頁(yè)或者其他表示層組件可以用這些方法來(lái)跨多個(gè) HTTP 請求維護會(huì )話(huà)信息。會(huì )話(huà)綁定到特定的用戶(hù),但是在 Web 應用程序的所有 servlet 中共享 —— 不特定于某一個(gè) servlet。一種考慮會(huì )話(huà)的有用方法是,會(huì )話(huà)像一個(gè)在會(huì )話(huà)期間存儲對象的 Map —— 可以用 setAttribute 按名字存儲會(huì )話(huà)屬性,并用 getAttribute 提取它們。HttpSession 接口還包含會(huì )話(huà)生存周期方法,如 invalidate() (它通知容器應丟棄會(huì )話(huà))。清單 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();
...
}
理論上,可以跨群集一致性地完全復制會(huì )話(huà)狀態(tài),這樣群集中的所有節點(diǎn)都可以服務(wù)任何請求,一個(gè)簡(jiǎn)單的負載平衡器可以以輪詢(xún)方式傳送請求,避開(kāi)有故障的主機。不過(guò),這種緊密的復制有很高的性能成本,并且難于實(shí)現,當群集接近某一規模時(shí),還會(huì )有伸縮性的問(wèn)題。
一種更常用的方式是將負載平衡與會(huì )話(huà)相似性(affinity) 結合起來(lái) —— 負載平衡器可以將會(huì )話(huà)與連接相關(guān)聯(lián),并將會(huì )話(huà)中以后的請求發(fā)送給同一服務(wù)器。有很多硬件和軟件負載平衡器支持這個(gè)功能,并且這意味著(zhù)只有主連接主機和會(huì )話(huà)需要故障轉移到另一臺服務(wù)器時(shí)才訪(fǎng)問(wèn)復制的會(huì )話(huà)信息。
復制方式
復制提供了一些可能的好處,包括可用性、容錯和伸縮性。此外,有大量會(huì )話(huà)復制的方法可用:方法的選擇取決于應用程序群集的規模、復制的目標和 servlet 容器支持的復制設施。復制有性能成本,包括 CPU 周期(存儲在會(huì )話(huà)中的序列化對象)、網(wǎng)絡(luò )帶寬(廣播更新),以及基于磁盤(pán)的方案中寫(xiě)入到磁盤(pán)或者數據庫的成本。
幾乎所有 servlet 容器都通過(guò)存儲在 HttpSession 中的序列化對象進(jìn)行 HttpSession 復制,所以假如是創(chuàng )建一個(gè)分布式應用程序,應當確保只將可序列化對象放到會(huì )話(huà)中。(一些容器對像 EJB 引用、事務(wù)上下文、還有其他非可序列化的 J2EE 對象類(lèi)型有非凡的處理。)
基于 JDBC 的復制
一種會(huì )話(huà)復制的方法是序列化會(huì )話(huà)內容并將它寫(xiě)入數據庫。這種方法相當直觀(guān),其優(yōu)點(diǎn)是不僅會(huì )話(huà)可以故障轉移到其他主機,而且即使整個(gè)群集失效,會(huì )話(huà)數據也可以保存下來(lái)?;跀祿斓膹椭频娜秉c(diǎn)是性能成本 —— 數據庫事務(wù)是昂貴的。雖然它可以在 Web 層很好地伸縮,但是它可能在數據層產(chǎn)生伸縮問(wèn)題 —— 假如群集增長(cháng)大到一定程度,擴展數據層以容納會(huì )話(huà)數據會(huì )很困難或者成本無(wú)法接受。
基于文件的復制
基于文件的復制類(lèi)似于使用數據庫存儲序列化的會(huì )話(huà),只不過(guò)是使用共享文件服務(wù)器而不是數據庫來(lái)存儲會(huì )話(huà)數據。這種方式的成本一般比使用數據庫的成本(硬件成本、軟件許可證和計算開(kāi)銷(xiāo))低,其代價(jià)則是可靠性(數據庫可提供比文件系統更強的持久化保證)。
基于內存的復制
另一種復制方式是與群集中的一個(gè)或者多個(gè)其他服務(wù)器共享序列化的會(huì )話(huà)數據副本。復制所有會(huì )話(huà)到所有主機中提供了最大的可用性,并且負載平衡最輕易,但是因為復制消息所消耗的每個(gè)節點(diǎn)的內存和網(wǎng)絡(luò )帶寬,最終會(huì )限制群集的規模。一些應用服務(wù)器支持與“伙伴(buddy)”節點(diǎn)的基于內存的復制,其中每一個(gè)會(huì )話(huà)存在于主服務(wù)器上和一臺(或更多)備份服務(wù)器上。這種方案比將所有會(huì )話(huà)復制到所有服務(wù)器的伸縮性更好,但是當需要將會(huì )話(huà)故障轉移到另一臺服務(wù)器上時(shí)會(huì )使負載平衡任務(wù)復雜化,因為它必須找出另外哪一臺(幾臺)服務(wù)器有這個(gè)會(huì )話(huà)。
時(shí)間考慮
除了決定如何存儲復制會(huì )話(huà)數據,還有什么時(shí)候復制數據的問(wèn)題。最可靠但也最昂貴的方法是每次數據改變時(shí)復制它(如每次 servlet 調用結束)。不那么昂貴、但是在故障時(shí)會(huì )有丟失一些數據的風(fēng)險的方法是在每超過(guò) N 秒時(shí)復制數據。
與時(shí)間問(wèn)題有關(guān)的問(wèn)題是,是復制整個(gè)會(huì )話(huà)還是只試嘗復制會(huì )話(huà)中改變了的屬性(它包含的數據會(huì )少得多)。這些都需要在可靠性和性能之間進(jìn)行取舍。Servlet 開(kāi)發(fā)人員應當熟悉到在故障轉移時(shí),會(huì )話(huà)狀態(tài)可能變得“過(guò)時(shí)”(是幾次請求前的復制),并應當預備處理不是最新的會(huì )話(huà)內容。(例如,假如一個(gè)interview 的第 3 步產(chǎn)生一個(gè)會(huì )話(huà)屬性,而用戶(hù)在第 4 步時(shí),請求被故障轉移到一個(gè)具有兩次請求之前的會(huì )話(huà)狀態(tài)復制的系統上,那么第 4 步的 servlet 代碼應預備在會(huì )話(huà)中找不到這個(gè)屬性,并采取相應的行動(dòng) —— 如重定向,而不是認定它會(huì )在那里、并在找不到它時(shí)拋出一個(gè) NullPointerException。)
容器支持
Servlet 容器的 HttpSession 復制選項以及如何配置這些選項是各不相同的。IBM WebSphere? 提供的復制選項是最多的,它提供了在內存中復制或者基于數據庫的復制、在 servlet 末尾或者基于時(shí)間的復制時(shí)間、傳播全部會(huì )話(huà)快照(JBoss 3.2 或以后版本)或者只傳播改變了的屬性等選擇?;趦却娴膹椭苹?JMS 發(fā)布-訂閱,它可以復制到所有克隆、一個(gè)“伙伴”復制品或者一個(gè)專(zhuān)門(mén)的復制服務(wù)器。
WebLogic 還提供了一組選擇,包括內存中(使用一個(gè)伙伴復制品)、基于文件的或者基于數據庫的。JBoss 與 Tomcat 或者 Jetty servlet 容器一同使用時(shí),進(jìn)行基于內存的復制,可以選擇 servlet 末尾或者基于時(shí)間的復制時(shí)間,而快照選項(在 JBoss 3.2 或以后版本)是只復制改變了的屬性。Tomcat 5.0 為所有群集節點(diǎn)提供了基于內存的復制。此外,通過(guò)像 WADI 這樣的項目,可以用 servlet 過(guò)濾機制將會(huì )話(huà)復制添加到像 Tomcat 或者 Jetty 這樣的 servlet 容器中。
聯(lián)系客服