2014-02-14 124 views
18

我們最近開發了基於SOA的網站,但這個網站最後不得不可怕的負載和性能問題時,它負載下去。我張貼有關這個問題在這裏一個問題:異步並等待:它們不好嗎?

ASP.NET website becomes unresponsive under load

該網站由被4個節點的集羣,這是另一個4-託管的網站上託管的API(WEB API)網站節點集羣並調用API。兩者都是使用ASP.NET MVC 5開發的,所有操作/方法都基於異步等待方法。

在一些監控工具(如NewRelic)下運行該站點後,調查幾個轉儲文件並分析工作進程,結果發現在非常輕的負載下(例如16個併發用戶),我們最終有大約900個線程利用100%的CPU並填滿IIS線程隊列!

即使我們設法通過引入大量緩存和性能修正功能將站點部署到生產環境中我們團隊中的許多開發人員都認爲我們必須刪除所有異步方法並將API和網站轉換爲普通的Web API以及只返回Action結果的Action方法。

我個人不是很滿意的方法,因爲我的直覺是,我們還沒有使用的異步方法正確,否則就意味着,微軟已經推出了一個功能,基本上是相當的破壞性,無法使用!

你知道,將其清除出是在哪裏以及如何異步方法應該/可以使用任何參考?我們應該如何使用它們來避免這樣的戲劇?例如根據我在MSDN上閱讀的內容,我認爲API層應該是異步的,但網站可能是一個正常的非異步ASP.NET MVC網站。

更新:

這裏是異步方法,使所有與API通信。

public static async Task<T> GetApiResponse<T>(object parameters, string action, CancellationToken ctk) 
{ 
     using (var httpClient = new HttpClient()) 
     { 
      httpClient.BaseAddress = new Uri(BaseApiAddress); 

      var formatter = new JsonMediaTypeFormatter(); 

      return 
       await 
        httpClient.PostAsJsonAsync(action, parameters, ctk) 
         .ContinueWith(x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }).Result, ctk); 
     } 
    } 

這種方法有什麼愚蠢的東西嗎?請注意,當我們將所有方法轉換爲非異步方法時,我們獲得了更好的性能。

下面是一個示例用法(我剪掉了與驗證,日誌記錄等有關的代碼的其他部分。此代碼是MVC操作方法的主體)。

在我們服務的包裝:

public async static Task<IList<DownloadType>> GetSupportedContentTypes() 
{ 
    string userAgent = Request.UserAgent; 
    var parameters = new { Util.AppKey, Util.StoreId, QueryParameters = new { UserAgent = userAgent } }; 
    var taskResponse = await Util.GetApiResponse<ApiResponse<SearchResponse<ProductItem>>>(
        parameters, 
        "api/Content/ContentTypeSummary", 
        default(CancellationToken)); 
        return task.Data.Groups.Select(x => x.DownloadType()).ToList(); 
} 

而且在行動:

public async Task<ActionResult> DownloadTypes() 
    { 
     IList<DownloadType> supportedTypes = await ContentService.GetSupportedContentTypes(); 
+2

有關應用程序如何使用async/await的任何示例?當與IO綁定操作(如數據庫調用和文件操作)一起使用時,通常應該提高可伸縮性,而不是降低它。如果它產生了不必要的線程,那麼每個方法都可以標記爲異步,這可能是一個紅旗,可能是你的問題的原因。 –

+1

也許,你的代碼使用'Task.Run'封裝同步API,而不是使用自然的異步API? http://stackoverflow.com/q/21690385/1768303。通常,在服務器上使用'Task.Run'是個不錯的主意。 – Noseratio

+0

是MVC 5嗎?或者你的標籤說MVC 3? –

回答

34

這種方法有什麼愚蠢的東西嗎?請注意,當我們將 所有方法轉換爲非異步方法時,我們獲得了更好的性能。

我至少可以看到這兩者是錯在這裏:

public static async Task<T> GetApiResponse<T>(object parameters, string action, CancellationToken ctk) 
{ 
     using (var httpClient = new HttpClient()) 
     { 
      httpClient.BaseAddress = new Uri(BaseApiAddress); 

      var formatter = new JsonMediaTypeFormatter(); 

      return 
       await 
        httpClient.PostAsJsonAsync(action, parameters, ctk) 
         .ContinueWith(x => x.Result.Content 
          .ReadAsAsync<T>(new[] { formatter }).Result, ctk); 
     } 
    } 

首先,你傳遞給ContinueWith拉姆達阻止:

x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }).Result 

這等價於:

x => { 
    var task = x.Result.Content.ReadAsAsync<T>(new[] { formatter }); 
    task.Wait(); 
    return task.Result; 
}; 

因此,您正在阻塞執行lambda的池線程。這有效地消除了自然異步API的優勢,並降低了您的Web應用程序的可伸縮性。在你的代碼中注意這樣的其他地方。

其次,ASP.NET請求由安裝了特殊同步上下文的服務器線程處理,AspNetSynchronizationContext。如果繼續使用await,則繼續回調將發佈到同一個同步上下文,編譯器生成的代碼將處理此問題。 OTOH,當你使用ContinueWith時,這不會自動發生。

因此,你需要明確地提供正確的任務調度,去除阻塞.Result(這將返回一個任務)和Unwrap嵌套任務:

return 
    await 
     httpClient.PostAsJsonAsync(action, parameters, ctk).ContinueWith(
      x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }), 
      ctk, 
      TaskContinuationOptions.None, 
      TaskScheduler.FromCurrentSynchronizationContext()).Unwrap(); 

這就是說,你真的不需要這樣的加入ContinueWith複雜性在這裏:

var x = await httpClient.PostAsJsonAsync(action, parameters, ctk); 
return await x.Content.ReadAsAsync<T>(new[] { formatter }); 

由Stephen Toub下面的文章是高度相關:

"Async Performance: Understanding the Costs of Async and Await"

如果我要調用一個異步方法在同步方面,其中使用的await 是不可能的,什麼是做的最好的方法是什麼?

你幾乎永遠不應該需要混合awaitContinueWith,你應該堅持await。基本上,如果你使用async,它必須是異步"all the way"

對於服務器側ASP.NET MVC /網絡API的執行環境,它只是意味着控制器方法應該是async並返回一個或TaskTask<>,檢查this。 ASP.NET跟蹤給定HTTP請求的未完成任務。在所有任務完成之前,請求沒有完成。

如果你真的需要調用從ASP.NET同步方法的async方法,你可以使用AsyncManagerthis註冊一個未決任務。對於傳統的ASP.NET,您可以使用PageAsyncTask

在最壞的情況下,您會撥打task.Wait()並阻止,因爲否則您的任務可能會繼續超出該特定HTTP請求的範圍。

對於客戶端UI應用程序,從同步方法中調用async方法可能會出現一些不同的情況。例如,您可以使用ContinueWith(action, TaskScheduler.FromCurrentSynchronizationContext())並從action(如this)發起完成事件。

+1

那裏好抓。我懷疑ContinueWith,但我無法明確指出問題所在。我似乎很清楚有些東西阻止線程返回到線程池。我喜歡你完全擺脫ContinueWith的解決方案。簡單得多。 –

+0

@ErikTheViking,也許有一些這樣的片段。或者就像在緊密的循環中調用它。我不確定單個阻止呼叫可能會爲16個用戶造成問題。 – Noseratio

+1

@Noseratio你是一個天才!我做了這個改變,我的負載測試結果大大提高了!一個問題:如果我必須在同步上下文中調用異步方法,那麼在何處使用await是不可能的,那麼執行此操作的最佳方式是什麼? – Aref

3

異步和等待不應該創建大量線程的,特別不只有16個用戶。事實上,它應該可以幫助你更好地利用線程。 MVC中異步和等待的目的是當它忙於處理IO綁定任務時,實際上放棄線程池線程。這表明你在某個地方做了一些愚蠢的事情,比如產生線程,然後無限期地等待。

不過,900線程並不是很多,如果他們使用100%cpu,那麼他們不會等待......他們正在咀嚼什麼。這是你應該關注的東西。你說過你已經使用過NewRelic這樣的工具,他們指出哪些是CPU使用率的來源?什麼方法?

如果我是你,我會首先證明,僅僅使用異步和等待不是你的問題的原因。只需創建一個模擬該行爲的簡單網站,然後在其上運行相同的測試。

其次,獲取您的應用程序的副本,並開始剝離東西,然後運行測試。看看你是否可以追查問題的確切位置。

+0

我怎麼找到那個愚蠢的東西?我們已經對幾乎整個網站和API進行了簡介,並修復了每個性能瓶頸。 – Aref

+0

@Aref:你應該尋找任何使用'Task.Run','Task.Factory.StartNew'或'Task.Start';這些不應該在ASP.NET中使用。此外,任何使用「並行」或並行LINQ。 –

+0

@Aref - 我給了你一些你可以使用的技巧,比如製作一個副本並剝離東西,直到問題消失。 –

3

有很多東西要討論。

首先,當應用程序幾乎沒有業務邏輯時,async/await可以自然地幫助您。我的意思是async/await的意思是在睡眠模式下沒有多線程等待某些東西,大部分是IO,例如數據庫查詢(和讀取)。如果您的應用程序使用cpu 100%進行巨大的業務邏輯,則異步/等待不會對您有所幫助。

900線程的問題在於它們效率低下 - 如果它們同時運行。重點在於擁有如此數量的「業務」線程會更好,因爲服務器具有核心/處理器。原因是線程上下文切換,鎖爭用等。有很多像LMAX distruptor模式或Redis這樣的系統在一個線程中處理數據(或者每個核心中有一個線程)。這更好,因爲你不必處理鎖定。

如何達到所述的方法?查看干擾程序,對傳入的請求進行排隊並逐個處理它們,而不是並行處理。

相反的方法,當幾乎沒有業務邏輯時,許多線程只是等待IO是將異步/等待放入工作的好地方。

它是如何工作的:有一個線程從網絡讀取字節 - 大多隻有一個。一旦某些請求到達,該線程將讀取數據。處理請求的工作線程池也很有限。異步的一點是,一旦一個處理線程正在等待某些事情,主要是io,db,該線程將以輪詢返回,並可用於其他請求。一旦IO響應準備就緒,池中的某個線程將用於完成處理。這就是如何在一秒鐘內使用少量線程來處理千次請求的方式。

我建議你應該畫一些圖片你的網站是如何工作的,每個線程做了什麼以及它如何同時工作。請注意,有必要確定吞吐量或延遲對您是否重要。