摘要:示範如何在 ASP.NET 應用程式中處理長時間執行的背景工作,以及如何設定 IIS 來讓網站自動啟動並持續運行。換言之,就是把 ASP.NET 網站當成 Windows Service 來用。
前言
以往要撰寫自動隨 Windows 啟動而自動在背景執行的應用程式,通常第一個想到的作法就是撰寫 Windows Services 應用程式。
有時候,Web 應用程式也需要執行一些比較花時間的背景工作,例如發送大量電子郵件。或者說,正因為這些工作需要花費較多時間,所以只能放在背景處理。如果處理這些背景工作的程式碼能直接寫在 ASP.NET 應用程式裡面,對於已經熟悉 ASP.NET 程式開發模型的人來說,不是很方便嗎?
這種作法,說穿了就是把 ASP.NET 應用程式當成 Windows Service 應用程式來用。
聽起來好像不太保險對吧?至少不是正規作法。
以前我也這麼覺得,直到一位同事在某個專案中使用,實際上線運行之後,發現其實挺穩定的,沒有出什麼亂子。後來我在另一個專案中如法炮製,運行幾個月了,也都相安無事。所以我想,應該可以把這篇筆記整理出來了。
環境需求
IIS 8 或之後的版本。IIS 7.5 也可以,只是部署的程序稍微麻煩些。(Windows Server 2008 R2 裡面的 IIS 是 7.5 版)
註:IIS 7.0 或之前的版本不支援應用程式自動初始化,故不適用。
範例
這裡先用一個簡單的範例來測試和觀察程式運行的狀況。假設的情境是我們需要在背景發送電子郵件。
首先建立一個 ASP.NET 應用程式專案,然後在專案中加入一個 class:SendMailTask。我打算由這個類別來負責發送電子郵件的背景工作。程式碼如下:
呼叫此類別的 Run 方法就會開始執行發送郵件的工作。其中有幾個地方需要特別說明:
接著,在 Global.asax 的 Application_Start 方法中建立 SendMailTask 物件,並呼叫其 Run 方法:
這個簡單的範例程式就完成了。
把這個應用程式部署到 IIS 中,然後觀察輸出的 log 檔案內容。結果 log.txt 這個檔案根本沒有產生。這是因為 ASP.NET 應用程式必須等到有第一個 HTTP 請求出現才會啟動,也就是說,我們的 Application_Start 事件根本未曾觸發過。
接著開啟瀏覽器,對此 ASP.NET 應用程式發出一個 HTTP 請求(例如瀏覽網站首頁),然後查看 log.txt 檔案。現在應該可以看到 log.txt 檔案了,內容像這樣:
每過一分鐘查看這個 log 檔案,都會多一筆新的 log 訊息。但只維持了 20 分鐘,之後就沒有新的 log 進來了。這表示我們的 SendMailTask 又停掉了。停止的原因是 ASP.NET 應用程式的 app pool 預設於閒置 20 分鐘後自動停止。
也就是說,要讓我們的 ASP.NET 應用程式中的背景工作能夠像 Windows Service 那樣自動啟動且持續運行,還需要 IIS 的幫助。
設定 IIS 8
IIS 8 提供了應用程式持續運行與預先載入的功能,只要幾個簡單的步驟就能設定好。首先必須先確定已經有安裝「應用程式初始化」模組,如下圖:
接著開啟 IIS 管理員,修改你的 ASP.NET 網站所屬的應用程式集區的進階設定,將「啟動模式」從預設的「OnDemand」改為「AlwaysRunning」:
光這樣不夠,還必須修改你的 ASP.NET 網站的進階設定,把「預先載入已啟用」(Preload Enabled)由預設的 False 改為 True。
這樣就行了。
眼見為憑,你可以重啟 IIS,觀察先前的範例是否有建立 log 檔案並輸出訊息。然後,把應用程式集區的閒置逾時時間由原本的 20 分鐘改為 5 分鐘,以便觀察 5 分鐘之後,log 檔案的內容是否仍然持續增加,來確認應用程式的背景工作依然活著。
設定 IIS 7.5
如果是 IIS 7.5 就稍微麻煩一點了,得額外下載兩個擴充模組來安裝才行。
首先,到微軟網站下載 Application Initialization Module for IIS 7.5。注意:此模組安裝完成後可能需要重新啟動。
由於此模組沒有包含視覺化元件,所以安裝完後,IIS 管理員裡面並沒有任何變化。這個視覺化模組可以從以下連結下載:
http://blogs.msdn.com/cfs-file.ashx/__key/communityserver-components-postattachments/00-10-38-83-23/ApplicationInitializationInstaller_5F00_x64.zip
此模組安裝好之後,IIS 管理員裡面就會出現一個新功能:Application Initialization,參考下圖。
雙擊 Application Initialization 圖示之後,會開啟一個對話窗,裡面有兩個頁籤:Application Pool 和 WebSites,分別用來設定應用程集區的 StartMode 以及網站的 Preload 選項。這個部分就不抓圖了。
小結
就我個人而言,碰到需要背景定期執行的需求時,Windows Service 總是最後的選擇,因為無論是開發、部署還是除錯,Windows Service 都比較麻煩。我寧願寫個 Console 程式,然後設定 Windows 排程來定期執行這個命令列程式。現在,從 IIS 7.5 之後,ASP.NET 開發人員又多了一個可以選擇的方案:把長時間執行的背景工作寫在 ASP.NET 程式裡。此作法不論開發、部署、還是除錯都很方便,但也有一些需要特別注意的地方:
本文的範例只是為了方便實驗,寫得比較簡陋,沒有處理上述第 3 個問題。如果你想找看看有沒有功能更強、考慮更周延的範例或函式庫,以下列出幾個可以參考的方案:
參考資料
前言
以往要撰寫自動隨 Windows 啟動而自動在背景執行的應用程式,通常第一個想到的作法就是撰寫 Windows Services 應用程式。
有時候,Web 應用程式也需要執行一些比較花時間的背景工作,例如發送大量電子郵件。或者說,正因為這些工作需要花費較多時間,所以只能放在背景處理。如果處理這些背景工作的程式碼能直接寫在 ASP.NET 應用程式裡面,對於已經熟悉 ASP.NET 程式開發模型的人來說,不是很方便嗎?
這種作法,說穿了就是把 ASP.NET 應用程式當成 Windows Service 應用程式來用。
聽起來好像不太保險對吧?至少不是正規作法。
以前我也這麼覺得,直到一位同事在某個專案中使用,實際上線運行之後,發現其實挺穩定的,沒有出什麼亂子。後來我在另一個專案中如法炮製,運行幾個月了,也都相安無事。所以我想,應該可以把這篇筆記整理出來了。
環境需求
IIS 8 或之後的版本。IIS 7.5 也可以,只是部署的程序稍微麻煩些。(Windows Server 2008 R2 裡面的 IIS 是 7.5 版)
註:IIS 7.0 或之前的版本不支援應用程式自動初始化,故不適用。
範例
這裡先用一個簡單的範例來測試和觀察程式運行的狀況。假設的情境是我們需要在背景發送電子郵件。
首先建立一個 ASP.NET 應用程式專案,然後在專案中加入一個 class:SendMailTask。我打算由這個類別來負責發送電子郵件的背景工作。程式碼如下:
public class SendMailTask { private bool _stopping = false; public SendMailTask() { // 從組態檔載入相關參數,例如 SmtpHost、SmtpPort、SenderEmail 等等. } public void Run() { var aThread = new Thread(TaskLoop); aThread.IsBackground = true; aThread.Priority = ThreadPriority.BelowNormal; // 避免此背景工作拖慢 ASP.NET 處理 HTTP 請求. aThread.Start(); } public void Stop() { _stopping = true; } private void Log(string msg) { System.IO.File.AppendAllText(@"C:\Temp\log.txt", msg + Environment.NewLine); } private void TaskLoop() { // 設定每一輪工作執行完畢之後要間隔幾分鐘再執行下一輪工作. const int LoopIntervalInMinutes = 1000 * 60 * 1; Log("TaskLoop on thread ID: " + Thread.CurrentThread.ManagedThreadId.ToString()); while (!_stopping) { try { DoSendMail(); } catch (Exception ex) { // 發生意外時只記在 log 裡,不拋出 exception,以確保迴圈持續執行. Log(ex.ToString()); } finally { // 每一輪工作完成後的延遲. System.Threading.Thread.Sleep(LoopIntervalInMinutes); } } } private void DoSendMail() { // 發送 email。這裡只固定輸出一筆文字訊息至 log 檔案,方便觀察測試結果。 string msg = String.Format("DoSendMail() at {0:yyyy/MM/dd HH:mm:ss}", DateTime.Now); Log(msg); } }
呼叫此類別的 Run 方法就會開始執行發送郵件的工作。其中有幾個地方需要特別說明:
- 發送郵件的背景工作是由一個單獨的執行緒來負責處理。由於我們需要一個長時間執行的專屬執行緒,所以建立執行緒時不透過 thread pool。
- 把背景執行緒的優先權設定成 BelowNormal,是為了避免影響 ASP.NET 處理 HTTP 請求的效能。
- 執行於背景執行緒的程式碼必須格外小心處理 exception。我們自行建立的執行緒當中如果有未處理的 exception,將會導致 ASP.NET 應用程式意外終止。
- 此範例中的 Stop 方法可供外界手動停止背景工作,但實際上不見得會用到。
- 在 TaskLoop 方法中,每隔一分鐘會呼叫一次 DoSendMail 方法。DoSendMail 方法實際上根本沒有發信,只是把文字訊息輸出至一個 log 檔,方便觀察運行過程。
接著,在 Global.asax 的 Application_Start 方法中建立 SendMailTask 物件,並呼叫其 Run 方法:
public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { var task = new SendMailTask(); task.Run(); } }
這個簡單的範例程式就完成了。
把這個應用程式部署到 IIS 中,然後觀察輸出的 log 檔案內容。結果 log.txt 這個檔案根本沒有產生。這是因為 ASP.NET 應用程式必須等到有第一個 HTTP 請求出現才會啟動,也就是說,我們的 Application_Start 事件根本未曾觸發過。
接著開啟瀏覽器,對此 ASP.NET 應用程式發出一個 HTTP 請求(例如瀏覽網站首頁),然後查看 log.txt 檔案。現在應該可以看到 log.txt 檔案了,內容像這樣:
TaskLoop on thread ID: 7 DoSendMail() at 2014/03/02 00:19:17 DoSendMail() at 2014/03/02 00:20:17 DoSendMail() at 2014/03/02 00:21:17 DoSendMail() at 2014/03/02 00:22:17 ...(略)
每過一分鐘查看這個 log 檔案,都會多一筆新的 log 訊息。但只維持了 20 分鐘,之後就沒有新的 log 進來了。這表示我們的 SendMailTask 又停掉了。停止的原因是 ASP.NET 應用程式的 app pool 預設於閒置 20 分鐘後自動停止。
也就是說,要讓我們的 ASP.NET 應用程式中的背景工作能夠像 Windows Service 那樣自動啟動且持續運行,還需要 IIS 的幫助。
設定 IIS 8
IIS 8 提供了應用程式持續運行與預先載入的功能,只要幾個簡單的步驟就能設定好。首先必須先確定已經有安裝「應用程式初始化」模組,如下圖:
接著開啟 IIS 管理員,修改你的 ASP.NET 網站所屬的應用程式集區的進階設定,將「啟動模式」從預設的「OnDemand」改為「AlwaysRunning」:
光這樣不夠,還必須修改你的 ASP.NET 網站的進階設定,把「預先載入已啟用」(Preload Enabled)由預設的 False 改為 True。
這樣就行了。
眼見為憑,你可以重啟 IIS,觀察先前的範例是否有建立 log 檔案並輸出訊息。然後,把應用程式集區的閒置逾時時間由原本的 20 分鐘改為 5 分鐘,以便觀察 5 分鐘之後,log 檔案的內容是否仍然持續增加,來確認應用程式的背景工作依然活著。
設定 IIS 7.5
如果是 IIS 7.5 就稍微麻煩一點了,得額外下載兩個擴充模組來安裝才行。
首先,到微軟網站下載 Application Initialization Module for IIS 7.5。注意:此模組安裝完成後可能需要重新啟動。
由於此模組沒有包含視覺化元件,所以安裝完後,IIS 管理員裡面並沒有任何變化。這個視覺化模組可以從以下連結下載:
http://blogs.msdn.com/cfs-file.ashx/__key/communityserver-components-postattachments/00-10-38-83-23/ApplicationInitializationInstaller_5F00_x64.zip
此模組安裝好之後,IIS 管理員裡面就會出現一個新功能:Application Initialization,參考下圖。
雙擊 Application Initialization 圖示之後,會開啟一個對話窗,裡面有兩個頁籤:Application Pool 和 WebSites,分別用來設定應用程集區的 StartMode 以及網站的 Preload 選項。這個部分就不抓圖了。
小結
就我個人而言,碰到需要背景定期執行的需求時,Windows Service 總是最後的選擇,因為無論是開發、部署還是除錯,Windows Service 都比較麻煩。我寧願寫個 Console 程式,然後設定 Windows 排程來定期執行這個命令列程式。現在,從 IIS 7.5 之後,ASP.NET 開發人員又多了一個可以選擇的方案:把長時間執行的背景工作寫在 ASP.NET 程式裡。此作法不論開發、部署、還是除錯都很方便,但也有一些需要特別注意的地方:
- 有別於 ASP.NET 用來處理 HTTP 請求的執行緒,我們自行建立的背景執行緒如果有未處理的 exception,將會導致整個 ASP.NET 應用程式異常終止(即使有在 Application_Error 事件中處理錯誤也一樣)。
- 通常不能部署在 Web Farm 環境,因為當同一個背景工作在不同機器上同時執行,很容易出問題,例如重複發送電子郵件。
- 有數種情況會令 ASP.NET 應用程式的 app domain 被釋放掉(例如 web.config 改動、app pool 自動回收等等),使得背景執行緒也被強迫終止,因而可能有資料遺失的問題。
本文的範例只是為了方便實驗,寫得比較簡陋,沒有處理上述第 3 個問題。如果你想找看看有沒有功能更強、考慮更周延的範例或函式庫,以下列出幾個可以參考的方案:
- ASP.Net Long-Running Interval Task by Chris Moschini(限制:只能建立一個背景工作)
- WebBackgrounder by Phil Haack(有考慮到上述三個問題)
- Quartz.NET (功能強大的工作排程系統)
參考資料
沒有留言: