我有一个 .NET 7 应用程序,每天处理几千个用户。在高峰时段,应用程序会消耗其运行的 IIS 服务器上的所有资源。
我知道下一步是容器化和所有这些好东西,但现在我想充分利用我所拥有的东西。我对开发还很陌生,而且没有计算机科学背景,也不能自称完全理解异步或多线程。
例如,我有一个端点,每秒可能被调用大约 300 次。数据库后端是 SQL Server 2019(标准),我使用的是 Entity Framework Code First。它使用 .NET Identity 在这些端点上进行身份验证。
关于如何使这两个片段达到绝对最快的速度,并在不破坏我的服务器的情况下处理最多数量的并发用户的任何建议,以及一个简短的解释,以便我可以在我的应用程序中采用类似的范例。
获取结果
[Route("/mobile/result/{matchtype}/{referenceid}/{lastchecked}")]
public async Task<IActionResult> GetResult(MatchType matchType, long referenceid, string lastchecked)
{
DateTime datetocheck = DateTime.Parse(HttpUtility.UrlDecode(lastchecked));
Result response = new Result();
if (matchType == MatchType.League)
{
response = _context.Results
.Where(i => i.Fixture.Id == referenceid && i.LastUpdated >= datetocheck)
.Select(i => new Result()
{
ReferenceId = referenceid,
MatchType = matchType,
ResultId = i.Id,
HomeFrameScore = i.HomeScore,
AwayFrameScore = i.AwayScore,
HomeHandicap = i.HomeTeam.Handicap,
AwayHandicap = i.AwayTeam.Handicap,
MatchStatus = i.Status
//Set scores for comps
})
.FirstOrDefault();
if (response == null)
return NoContent();
response.Frames = new List<Frame>();
response.Frames = _context.ResultFrames
.Where(i => i.Result.Fixture.Id == referenceid)
.Select(i => new Frame()
{
ResultFrameId = i.Id,
FrameNumber = i.FrameNumber,
HomeBreak = i.HomeBreak,
HomeDish = i.HomeDish,
HomeForfeit = i.HomeForfeit,
HomeLockedIn = i.HomeLockedIn,
HomeScore = i.HomeScore,
AwayBreak = i.AwayBreak,
AwayDish = i.AwayDish,
AwayForfeit = i.AwayForfeit,
AwayLockedIn = i.AwayLockedIn,
AwayScore = i.AwayScore,
Status = i.FrameStatus,
MatchFormatSectionId = i.MatchFormatSection.Id
}).ToList();
var resultframeplayers = _context.ResultFramePlayers
.Where(i => i.Result.Fixture.Id == referenceid)
.Select(i => new FramePlayer()
{
ResultFramePlayerId = i.Id,
Number = i.Number,
ResultFrameId = i.ResultFrame.Id,
Joker = i.Joker,
HomeAway = i.HomeAway,
PlayerId = i.PlayerSeasonTeam != null ? i.PlayerSeasonTeam.Player.Id : 0
}).ToList();
foreach( var rf in response.Frames)
{
rf.HomeFramePlayers = resultframeplayers.Where(i => i.ResultFrameId == rf.ResultFrameId && i.HomeAway == HomeAway.Home).ToList();
rf.AwayFramePlayers = resultframeplayers.Where(i => i.ResultFrameId == rf.ResultFrameId && i.HomeAway == HomeAway.Away).ToList();
}
}
return Json(response);
}
锁定部分
[Route("/mobile/lockinsection")]
[HttpPost]
public async Task<IActionResult> LockInSection([FromBody] FrameResultData data)
{
var user = await _userManager.GetUserAsync(User);
if (data.MatchType == MatchType.League)
{
var checkcaptain = ValidCaptain(data.MatchType, data.ReferenceId);
if (checkcaptain == false)
return BadRequest("Not a valid captain for this match");
var captainhomeaway = CaptainHomeOrAway(data.MatchType, data.ReferenceId);
if (data.LockIn.HomeAway != captainhomeaway)
return BadRequest("Captain locking in for the wrong team");
foreach(var frame in data.LockIn.Frames)
{
var resultframe = _context.ResultFrames
.Include(i => i.Result)
.FirstOrDefault(i => i.Id == frame.ResultFrameId);
if (data.LockIn.HomeAway == HomeAway.Home)
resultframe.HomeLockedIn = LockedIn.True;
if (data.LockIn.HomeAway == HomeAway.Away)
resultframe.AwayLockedIn = LockedIn.True;
_context.Update(resultframe);
}
var result = _context.Results.FirstOrDefault(i => i.Fixture.Id == data.ReferenceId);
result.LastUpdated = DateTime.UtcNow;
_context.Update(result);
_context.SaveChanges();
return Ok();
}
else
{
//Competition
}
return BadRequest();
}
对于性能,有两个相关但独立的问题需要考虑。执行一段代码需要时间,然后还有等待响应的代码可以做什么的问题。
对于性能,我可以建议进行一些调整。在读取中,您可以通过利用导航属性引用来简化对单个查询的调用:
其作用是在单个查询中读取您与框架的匹配,然后根据我称为“玩家”的预期导航属性单独加载所有玩家作为示例。因此,生成的匿名对象包含结果及其关联帧,以及结果的所有玩家的集合。从那里您可以浏览框架来过滤适当的主客场设置,而不是再次访问数据库。您可以在填充帧时在内部查询结果帧播放器,但一次性加载它们并进行过滤可能更有效。这使得数据库无需多次往返即可完成大部分工作。
对于更新...我只需加载结果及其关联的帧一次,然后更新字段:所以而不是这样:
如果您更新 3 个帧,将执行 4 个查询,这可以改进为仅使用单个查询:
然后在结果中找到适用的帧并更改这些值以及 Result.last 修改等。或者,如果结果有很多帧并且您只需要一个特定的集合:
因此,如果结果包含几十个帧,而我们只关心更新一小部分,那么只加载我们关心的帧而不是全部可能会更有效。
接下来的事情是在加载跟踪实体时不使用
Update()
,只需更新值并调用SaveChanges()
.Update()
是一种更新分离实体的方法。问题在于Update()
,无论是否发生任何更改,它都会为所有列生成更新 SQL 语句。对于跟踪的实体,EF 将仅针对已更改的列生成 Update SQL 语句,并且仅当实际发生任何更改时。您的 API 方法设置为异步执行,但其中的所有代码大部分都是同步的。这意味着调用将阻塞并且在代码完成之前不执行任何操作。异步不会使代码执行得更快,如果有的话,它会使代码执行得稍微慢一些,但它确实可以让调用者在代码执行时可以做其他事情。对于 Web API,等待代码是处理针对服务器的 Web 请求的线程。在同步世界中,如果您有 100 个工作线程,您可以接收 100 个请求,那么请求 101 必须等待其中一个工作线程完成。提高响应能力的唯一方法是使代码更快或增加资源,以便服务器可以一次处理 200 个请求等。这可以是垂直扩展(更大的硬件)或水平扩展(具有负载平衡的更多侦听器)存在限制和瓶颈,例如数据库连接。一旦代码尽可能快地执行,并且您平均每个请求 150 毫秒,但希望确保 Web 服务器能够响应,这就是异步可以提供帮助的地方。这可能意味着代码需要 160 毫秒而不是 150 毫秒才能运行,但会在执行时释放 Web 请求线程。第 101 个请求不会等待 150 毫秒才开始,它可能只等待 45 毫秒。最好情况下,对每个请求的响应可能需要 160 毫秒或更长时间,但每个服务器实例通常可以处理更多请求。由于您的方法已经设置为 并且您平均每个请求需要 150 毫秒,但希望确保 Web 服务器能够响应,这就是异步可以提供帮助的地方。这可能意味着代码需要 160 毫秒而不是 150 毫秒才能运行,但会在执行时释放 Web 请求线程。第 101 个请求不会等待 150 毫秒才开始,它可能只等待 45 毫秒。最好情况下,对每个请求的响应可能需要 160 毫秒或更长时间,但每个服务器实例通常可以处理更多请求。由于您的方法已经设置为 并且您平均每个请求需要 150 毫秒,但希望确保 Web 服务器能够响应,这就是异步可以提供帮助的地方。这可能意味着代码需要 160 毫秒而不是 150 毫秒才能运行,但会在执行时释放 Web 请求线程。第 101 个请求不会等待 150 毫秒才开始,它可能只等待 45 毫秒。最好情况下,对每个请求的响应可能需要 160 毫秒或更长时间,但每个服务器实例通常可以处理更多请求。由于您的方法已经设置为 最好情况下,对每个请求的响应可能需要 160 毫秒或更长时间,但每个服务器实例通常可以处理更多请求。由于您的方法已经设置为 最好情况下,对每个请求的响应可能需要 160 毫秒或更长时间,但每个服务器实例通常可以处理更多请求。由于您的方法已经设置为
async
,您可以通过等待读取而FirstOrDefaultAsync()
不是FirstOrDefault()
和 等方法来提高响应能力SaveChangesAsync()
。只要确保await
他们。希望这能让您思考和尝试一些事情,请随时用您认为可能需要改进的特定问题领域来更新问题。
对于 1000 mCPU(1 核),.NET 最多可以处理大约 150-300 个不同的请求,这相当于每个请求 1ms-3ms。我的意思是,如果您的请求是批量的,那么rps 可能会大得多,但是您的客户通常是针对单一实体 - 单一客户,您不能强迫他们,仅此而已。
您的任务是将它们组合起来,以便可以降低 IO/CPU(降低结果延迟)。做到这一点的最佳方法是缓存和 RPC(远程过程调用)。
它在实践中是如何运作的?
这样您就可以放大/缩小 FE 负载和 BE 负载。FE 具有低延迟、快速读取/更新操作,甚至没有单个 500 错误,全部在 1-150 毫秒以下,可以地理分布式,在这里提供最佳的用户体验和可用性。BE 具有高延迟(长达几分钟/天),有时可能会死机,通常存在于单个区域,可以频繁更新,但可以以更低的成本更快地进行批量处理,并在出现问题时重试。
就是这样。这就是大多数成功公司的做法,这是分销的圣杯,您自己可能已经看到了它的一个变体(它可能要复杂得多,并且与其他堆栈一起使用,但根据我的经验永远不会改变)。
您可以尝试优化您的代码(就像@Steve建议的那样),但您永远不会比最终一致的模型更快。现在你的服务是强一致性的- 它的结果在更新后立即可用,锁定你的客户端和服务器上的资源,等待某些事情发生或失败,这是你的主要瓶颈。不是你的代码、IIS 或缺乏容器化(虽然效率低下,但现在并不重要)。