構建 J2EE 或 J2SE 伺服器應用,多半會使用 Java Servlet——直接透過 JSP、Velocity、WebMacro 等表現層,或透過 Axis、Glue 等基於 Servlet 的 Web 服務實作。Servlet API 最重要功能之一是 Session 管理——透過 HttpSession 介面進行使用者狀態認證、失效與維護。
幾乎每個 Web 應用都有 Session 狀態:從簡單的「記住是否登入」到詳細的會話歷史(購物車內容、查詢結果快取、20 頁動態問卷完整回應歷史)。HTTP 協定無狀態,故需將 Session 狀態儲存並與瀏覽會話關聯,下次請求時易於取得。J2EE 提供多種管理方式:
HttpSession 介面不當的 Session 管理會帶來嚴重效能問題。
應用程式能將使用者狀態儲存於 HttpSession 時,通常優於其他方式。客戶端 Cookie/隱藏欄位有重大安全風險——暴露應用內部內容給非受信任客戶端(早期電商網站將購物車含價格存隱藏欄位,用戶輕易竄改以 $0.01 購買商品)。此外 Cookie/隱藏欄位混亂、易錯、脆弱(用戶禁用 Cookie 導致完全失效)。
J2EE 其他伺服器端儲存方式:有狀態 Session Bean、資料庫儲存 Session 狀態。雖有狀態 Session Bean 靈活度較高,但能將 Session 狀態儲存於 Web 層仍有優勢:業務物件無狀態時,僅增加 Web 伺服器即可擴展,成本較低且容易(增加 Web 伺服器+EJB 容器成本更高)。HttpSession 另一優勢:Servlet API 提供 Session 失效通知的簡易方法。資料庫儲存 Session 狀態成本可能難以負擔。
Servlet 規範未要求容器進行特定類型 Session 複寫或持久化,但建議將狀態複寫作為 Servlet 存在理由的重要部分,並對複寫容器提出要求。Session 複寫提供負載平衡、擴展性、容錯、高可用性等大量效益。相應地,大多數 Servlet 容器支援某種 HttpSession 複寫,但機制、配置、時機由實作決定。
簡單說,HttpSession 支援多種方法,Servlet、JSP 頁或其他表現層組件可跨多個 HTTP 請求維護會話資訊。Session 綁定特定用戶,但在 Web 應用所有 Servlet 間共享——非特定於某 Servlet。可視為會話期間儲存物件的 Map:setAttribute 按名稱儲存屬性、getAttribute 取出。包含生命週期方法如 invalidate()(通知容器捨棄 Session)。常用元素如清單 1:
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();
...
}
理論上可跨叢集一致性完整複寫 Session 狀態,所有節點服務任意請求,簡單負載平衡器輪詢分發請求並避開故障主機。但緊密複寫效能成本高、難實作、叢集達一定規模有擴展性問題。
更常用方式:負載平衡結合 Session 親和性——負載平衡器將 Session 與連線關聯,後續請求發送給同一伺服器。許多硬體/軟體負載平衡器支援此功能,意味著只有主連線主機故障需故障轉移時才存取複寫 Session 資訊。
複寫提供可用性、容錯、擴展性等效益。大量複寫方法可用:選擇取決於應用叢集規模、複寫目標、Servlet 容器支援設施。複寫有效能成本:CPU 週期(序列化 Session 物件)、網路頻寬(廣播更新)、磁碟方案寫入磁碟/資料庫成本。
幾乎所有 Servlet 容器透過儲存於 HttpSession 的可序列化物件進行複寫,故建構分散式應用時,確保只將可序列化物件放入 Session(部分容器對 EJB 參考、交易上下文等非可序列化 J2EE 物件有特殊處理)。
序列化 Session 內容寫入資料庫。優點:Session 可故障轉移至其他主機,整個叢集失效時 Session 資料仍保留。缺點:效能成本高——資料庫交易昂貴。Web 層擴展良好,但資料層可能產生擴展問題——叢集增長到一定程度,擴展資料層容納 Session 資料困難或成本不可接受。
類似資料庫儲存序列化 Session,改用共享檔案伺服器而非資料庫。成本通常低於資料庫(硬體、授權、計算開銷),代價是可靠性(資料庫提供比檔案系統更強持久化保證)。
與叢集中一或多其他伺服器共享序列化 Session 資料副本。全複寫到所有主機提供最大可用性、負載平衡最容易,但每節點記憶體與網路頻寬消耗限制叢集規模。部分應用伺服器支援「夥伴節點」基於記憶體複寫:每個 Session 存在主伺服器及一台(或多)備份伺服器。此方案比全複寫擴展性佳,但故障轉移時負載平衡任務複雜化——必須找出還有哪台伺服器持有該 Session。
除決定如何儲存複寫資料,還有何時複寫的問題。最可靠但最昂貴:每次資料改變複寫(如每次 Servlet 調用結束)。較不昂貴但故障時有遺失資料風險:每超過 N 秒複寫一次。相關問題:複寫整個 Session 還是僅複寫改變屬性(資料量少得多)?皆在可靠性與效能間權衡。Servlet 開發者應認知故障轉移時 Session 狀態可能「過時」(幾次請求前的複寫),準備處理非最新 Session 內容(如面試第 3 步產生屬性,用戶第 4 步故障轉移至兩次請求前狀態,第 4 步 Servlet 應預期找不到屬性並採取相應行動——如重導向,而非假定存在、找不到拋 NullPointerException)。
Servlet 容器 HttpSession 複寫選項與配置各異。IBM WebSphere 提供最多選項:記憶體/資料庫複寫、Servlet 結束/基於時間複寫、傳播完整 Session 快照(JBoss 3.2+)或僅改變屬性。記憶體複寫基於 JMS 發布-訂閱,可複寫到所有克隆、單一「夥伴」複寫或專用複寫伺服器。WebLogic 也提供選擇:記憶體(單一夥伴複寫)、基於檔案、基於資料庫。JBoss 搭配 Tomcat/Jetty 進行記憶體複寫,可選 Servlet 結束/基於時間複寫,快照選項(JBoss 3.2+)僅複寫改變屬性。Tomcat 5.0 為所有叢集節點提供記憶體複寫。此外,透過 WADI 等專案,可用 Servlet 過濾機制為 Tomcat/Jetty 等容器添加 Session 複寫。
無論採用何種 Session 複寫機制,有幾種方式改善 Web 應用效能與擴展性。首先記住:為獲 Session 複寫效益,需在部署描述符中將 Web 應用標記為 distributable,並確保 Session 中所有內容可序列化。
複寫 Session 成本隨 Session 物件圖增大而增加,故應盡可能少放資料。減少序列化開銷、網路頻寬需求、磁碟需求。特別是:將共享物件存 Session 通常非好主意,因需複寫到每個所屬 Session。
改變 Session 屬性時,即使容器嘗試最小更新(僅傳播改變屬性),若未調用 setAttribute,容器可能未察覺屬性已改變。例如 Session 中有 Vector 表示購物車商品,調用 getAttribute() 取得 Vector、添加內容、不再調用 setAttribute,容器可能不知 Vector 已改變。
支援最小更新的容器,將多個細粒度物件而非單一大物件放入 Session,可降低複寫成本。快速改變資料的變更不會迫使容器序列化傳播緩慢改變資料。
知用戶完成 Session 使用(如選擇登出),確保調用 HttpSession.invalidate()。否則 Session 持久化至失效,消耗記憶體且可能持久很久(視 Session 逾時)。許多容器限制跨所有 Session 可用記憶體,達限制時序列化最早使用的 Session 寫入磁碟。知用戶用畢 Session,可讓容器停止處理並使其失效。
Session 中有大物件且僅部分使用時,不再需要應刪除。刪除可減少複寫成本(類似顯式 null 協助垃圾收集,筆者通常不建議,但因複寫存在,Session 中保留垃圾成本高得多,值得以此方式協助容器)。
透過 HttpSession 複寫,Servlet 容器可在構建複寫、高可用性 Web 應用方面減輕大量負擔。但複寫有配置選項,各容器不同,策略選擇影響容錯、效能、擴展性。複寫策略選擇不應為事後之舉——構建 Web 應用時即應考慮。且切勿忘記負載測試以確定應用擴展性——在客戶替你做之前。