影响ASP.NET/ASP.NET Core 服务的10个性能问题及解决方案

我将告诉您可能导致服务性能问题的 10 种类型的问题。这并不是说我对所有可能的问题类型进行了分类,但这些可能会给您一些想法,并在您下次深入研究性能问题时将您指向正确的方向。

服务性能问题的发生有很多不同的原因。内存问题、缓慢的数据库请求和机器资源不够用或许只是其中的一部分。我亲眼目睹了很多问题,并在此过程中学到了一些技巧。

在本文中,我将告诉您可能导致服务性能问题的 10 种类型的问题。这并不是说我对所有可能的问题类型进行了分类,但这些可能会给您一些想法,并在您下次深入研究性能问题时将您指向正确的方向。

这些问题如下,排名不分先后:

1. 数据库慢查询

与数据库的快速交互可能是获得良好性能的最重要的事情。至少在大多数应用中。不幸的是,有很多事情可能会出错,即使是看似无辜的实现也会导致问题。以下是一些可能导致缓慢的数据库请求并破坏应用程序性能的问题:

错误的索引策略糟糕的架构设计•在服务器上而不是在数据库上完成的工作。像这样:

// Goodvar adults = dbContext.Users.Where(user => user.Age >= 18);var count = adults.Count;// Badvar adults = dbContext.Users.Where(user => user.Age >= 18).ToList;var count = adults.Count;

在第二种情况下,第一个表达式以结尾

.ToList

,它告诉 Entity Framework 立即执行查询。结果,所有用户都从数据库中检索出来,然后在服务器进程中计数。相比之下,在第一种情况下,SQL 查询会包含一个

COUNT

操作,该功能会在数据库端完成。•地理位置。最好将数据库放置在地理位置靠近服务器的位置,最好是在同一数据中心。为什么要为每个请求支付 ping 持续时间价格?•数据库使用不当. 并非所有数据库都擅长处理同类业务。有些非常适合键值存储,有些非常适合交易,有些仍然非常适合存储日志。使用最适合您需求的数据库。例如,像MongoDB[1]这样的基于文档的数据库不擅长[2]JOIN操作,使用它们会影响性能。但它非常适合存储包含大量数据的文档。因此,一种解决方案可能是在文档之间复制信息,而不是使用JOIN(谨慎使用)。•数据库资源不足。虽然服务器的扩展是显而易见的,但不要忘记数据库也需要扩展。例如,在 Azure SQL Server 中,您必须密切关注DTU[3](数据库事务单元)。在其他数据库中,您需要关注存储、RAM、网络和 CPU。根据我的经验,当接近极限时,您并不总是能得到清晰的警报。事情开始变慢,让你想知道到底发生了什么。•低效查询总是可能的。如果您使用的是实体框架,则生成的 SQL 并不总是最佳的。•每次都需要重新建立连接。如果您的数据库连接没有正确复用,您可能会发现自己为每个查询重新建立连接。•当复杂查询需要大量时间时,请考虑小心的使用存储过程。•糟糕的分片策略。请注意将相关数据分组在同一个分片中,否则您可能会在同一个请求中查询多个分片。

解决这些问题最困难的部分是首先识别它们。有许多工具可以查看您的请求在生产中的执行情况。通常,数据库本身将能够显示缓慢的查询、扩展问题、网络达到其限制等。

Application Insights[4]等 APM 解决方案很好地展示了这一点。将请求执行时间添加到日志中[5]并围绕它构建查询和自动化[6]也非常简单。

2. 内存压力

高吞吐量服务器中最常见的问题之一是内存压力[7]。在这种状态下,垃圾收集器跟不上内存分配和释放。当 GC 受到压力时,您的服务器会在垃圾收集过程中花费更多时间,而执行代码的时间更少。

这种状态可能在几种情况下发生。最常见的情况是当您的内存容量耗尽时。当您达到内存限制时,垃圾收集器会恐慌并启动更频繁的完整 GC 收集[8](那些是昂贵的)。但问题是为什么首先会发生这种情况?为什么没存接近极限?原因通常是糟糕的缓存管理或内存泄漏[9]。通过捕获内存快照并检查占用所有字节的内容,使用内存分析器[10]很容易发现[11]这一点。

重要的是首先要意识到您有内存问题。找出这一点的最简单方法是使用性能计数器[12]

3. 未使用缓存

缓存可以是一种很好的优化技术。典型的例子是当客户端发送请求时,服务器可以将结果保存在缓存中。当客户端再次发送相同的请求(可能是不同的客户端或相同的客户端)时,服务器不需要再次查询数据库或进行任何类型的计算来获得结果。它只是从缓存中检索它。

一个简单的例子是当您在 Google 上搜索内容时。如果这是一个常见的搜索,它可能每天都会被要求多次。没有必要重新做任何 Google 正在做的魔术来获得具有相同 10 个结果的第一页。它可以从缓存中检索。

当然缓存增加了复杂性。一方面,您需要每隔一段时间使该缓存无效。在谷歌搜索的情况下,考虑到在搜索新闻时,你不能永远返回相同的结果。缓存的另一个问题是,如果管理不当,它会膨胀并导致内存问题。

如果您使用的是 ASP.NET,则有出色的缓存实现[13]可以为您完成大部分工作。

4. 非最优GC模式

.NET 垃圾收集器有两种不同的模式:工作站 GC 模式和服务器 GC 模式[14]。前者针对资源使用量最少的快速响应进行了优化,后者针对高吞吐量进行了优化。

.NET 运行时将模式默认设置为桌面应用程序中的 Workstation GC 和服务器中的 Server GC。这个默认值几乎总是最好的。在服务器的情况下,GC 将使用更多的机器资源,但将能够处理更大的吞吐量。换句话说,该进程将有更多线程专用于垃圾收集,并且能够每秒释放更多字节。

无论出于何种原因,您的服务器都可能在工作站模式下工作,而更改为服务器模式将提高性能。在极少数情况下,您可能希望将服务器 GC 模式设置为 Workstation,如果您希望服务器消耗更少的机器资源(CPU 和 RAM),这可能是合理的。

5. 不必要的客户端请求

有时,可以试着减少客户端请求的数量。减少这个数据量,您可以拥有更少的服务器机器或减少现有机器的负载。这里有几种方法可以做到这一点:

1.限制– 类似于在 Google 中搜索时提供的自动完成机制。当您开始输入字母时,谷歌会显示一个下拉列表,其中包含以这些字母开头的最常见搜索。要获取这些自动完成值,Google 必须从服务器中检索它们。因此,假设您正在键入“制表符与空格”。Google 可以向其服务器发送 15 个请求——在“T”、“Ta”、“Tab”等上。但它不需要。它可以实现一个简单的节流机制,等待您停止输入 500 毫秒,然后发送单个请求。2.客户端缓存– 继续我们的 Google 搜索自动完成示例,有很多搜索以相同的词开头。例如“为什么是”、“我应该”、“在哪里”等。谷歌可能会提前在客户端保存最常见的自动完成结果,而不是每次都发送请求,从而节省不必要的请求。3.批处理– 让我们假设谷歌监视用户活动以利用个性化数据。使用 Gmail 时,它可能希望在您每次阅读电子邮件并将鼠标悬停在某个词上时发送遥测数据。Google 可以为每个此类事件发送一个请求,但保存一堆这样的事件然后在单个请求中发送它们会更有效。

6. 请求挂起

在某些情况下,请求会挂起。也就是说,您发送请求但从未收到响应。或者更确切地说,您最终会收到超时响应。例如,如果处理请求的代码中存在死锁[15],就会发生这种情况。或者,如果您有某种无限循环,称为 “CPU 绑定挂起”。或者,如果您正在无限期地等待永远不会到来的东西——比如来自队列的消息、长的数据库响应或对其他服务的调用。

在后台运行时,当请求挂起时,它会挂起一个或多个线程。但是应用程序将继续运行,与其他线程一起处理新请求。假设这个挂起在额外的请求上重现,随着时间的推移,更多的线程将挂起。后果取决于挂起的原因。如果是 CPU 绑定挂起,例如无限循环,CPU 内核将很快达到最大值,这将使系统爬行,从而导致请求非常缓慢。最终,IIS 将开始返回 503 错误响应–服务不可用。如果原因是死锁,那么它也会逐渐导致内存和性能问题,并最终导致相同的结果——非常慢的请求和 503 错误。

因此,如果请求挂起持续发生,则可能会对服务器的性能造成极大的破坏。

解决这个问题的方法是解决问题的核心原因。您必须首先诊断出挂起的问题,然后采取措施调试这些挂起。

7. 异常处理导致的崩溃

与挂起一样,崩溃可能表现为性能问题。

ASP.NET 服务器何时崩溃?当请求期间发生常规异常时,应用程序不会崩溃。服务器返回 500 错误响应,一切照常进行。但是如果异常发生在请求上下文之外,比如在您自己启动的线程中,则可能会发生崩溃。除此之外,也有像灾难性的例外OutOfMemoryExceptionExecutionEngineException和我最喜欢的一个StackOverflowException。无论catch您放置多少子句,这些都会使过程崩溃。

当托管在 IIS 中的 ASP.NET 应用程序崩溃时,服务器将暂时关闭。IIS 将执行应用程序池回收,重新启动您的iis并恢复正常业务。对客户端的影响将是临时缓慢的请求或 503 错误。

根据您的应用程序,一次崩溃可能不是世界末日,但反复崩溃会使服务器非常缓慢,将真正的原因隐藏为性能问题。解决这个问题的方法当然是处理崩溃的根本原因[16]

8.垂直扩展和水平扩展

这个问题很明显,但我还是要提一下。随着您的应用程序使用量开始增长,您必须考虑如何处理更大的吞吐量。

解决方案当然是扩大规模。有两种方法可以做到这一点——垂直扩展(又名扩展)和水平扩展(又名扩展)。垂直扩展意味着为您的机器添加更多功能,例如更多 CPU 和 RAM,而水平扩展意味着添加更多机器。

云提供商通常会提供某种简单的自动扩展[17],值得考虑。

9. 针对请求设计附加功能

用附加功能装饰您的请求是很常见的。这些可能以ASP.NET Core 中间件[18]或Action 过滤器[19]的形式出现。功能可能是遥测、授权、添加响应标头或其他完全的东西。请特别注意这些代码段,因为它们是针对每个请求执行的。

这是我亲身经历的一个例子。有问题的服务器包括一个中间件,如果用户有一个有效的许可证,它会检查每个请求。这涉及向 Identity Server 发送请求和数据库调用。因此,每个请求都必须等待这些响应,从而增加了大量时间并增加了身份服务器和数据库的负载。解决方案是一个简单的缓存机制,将许可证信息保存在内存中一天。

如果您有类似的功能,缓存可能是一个选项。另一种选择可能是分批完成任务。例如,每 1000 次而不是每一次记录遥测。或者可能将消息放入队列中,将此功能变为异步。

10. 同步 vs 异步

每当您的服务向某个服务发送请求并需要等待响应时,就会存在风险。如果其他服务很忙,处理大量请求怎么办?如果它有一个性能问题,你也必须传递性地遭受痛苦怎么办?

处理这种情况的基本模式是将同步调用更改为异步调用。这通常是通过像Kafka[20]或RabbitMQ[21]这样的队列服务来完成的(Azure 也有队列服务[22])。您可以向这样的队列发送消息,而不是发送请求并等待响应。另一个服务将拉取这些消息并处理它们。

但是,如果您需要回复怎么办?然后,另一个服务将向同一个队列发送带有响应的消息,而不是等待一个。您还将从队列中提取消息,当响应到达时,您可以在原始请求的上下文之外根据需要处理它。如果您需要将响应发送给客户端,您可以使用带有SignalR 之[23]类的推送通知。

这种模式的好处是系统组件从不主动等待服务。一切都是异步处理的,而不是同步处理的。另一个优点是服务之间可以更加松散地耦合。

缺点是这使得系统变得复杂。您需要引入一个队列,推送和拉取消息,并在拉取消息但尚未处理时处理诸如服务崩溃之类的事情,而不是对服务的简单请求。

结论

许多问题都可能会扰乱您的服务性能,其中有很多可能出错的地方。我认为构建一个快速而健壮的系统没有任何技巧和捷径,您需要仔细的计划、经验丰富的工程师和大量的时间来处理可能出错的事情。

除了工程师,需要下一个最重要的事情:调试出这些问题。检测问题并找到核心原因通常是工作的 90%。如果出现服务问题,许多工具都可以提供帮助,例如性能计数器、APM 工具、性能分析器等。

References

[1]MongoDB:https://www.mongodb.com/

[2]擅长:https://www.mongodb.com/

[3]DTU:https://docs.microsoft.com/en-us/azure/azure-sql/database/purchasing-models#dtu-based-purchasing-model

[4]Insights:https://docs.microsoft.com/en-us/azure/azure-monitor/app/asp-net-dependencies

[5]将请求执行时间添加到日志中:
https://docs.microsoft.com/en-us/ef/ef6/fundamentals/logging-and-interception

[6]围绕它构建查询和自动化:
https://michaelscodingspot.com/maximizing-the-power-of-logs-as-your-application-scales/

[7]内存压力:
https://michaelscodingspot.com/avoid-gc-pressure/

[8]完整 GC 收集:
https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals#generations

[9]内存泄漏:
https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/

[10],使用内存分析器:
https://michaelscodingspot.com/memory-profilers-principles/

[11]发现:
https://michaelscodingspot.com/memory-profilers-principles/

[12]性能计数器:
https://michaelscodingspot.com/performance-counters/

[13]缓存实现:
https://michaelscodingspot.com/cache-implementations-in-csharp-net/

[14]工作站 GC 模式和服务器 GC 模式:
https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/workstation-server-gc

[15]死锁:
https://michaelscodingspot.com/c-deadlocks-in-depth-part-1/

[16]处理崩溃的根本原因:
https://michaelscodingspot.com/debugging-net-program-crash-post-morterm-debugging/

[17]自动扩展:
https://azure.microsoft.com/en-us/features/autoscale/

[18]ASP.NET Core 中间件:
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/

[19]Action 过滤器:
https://www.tutorialsteacher.com/mvc/action-filters-in-mvc

[20]Kafka:https://kafka.apache.org/

[21]RabbitMQ:https://www.rabbitmq.com/

[22]队列服务:
https://www.todaysoftmag.com/article/1260/what-messaging-queue-should-i-use-in-azure

[23]SignalR 之:
https://dotnet.microsoft.com/apps/aspnet/signalr

内容出处:,

声明:本网站所收集的部分公开资料来源于互联网,转载的目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。如果您发现网站上有侵犯您的知识产权的作品,请与我们取得联系,我们会及时修改或删除。文章链接:http://www.yixao.com/tech/24924.html

发表评论

登录后才能评论