2009年11月26日 星期四

簡單的問題最棘手:稀疏平常的ASP.NET Session Lost問題

1.3.1 問題描述

客戶抱怨,剛剛開發完成的大型ASP.NET站點測試階段一切正常,但是放到生產環境上,運行壓力一大,就會發生Session Lost現象。問題的表現是一個NullReferenceException異常。分析代碼後發現,該NullReferenceException是試圖訪問一個SessionObject時候發生的。該SessionObject應該在前面就已經設置過。問題半年來一共就發生過3次,而且是在3個不同的頁面發生。

1.3.2 制定策略

這個問題困難的地方在於重現的幾率很小,沒有多少詳細觀察的機會。所以,必須制訂非常周全的計畫,以便問題再次發生的時候,獲取足夠多的資訊。如何制訂周密的計畫呢?

思路非常直觀,瞭解Session實現的細節,總結出導致問題的所有可能性,獲取資訊的時候排查所有的可能性。

關於ASP.NET Session的細節,可以參考:

Underpinnings of the Session State Implementation in ASP.NET

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspp/html/ASPNetSessionState.asp

有了對Session的理解後,把這個問題分成了下面幾種情況:

1. 客戶使用了負載均衡的環境,但是沒有正確配置基於資料庫或者服務的Session模式。或者是幾台伺服器的Machine key沒有配置成一致。跟客戶確認後排除了這種情況,因為客戶只有一個伺服器,使用的In-Proc。

2. 最簡單的情況就是所有的用戶,所有的session都丟了。這種情況一般發生在In-Proc的session mode上。原因就是appdomain重啟或者IIS進程崩潰。可以通過性能日誌或系統日誌來排查。

3. 稍微麻煩一點的就是某一個用戶的session丟了,而沒有影響到所有用戶。觀察方法是,首先在session start裏面做log, 把每一個session的創建時間以及session id都記錄到本地的一個log裏面,同時往session裏面添加一個測試用的session value。問題發生的時候,在ASP.NET的全局錯誤處理函數中當前的session id讀出來,比較log中的記錄看看這個session是不是剛剛建立的。如果是,很有可能是用戶端的原因導致session id丟了,比如IE crash導致cookie丟失。

4. 如果不是,那就看看測試用的session value是不是丟了。如果這個也丟了,應該是代碼中掉了Session.Clear。

5. 如果測試用的session value沒有丟,情況就變成一個用戶的session裏面的一部分value丟了。很可能是由於用戶的代碼邏輯導致的。解決方法就是通過更詳細的log來定位問題,然後閱讀代碼來檢查。

可以看到,問題的特徵跟潛在的根源是對應的。目的在於區分出這3種情況:

1. 所有用戶的所有Session全沒有了。

2. 一個用戶的Session沒有了。

3. 一個用戶的部分Session沒有了。

針對每種情況,採取的log策略是:

對於第1類情況,可以在Application_Start/End函數中記錄下時間來檢查Appdomain是不是重新啟動過。

對於第2類情況,log檔應該記錄下session id和session創建的時間。以便判斷問題是否是cookie id lost導致的。如果是cookie id lost,那問題就出在用戶端,或者是網路原因。

對於第3類情況,可以在工程中搜索所有Session Clear的調用,每次調用前寫log檔來記錄。如果工程很大,無法逐一添加,可以載入調試器,在Session Clear函數中設定條件中斷點來記錄。

1.3.3 具體操作和結論

總結下來,具體的實現是:

1. 在global.asax檔的session_start中把這個session的創建時間記錄到session裏面。這個創建時間也同時充當測試用的session value。

2. 代碼中對session操作的地方,寫log到以sessionid為檔案名的檔中去。

3. 用log檔記錄每次session的操作,發生在什麼函數,發生的時間,session內容的變化。

4. 當Exception發生的時候,在Exception handler中記錄發生問題的session id和殘留下來的Session value。

這樣,問題發生的時候,根據Exception handler記錄的session id找到log檔,就可以很清楚地得到所需要的資訊。

在做了上述部署之後,等了大約一個星期問題重現了。在log檔中,發現這樣的資訊:

1. 某一個用戶的部分Session丟失。

2. 從Session創建時間看,該Session已經維持很長時間了。

3. 通過檢查Session Clear的調用紀錄,發現丟失的Session的確是由用戶自己的代碼清除的。同時發現這些代碼的運行次序跟設計不吻合。根據設計初衷,在清除Session後,頁面會重定向到一個專門的頁面並重新添加Session,然後繼續操作。但是log表明這個專門的頁面並沒有得到執行。

檢查用戶的重定向代碼後發現,重定向是通過用戶端JavaScript來實現的。用JavaScript來維護事務邏輯,犯了web開發的大忌。因為JavaScript的行為對用戶端流覽器的依賴非常大。重定向最好用http 302來實現(Response.redirect就是這樣實現的),同時需要在伺服器端添加檢測代碼來確保業務邏輯的正確順序:

HTTP Status code definition

http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

在做了上面的改動後,問題得到了解決。Session lost不是一個很有趣的例子,但它卻非常具有代表性。

問題現象簡單而且明確,但是很難重現。問題的原因是在早期積累下來的,如果只觀察問題暴露出來後的現象,為時已晚。對付這類問題,關鍵在於根據問題的特點,在適當的地方主動抓取資訊。

1.3.4 題外話和相關討論

排查session lost的經驗

除了上面的步驟外,遇上session lost還應該注意。

1. 如果用戶開兩個IE進程(注意,不是用Ctrl+N開兩個IE視窗) 訪問同一個url, 這兩個進程分別對應兩個獨立的session。

2. 最容易忽視的情況是session timeout。可以觀察Session id是否變化,或者在session end裏面加log進行排查。

沒有留言:

張貼留言