原文:Running async tasks on app startup in ASP.NET Core (Part 3)
作者:Andrew Lock
譯者:Lamond Lu
之前我寫了兩篇有關在ASP.NET Core中執行非同步任務的博文,本篇博文是對之前兩篇博文中演示示例和實現方法的簡短跟進。
你可以透過以下連結檢視之前的博文。
啟動任務的例子
在之前部落格中,我收到的最常見的反饋是關於我在描述問題時使用的例子。在我最初的部落格中,我列舉了3種可能場景,在這3種場景中,你希望在ASP.NET Core應用啟動時執行一些非同步任務。
-
檢查強型別配置是否合法
-
使用資料庫或者API填充快取
-
執行資料庫遷移
對於前兩種場景,沒有任何問題,但是對於資料庫遷移,一些博友提出了一些疑問。其實在兩篇博文中我一直都反覆說明,資料庫遷移作為啟動任務不是一個很好的方案,這裡我只是想用它作為一個說明如何在ASP.NET Core程式啟動時執行非同步任務的例子。現在來看,當時使用這個例子是非常失敗的。
資料庫遷移是一個糟糕的選擇
那麼為什麼在ASP.NET Core應用啟動時,執行資料庫遷移任務會是一個問題呢?畢竟,在應用程式開始處理請求之前,你肯定要完成資料庫遷移!
其實這裡其實有3個問題:
-
資料庫遷移是應該是單執行緒的
-
遷移資料庫需要更多的許可權
-
開發人員不太喜歡直接執行資料庫遷移
下麵我們依次說明一下。
資料庫遷移應該是單執行緒的
擴充套件一個Web應用最常用的方式之一是進行橫向擴充套件,啟動多個執行實體並使用負載均衡分發請求
這種Web叢集擴充套件的方案是非常有效的,特別是噹噹前應用是無狀態的(請求被分發到各個應用程式中,如果一個應用程式崩潰,其他的Web應用實體依然可以處理請求)。
但是不幸的是,如果嘗試將資料庫遷移作為應用啟動任務,你很可能就會遇到問題。如果有超過1個以上的實體同時啟動,多個資料庫遷移任務將同時執行。
雖然並不能保證你一定會遇到麻煩,但除非你非常小心地確保冪等更新和錯誤處理,否則你很可能會陷入困境。
你肯定不希望使用這種方法,因為它可能產生的資料庫完整性問題。 這裡一個更好的選擇是先啟動單個實體完成遷移操作。 這樣資料庫遷移任務變成一個單執行緒任務,自然也就避開最嚴重的危險。
這種方法比將資料庫遷移作為啟動任務執行更有意義,但它更安全,更容易實現。
當然,這不是唯一的選擇。 如果你對啟動任務遷移的想法有所瞭解,那麼你可以使用分散式鎖來確保只有一個應用程式實體來執行遷移指令碼。 然而,這並沒有解決第二個問題……
遷移通常需要更多的許可權
通常來說,最佳實踐是你必須限制你的應用程式,以便他們只有權訪問和修改所需的資源。 如果報表應用只需要讀取銷售資料,那麼它應該無法修改它們,或者更改資料庫表結構! 為指定的連線字串配置可操作的許可權,可以防止在的的應用出現安全問題時產生大量影響。
如果你正在使用Web應用程式本身來執行資料庫遷移,那麼該Web應用程式自然需要資料庫許可權才能執行高風險活動,例如修改資料庫表結構,更改許可權或更新/刪除資料。 你真的希望您的Web應用程式能夠刪除你的學生表嗎?
同樣,你可以使用一些特定的實現,例如,與正常的資料庫訪問相比,使用不同的連線字串進行遷移。 但是,如果使用外部遷移過程,你根本無法鎖定Web應用程式行程。
開發人員不習慣直接執行EF Core遷移
這是一個不那麼明顯的觀點,但是很多人表示在生產環境中使用EF Core遷移工具可能不是一個好主意。
就個人而言,我已經有1年多沒有在生產環境中使用EF Core遷移了,到目前為止遷移工具肯定已經有所改善。 話雖如此,我仍然看到一些問題:
-
使用EF Core全域性工具進行遷移需要安裝.NET Core SDK,這在生產伺服器上是不需要的。
-
如果你想安全地更新資料庫,你可能還是必須對生成的指令碼進行一些編輯。 遷移後的資料庫結構應與現有(執行)應用程式相容,以避免停機。
-
微軟官方檔案中暗示在應用啟動時執行EF Core遷移不是一個好主意!
就我自己而言,我經常使用DbUp和FluentMigrator,而不會使用EF Core遷移。我發現它們都執行良好。
因此,如果資料庫遷移任務不適合應用啟動任務示例,那麼哪些任務才是比較適合的呢?
比較適合作為啟動任務的一些例子
雖然在之前的博文中我已經反覆提到了一些例子,但我還是將在下麵再次描述它們。這裡其他博友還給我一些有趣的補充。
-
檢查強型別配置是否有效。ASP.NET Core 2.2引入了配置驗證,但它只在首次訪問
IOptions
類時執行此操作。 正如我在之前文章中所描述的那樣,你可能希望在應用啟動時進行驗證,以確保你的環境和配置有效。 -
填充快取。 你的應用程式可能需要來自檔案系統或遠端服務的資料,它只需要載入一次,但載入需要耗費相當多的資源,所以在應用程式啟動之前載入此資料可減少請求延遲。
-
預連線到資料庫和/或外部服務。 以類似的方式,你可以透過連線到資料庫或其他外部服務來填充資料庫連線池。 這些通常是相對昂貴的操作,因此是很好的用例。
-
預編譯載入應用中所有的單例服務。我認為這個一個非常有趣的想法,透過預載入依賴註入容器中註冊的單例服務,你可以減少請求的響應時間。
使用健康檢查來完成啟動任務
我完全同意有關資料庫遷移的反饋。 當這不是一個好主意時,將它們用作啟動任務的示例有點誤導,特別是因為我個人不使用我所描述的方法!
然而,很多人都同意我所描述的另外一種方法 – 在啟動Kestrel伺服器和處理請求之前執行啟動任務。
這裡Damian Hickey針對這個方案提出了一個問題,他建議儘快啟動Kestrel伺服器。 他建議在所有啟動任務完成後,使用執行健康檢查向負載均衡器發出訊號,表明應用程式已準備好開始接收請求。 與此同時,所有非健康檢查流量(如果負載均衡器正在執行此任務,則不應該有任何流量)將收到503服務不可用。
這種方法的主要好處是它可以避免網路超時。 一般來說,應用程式最好能儘快的針對請求傳回錯誤程式碼,而不是根本不響應請求,並導致客戶端超時。 這減少了客戶端所需的資源數量。 透過較早啟動Kestrel伺服器,應用程式可以更早地開始響應請求,即使響應是“未就緒”響應。
這實際上與我在第一篇文章中描述的方法非常相似,但是我沒有選用它了,因為它太複雜了,而且沒有達到我設定的標的。 從技術上來說,在那篇文章中,我只是關註在Kestrel伺服器啟動之前執行任務的方法,健康檢查方法不能完成這個功能。
然而,Damian提出的問題讓我再次思考。 在我下一篇部落格中,我將描述如何使用ASP.NET Core 2.2的健康檢查功能向負載均衡器傳送訊號,表明應用程式已經完成了所有啟動任務。
總結
在這篇文章中,我分享了我之前關於在ASP.NET Core應用程式啟動時執行非同步任務的一些反饋。 這裡最大的問題是我選擇使用EF Core資料庫遷移作為啟動任務的示例。 資料庫遷移不適合在應用程式啟動時執行,因為它們通常需要由單個行程執行,並且需要比更多的資料庫許可權。
我提供了一些比較適合作為的啟動任務的場景,並且描述了Damian給出的建議 – 儘快啟動Kestrel伺服器,並使用執行狀況檢查來指示任務何時完成。 我將在下一篇文章中描述如何實現這一功能。