我们为我们的解决方案提供SOA。我们正在使用.net framework 4.5.1,asp.net mvc 4.6,sql server,windows server和thinktecture身份服务器3(用于基于令牌的webapi调用。)
我们的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代码无法正常工作。
答案 0 :(得分:2)
更好的问题是; 在您的解决方案中,您的mvc应用程序如何与您的webapi应用程序进行通信?这里的最佳做法是什么?
此处的最佳做法是客户端(在您的情况下为浏览器)直接从Web API控制器检索数据,并且MVC控制器仅提供包含布局,样式的纯HTML视图( css),视觉结构,脚本(即javascript)等,而不是数据。
图片来源:Ode to Code。顺便提一下,该网站上的作者也不推荐您的方法,尽管它被列为一个选项。
如果不这样做并在调用堆栈中添加网络请求步骤,则会在数据流中创建一个不必要的昂贵步骤(从MVC控制器调用到Web API部署)。在执行较慢的执行期间越过越多的边界。
正如您已经想到的那样,快速解决方案是直接从您的MVC项目调用您的业务代码库。这将避免额外的和不必要的网络步骤。这样做没有任何问题,许多传统网站同时提供视图(html)和数据。它使得设计更加紧密,但它比你拥有的更好。
最好的长期解决方案是更改MVC视图,以便他们直接调用您的Web API部署。这可以使用Angular
,React
,Backbone
等框架来完成。如果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完成之前捕获同步上下文。否则,将在此上下文中完成继续,如果其他线程正在使用此锁定,则可能导致锁定。 这样做将允许在等待任务的上下文中继续进行。