最近,在使用EventStore的.NET Client API採用大量線程池線程同步寫入Event時(用於模擬ASP.NET服務端大併發寫入Event的狀況),發現EventStore的鏈接會隨機中斷,而且在服務端日誌中顯示客戶端鏈接Heartbeat超時(若是不瞭解EventStore,請點擊傳送門)。因爲系統中全局共享一個EventStore鏈接,當鏈接中斷時,會致使全部的寫入操做被Block,而EventStore鏈接的重連速度比較慢(測試機器上重連須要耗費20秒到1分鐘),這樣會致使比較嚴重的性能問題、甚至致使客戶端請求超時。服務器
示例代碼以下:併發
for( int index=0;index<100;index++) { Task.Run( ()=> { connection. AppendToStreamAsync(...).Wait(); } ); }
也許有人會問,爲何不爲每個請求單獨創建一個EventStore鏈接?好吧,繞開官方文檔推薦使用單鏈接這個問題不談,彷佛爲每個請求創建一個鏈接是一個好的解決方案,可是實際測試發現這個方案比單連接方案更不靠譜。通過測試發現(使用EventStore版本3.0.5.0測試),爲每個請求單獨創建鏈接會有如下幾個方面的問題:異步
1)每次請求都須要從新鏈接EventStore Server,耗費的時間比較長,性能比單鏈接差。性能
2)重複的創建和斷開鏈接會致使EventStore服務端的TCP端口失去響應(大概30K次鏈接以後),在這種狀況下除非重啓EventStore Server,不然客戶端再也沒法鏈接上EventStore Server。測試
3)若是一個鏈接非正常中斷,則全部的鏈接都會斷開重連。線程
因爲多鏈接存在上面的問題(中途也嘗試過鏈接池,發現也一樣存在上述問題),咱們只能在單鏈接這個方向上繼續前行,我嘗試着將上面的代碼修改成以下形式,居然發現問題神奇的消失了。調試
for( int index=0;index<100;index++) { var tempThread = new Thread(()=>{connection. AppendToStreamAsync(...).Wait();}); tempThread.IsBackground = true; tempThread.Start(); }
這兩段代碼有什麼區別呢?有問題的代碼是使用線程池的線程來同步寫入Event,而沒有問題的代碼則使用線程同步寫入Event,貌似沒有什麼區別。會是什麼問題呢?我又回過頭將第一段代碼修改成以下形式(將同步寫入修改成異步寫入,注意:將Wait方法調用去掉),居然也發現沒有問題。日誌
for( int index=0;index<100;index++) { Task.Run( ()=> { connection. AppendToStreamAsync(...); } ); }
那麼問題出在什麼地方呢?會不會是EventStore自己的問題呢?因爲使用HTTP接口來寫入Event很是的穩定,能夠基本排除是Event Store Server的問題,那麼EentStore .Net Client API有問題的可能性就比較大了。orm
沒有辦法,從GIT上下載EventStore的源代碼開始調試,最終發現了問題多是下面的代碼引發的:blog
public void EnqueueMessage(Message message) { Ensure.NotNull(message, "message"); _messageQueue.Enqueue(message); if (Interlocked.CompareExchange(ref _isProcessing, 1, 0) == 0) ThreadPool.QueueUserWorkItem(ProcessQueue); } private void ProcessQueue(object state) { do { Message message; while (_messageQueue.TryDequeue(out message)) { Action<Message> handler; if (!_handlers.TryGetValue(message.GetType(), out handler)) throw new Exception(string.Format("No handler registered for message {0}", message.GetType().Name)); handler(message); } Interlocked.Exchange(ref _isProcessing, 0); } while (_messageQueue.Count > 0 && Interlocked.CompareExchange(ref _isProcessing, 1, 0) == 0); }
這段代碼的目的是將客戶端寫入的自定義事件或者系統產生的事件(不管客戶端寫入的自定義事件仍是系統事件都會被存儲到一個惟一的Queue中),順序的發送到服務端。說到這裏就必須提一下EventStore TCP鏈接的心跳機制,當客戶端與EventStore服務端創建鏈接以後,客戶端會按期發送一個Heartbeat事件到服務端,通知服務端客戶端還活着,若是在必定的時間內,服務端收不到來自客戶端的Heartbeat事件,那麼服務端會主動關閉鏈接,而且在日誌中記錄一條客戶端Heartbeat超時日誌。
那麼這段代碼與鏈接中斷有什麼關係呢?注意,客戶端的Heartbeat事件也是經過這段代碼發送到服務端的。若是,我說若是,因爲什麼緣由致使Heartbeat事件不能及時的發送到服務端,會不會致使鏈接中斷呢?答案是確定的。
那麼這段怎麼看都沒有問題的代碼爲何在使用線程池線程併發寫入的狀況下會致使Heartbeat事件發送不及時呢?是否是這句話有問題?難道在大併發的狀況下QueueUserWorkItem會致使ProcessQueue的調用會被延遲?
ThreadPool.QueueUserWorkItem(ProcessQueue);
爲了驗證個人想法,我建立了一個System.Threading.Thread.Timer,定時100毫秒,在Callback中輸出兩次Callback之間的時間差,當大量的使用ThreadPool的線程時,發現兩次Callback之間的時間差慢慢的從200毫秒,越變越大,直到到好幾秒。問題到這裏就已經很清楚了,當ThreadPool的線程被大量佔用時,經過QueueUserWorkItem註冊的回調方法會有必定的延遲,具體的延遲時間與ThreadPool的線程被佔用的時間和數量有關係,對於實時性要求比較高的任務,好比EventStore的ProcessQueue來講,是不適合使用QueueUserWorkItem來註冊回調的。
最後,我將EventStore代碼中的QueueWorkItem調用替換爲線程調用,發現問題解決。
後續,爲了說明這個問題,我在EventStore的Group中發佈了一個帖子說明這個問題,EventStore的做者認爲Task.Run不能真實的模擬ASP.NET的狀況,建議我到真實環境中測試。爲此我建立了一個簡單的ASP.NET MVC項目和一個簡單的客戶端,模擬大壓力下兩種實現的差異。經過測試發現,使用原版的API,隨着併發線程數量的增加,Event的寫入速度愈來愈慢,而使用修改後的API,則發現隨着併發線程數量的增加,Event的寫入速度變化不明顯,基本上沒有太大的差異,理論上來講在某一個併發下應該會致使Heartbeat超時。因爲IIS默認併發訪問數量的限制,又懶得去調整服務器,而且因爲IIS自己又管理了一套鏈接池,想壓出Heartbeat超時比較困難,就沒有作Heartbeat超時的壓力測試。