ASP.NET Web API 2 中的并发请求比单个请求慢近六倍

Concurrent request in ASP.NET Web API 2 is nearly six times slower than a single request

背景:

我们构建了一个与 ASP.NET Web API 2 后端通信的 React SPA,.NET Framework 4.6.1。在我们的初始加载中,我们发出两个单独的请求来加载数据。当加载大量数据时,我们注意到 API 请求在应用程序中发出时比我们在 Postman 中单独尝试请求时要慢得多。

原文:

示例结构,fetch当然使用我们的API:

fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(JSON.stringify(myJson));
  });

fetch('http://example.com/otherMovies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(JSON.stringify(myJson));
  });

示例 C# API 方法:

[HttpGet]
[Route("{bsid}/{caseId}")]
public IHttpActionResult GetRenewalCycleForCustomer(string bsid, int caseId)
{
    var customerNumbers = GetCustomerNumbers();

    var userId = HttpContext.Current.User.Identity.GetUserId<int>();

    var user = identityDb.Users.Find(userId);

    var customerNumbers = user.ApplicationUserCustomerNumbers.Select(x => new CustomerNumberKey() { bsid = x.CustomerNumber.bsid, NameNo = x.CustomerNumber.NameNo }).ToList();

    var db = new DbContext();

    var caseService = new CaseService(db);

    var portfolioTabViewModel = caseService.GetPortfolioTabViewModelForCustomer(bsid, caseId, customerNumbers);

    return Ok(portfolioTabViewModel);
}

OS is Windows 10 Pro 和IIS 应该可以处理10 个并发连接,根据Internet。无论如何都没有关系,因为我们将它作为 Windows 服务器托管在 Azure 上,并且我们在那里有相同的响应时间。也尝试了 App Service,结果是一样的。

与 Postman Runner 同步测试响应时间

Postman Runner 的两个实例同时运行:

其他说明:

似乎与硬件无关,因为 CPU,无论是否同时发出请求,内存和磁盘都不会受到值得注意的影响。

我们能做些什么来解决这个问题?

似乎 运行 并行的非异步方法:

一些线程建议会话状态,但我在 Web.config 中找不到任何对此的引用,这不是我启用的任何内容。搜索 HttpContext.Current.SetSessionStateBehavior 得到 0 个结果。

Windows 10 个资源:

https://serverfault.com/a/800518/293367

https://forums.asp.net/t/2100558.aspx?concurrent+connections+on+windows+pro+10+IIS

更新异步和 IIS:

好像跟async和IIS没有关系,用下面的方法测试了并发请求。并发的慢请求似乎取决于其他东西。

异步:

[HttpGet]
[Route("{bsid}/{caseId}")]
public async Task<IHttpActionResult> Get(string bsid, int caseId)
{
    await Task.Delay(3000);
    return Ok();
}

同步:

[HttpGet]
[Route("{bsid}/{caseId}")]
public IHttpActionResult Get(string bsid, int caseId)
{
    Thread.Sleep(3000);
    return Ok();
}

更新 2:具有异步和同步调用的数据库:

好像也不是数据库调用。使用 Include 进行测试,尽管异步调用速度相当慢,但调用速度相似。

异步:

[HttpGet]
[Route("{bsid}/{caseId}/async")]
public async Task<IHttpActionResult> GetAsync(string bsid, int caseId)
{
    var db = new DbContext();

    var deviations = await db.RenewalCycles.Where(x => x.Deviations.Any())
        .Include(cycle => cycle.TPCase.CaseNames.Select(caseName => caseName.TPName))
        .Include(cycle => cycle.TPCase.CaseNames.Select(caseName => caseName.TPNameType))
        .Include(cycle => cycle.TPCase.GoodsAndServicesDescriptions)
        .Include(cycle => cycle.TPCase.RelatedCases.Select(relatedCase => relatedCase.TPCaseRelation))
        .Include(cycle => cycle.TPCase.RelatedCases.Select(relatedCase => relatedCase.TPCountry))
        .ToListAsync();

    return Ok();
}

同步:

[HttpGet]
[Route("{bsid}/{caseId}")]
public IHttpActionResult Get(string bsid, int caseId)
{
    var db = new DbContext();

   var deviations = db.RenewalCycles.Where(x => x.Deviations.Any())
        .Include(cycle => cycle.TPCase.CaseNames.Select(caseName => caseName.TPName))
        .Include(cycle => cycle.TPCase.CaseNames.Select(caseName => caseName.TPNameType))
        .Include(cycle => cycle.TPCase.GoodsAndServicesDescriptions)
        .Include(cycle => cycle.TPCase.RelatedCases.Select(relatedCase => relatedCase.TPCaseRelation))
        .Include(cycle => cycle.TPCase.RelatedCases.Select(relatedCase => relatedCase.TPCountry))
        .ToList();

    return Ok();
}

市长开销的部分竟然是数据库相关的。一个方法使用了db.Cases.Find(bsid, caseId),然后将模型转换为视图模型。 Cases 模型反过来有很多关系,所有这些都发出单独的数据库调用,因为模型具有标记为 virtual 的属性,例如 public virtual TPRenewalCycle TPRenewalCycle { get; set; } 以启用延迟加载.通过查看 Visual Studio Output Window (Debug -> Windows -> Output) 并设置 ApplicationDbContext 来找到它,如下所示。

public class ApplicationDbContext : DbContext
{
    protected const string ConnectionStringName = "defaultConnection";
    public ApplicationDbContext() : base(ConnectionStringName)
    {
        Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
        Database.CommandTimeout = 300;
    }