Mvc应用程序异步方法挂起

时间:2016-03-02 17:29:49

标签: c# asp.net-mvc asynchronous asp.net-web-api

我们为我们的解决方案提供SOA。我们正在使用.net framework 4.5.1,asp.net mvc 4.6,sql server,windows server和thinktecture身份服务器3(用于基于令牌的webapi调用。)

解决方案结构如下;
enter image description here

我们的mvc前端应用程序通过httpClient包装器与我们的webapi应用程序进行通信。这是通用的http客户端包装器代码;

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace Cheetah.HttpClientWrapper
{
    public class ResourceServerRestClient : IResourceServerRestClient
    {
        private readonly ITokenProvider _tokenProvider;

        public ResourceServerRestClient(ITokenProvider tokenProvider)
        {
            _tokenProvider = tokenProvider;
        }

        public string BaseAddress { get; set; }

        public Task<T> GetAsync<T>(string uri, string clientId)
        {
            return CheckAndInvokeAsync(async token =>
            {
                using (var client = new HttpClient())
                {
                    ConfigurateHttpClient(client, token, clientId);

                    HttpResponseMessage response = await client.GetAsync(uri);

                    if (response.IsSuccessStatusCode)
                    {
                        return await response.Content.ReadAsAsync<T>();
                    }

                    var exception = new Exception($"Resource server returned an error. StatusCode : {response.StatusCode}");
                    exception.Data.Add("StatusCode", response.StatusCode);
                    throw exception;
                }
            });
        }

        private void ConfigurateHttpClient(HttpClient client, string bearerToken, string resourceServiceClientName)
        {
            if (!string.IsNullOrEmpty(resourceServiceClientName))
            {
                client.DefaultRequestHeaders.Add("CN", resourceServiceClientName);
            }

            if (string.IsNullOrEmpty(BaseAddress))
            {
                throw new Exception("BaseAddress is required!");
            }

            client.BaseAddress = new Uri(BaseAddress);
            client.Timeout = new TimeSpan(0, 0, 0, 10);
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
        }

        private async Task<T> CheckAndInvokeAsync<T>(Func<string, Task<T>> method)
        {
            try
            {
                string token = await _tokenProvider.IsTokenNullOrExpired();

                if (!string.IsNullOrEmpty(token))
                {
                    return await method(token);
                }

                var exception = new Exception();
                exception.Data.Add("StatusCode", HttpStatusCode.Unauthorized);
                throw exception;
            }
            catch (Exception ex)
            {
                if (ex.Data.Contains("StatusCode") && ((HttpStatusCode)ex.Data["StatusCode"]) == HttpStatusCode.Unauthorized)
                {
                    string token = await _tokenProvider.GetTokenAsync();

                    if (!string.IsNullOrEmpty(token))
                    {
                        return await method(token);
                    }
                }

                throw;
            }
        }

        public void ThrowResourceServerException(List<string> messages)
        {
            string message = messages.Aggregate((p, q) => q + " - " + p);

            var exception = new Exception(message);

            exception.Data.Add("ServiceOperationException", message);

            throw exception;
        }
    }
}

此外,有时这个http客户端包装器使用NitoAsync管理器(调用异步方法作为同步。),有时我们直接使用这种通用方法等待 - 异步任务等待;

var result = await _resourceServerRestClient.GetAsync<ServiceOperation<DailyAgendaModel>>("dailyAgenda/" + id);

所以这是我们的问题:

当我们使用jmeter测试我们的mvc应用程序(用于进行某种负载测试/每1秒10个线程)时,几分钟后,mvc应用程序停止工作[异常是由于超时而取消的任务](也许此行上只有1-2个请求超时):HttpResponseMessage response = await client.GetAsync(uri);。但是在那个请求之后,所有请求都会失败,就像它们排成一行一样。所以mvc应用程序挂了2-15分钟(随机),但在那个时候我可以发送邮件给webapi的新请求。他们没问题,我的意思是webapi响应很好。几分钟后,mvc应用程序恢复正常。

注意:我们有mvc-ui和webapi的负载均衡器。因为有时我们在忙碌的一天中会在一分钟内收到120K请求。但是,如果webapi或mvc应用程序前没有负载均衡器,则会出现相同的错误。所以它不是LB问题。

注2:我们尝试将RestSharp用于mvc-ui和webapi通信。我们在这里得到了同样的错当reuqest失败时,所有请求将连续失败。看起来它是一个网络错误,但我们无法找到它的证明。

你能在我的httpClient包装器上看到任何错误吗?或更好的问题是;
在您的解决方案中,您的mvc应用程序如何与您的webapi应用程序进行通信?这里的最佳做法是什么?

Update1 :我们将项目.net 4.5.1移至4.6.1。同样的僵局再次发生。而且我们暂时移动了该层的所有源代码:&#34; Business&amp;储存库&#34;作为DLL级别。商业与娱乐之间没有webapi。现在的演讲水平。死锁解决了。当我们从webapplication控制器调用webapi方法时,我们仍在搜索为什么httpClientWrapper代码无法正常工作。

3 个答案:

答案 0 :(得分:2)

  更好的问题是;   在您的解决方案中,您的mvc应用程序如何与您的webapi应用程序进行通信?这里的最佳做法是什么?

此处的最佳做法是客户端(在您的情况下为浏览器)直接从Web API控制器检索数据,并且MVC控制器仅提供包含布局,样式的纯HTML视图( css),视觉结构,脚本(即javascript)等,而不是数据。

Browser communicating to MVC and Web API

图片来源:Ode to Code。顺便提一下,该网站上的作者也不推荐您的方法,尽管它被列为一个选项。

  1. 此服务器是您的观看次数和数据之间的良好SOC,可让您更轻松地对任一部分进行更改。
  2. 它允许客户端(浏览器)异步检索数据,从而创造更好的用户体验。
  3. 如果不这样做并在调用堆栈中添加网络请求步骤,则会在数据流中创建一个不必要的昂贵步骤(从MVC控制器调用到Web API部署)。在执行较慢的执行期间越过越多的边界。

    正如您已经想到的那样,快速解决方案是直接从您的MVC项目调用您的业务代码库。这将避免额外的和不必要的网络步骤。这样做没有任何问题,许多传统网站同时提供视图(html)和数据。它使得设计更加紧密,但它比你拥有的更好。

    最好的长期解决方案是更改MVC视图,以便他们直接调用您的Web API部署。这可以使用AngularReactBackbone等框架来完成。如果Web API方法调用有限且预计不会增长,您也可以使用JQuery或纯粹的javascript 我不会尝试在此构建复杂的应用程序,因此Angular这样的框架变得如此受欢迎是有原因的。

    对于这种情况下的实际底层技术问题,我们无法确定没有内存转储以查看导致死锁的资源。这可能是一件简单的事情,比如确保您的MVC操作方法也返回async Task<ActionResult>而不仅仅是ActionResult,我猜,这是你现在如何构建它们 )所以他们可以使用实际的HttpClient模式调用async/await。老实说,因为它的设计很糟糕,我不会花任何时间试图让它发挥作用。

答案 1 :(得分:0)

我不完全确定whu,但我会从重构GetAsync()方法开始

public async Task<T> GetAsync<T>(string uri, string clientId)
    {
        try
        {
         string token = await _tokenProvider.IsTokenNullOrExpired();
         if (!string.IsNullOrEmpty(token))
            {
             using (var client = new HttpClient())
             {
                ConfigurateHttpClient(client, token, clientId);

                HttpResponseMessage response = await client.GetAsync(uri);

                if (response.IsSuccessStatusCode)
                {
                    return await response.Content.ReadAsAsync<T>();
                }

                var exception = new Exception($"Resource server returned an error. StatusCode : {response.StatusCode}");
                exception.Data.Add("StatusCode", response.StatusCode);
                throw exception;
             }
          }
            else
            {
                var exception = new Exception();
                exception.Data.Add("StatusCode", HttpStatusCode.Unauthorized);
                throw exception;
            }
         }
        catch (Exception ex)
        {
            throw;
        }
    }

答案 2 :(得分:0)

你应该把.ConfigureAwait(false)放在你内心的等待声明中:

HttpResponseMessage response = await client.GetAsync(uri).ConfigureAwait(false);

(...)

return await response.Content.ReadAsAsync<T>().ConfigureAwait(false);

(...)

string token = await _tokenProvider.IsTokenNullOrExpired().ConfigureAwait(false);

(...)
return await method(token).ConfigureAwait(false);;

(...)

string token = await _tokenProvider.GetTokenAsync().ConfigureAwait(false);;

(...)

return await method(token).ConfigureAwait(false);

这样,您将避免在await完成之前捕获同步上下文。否则,将在此上下文中完成继续,如果其他线程正在使用此锁定,则可能导致锁定。 这样做将允许在等待任务的上下文中继续进行。