SQL性能、.Net优化与最佳实践

11

我需要您这些专家的确认/解释,因为我的团队告诉我“这没关系”,这让我感到沮丧 :)

背景:我们有一个SQL Server 2008,被我们的主要MVC3 / .Net4 Web应用程序使用。 在任何给定时间点有大约200多个并发用户。 服务器受到了极大的冲击(锁定,超时,总体缓慢),我正在尝试应用在我职业生涯和上一次微软认证课程中学到的东西。 这些都是我们一直在强调的事情(“立即关闭SQL连接”),我正在努力向我的团队解释,这些“小事情”,虽然单个不会有影响,但最终会累加。

我需要知道以下内容是否对性能产生影响,或者它只是“最佳实践”

1. 使用“USING”关键字。 他们的大部分代码都像这样:

public string SomeMethod(string x, string y) {
    SomethingDataContext dc = new SomethingDataContext();
    var x = dc.StoredProcedure(x, y);
}

尽管我试图告诉他们,使用 USING 可以更快地关闭/释放资源:

using (SomethingDataContext dc = new SomethingDataContext()) {
    var x = dc.StoredProcedure(x, y);
}

他们的论点是,GC在代码执行完毕后会很好地清理垃圾,因此USING不会产生巨大影响。这是真还是假?为什么?

2. 连接池

我经常听说设置连接池可以显著加速任何网站(至少对于使用MSSQL的.Net)。我建议我们将以下内容添加到web.config中的connectionstrings:

..."Pooling=True;Min Pool Size=3;Max Pool Size=100;Connection Timeout=10;"...

他们认为,.Net/MSSQL已经在幕后设置了连接池,因此没有必要在web.config中添加它。这是真还是假?为什么其他网站都说应该添加连接池以获得最佳性能,如果它已经设置好了呢?

3. 最小化数据库调用次数

默认的.Net MVC项目提供的角色/成员资格提供程序非常好用,它可以为你处理大部分工作。但是这些人严重使用UsersInRoles(),并像全局变量一样自由使用它(每次调用此方法时,它都会访问数据库)。 我创建了一个“用户对象”,在每个页面加载时预先加载所有角色(以及一些其他用户信息,如GUID等),然后查询此对象以查看用户是否具有该角色。

网站的其他部分有FOR语句,循环200次,并在每次通过时执行20-30个SQL查询=超过4000个数据库调用。它以某种方式在几秒钟内完成了这项工作,但我想将20-30个DB调用合并为一个,以便每个循环重复一次。但是因为SQL分析器显示查询花费了“0秒”,他们的论点是它非常快速和小型,服务器可以处理这些高数量的DB查询。

我的想法是“是的,这些查询正在快速运行,但它们正在损坏整体SQL服务器的性能。” 这可能是一个贡献因素吗?我是在担心无谓的事情,还是说这是服务器整体性能问题的(重要)贡献因素?

4. 其他代码优化

首先想到的一个是使用StringBuilder而不是一个简单的字符串变量。我知道为什么应该使用StringBuilder(特别是在循环中),但他们说这没有关系-即使他们需要编写10k+行,他们的论点是性能提升对他们没有影响。

总之,我们所学习和钻研的所有东西(“最小化范围!”)只是“最佳实践”,没有真正的性能提升,还是它们都导致了真正/可衡量的性能损失?

编辑***

感谢大家的回答!根据你们的回答,我有一个新问题(第五个): 他们实际上并没有使用 "USING",那么这意味着正在发生什么?如果自动发生连接池化,是不是会占用来自池的连接,直到GC出现?每个打开的与SQL服务器的连接是否都会为服务器增加一点负担并使其变慢?
根据您的建议,我计划进行一些严格的基准测试/记录连接时间,因为我怀疑:a)服务器很慢;b)他们没有关闭连接;c)Profiler说它运行了0秒,缓慢可能来自于连接。
非常感谢你们的帮助。再次感谢。


不是说研究不重要,但 SO 上的大师们确实提供了很多见解...学习他们所说的话非常有研究价值。 - Miserable Variable
@JakeWilson801 微软的文档并不总是最好的来源。(编辑:甚至不是一个好的来源) - Mike Cole
重复使用字符串生成器而不是连接字符串 - 如果您只连接一次字符串(例如 var s = "a" + "b"),那么一个字符串会更有效率。但需要记住的是,字符串实例是不可变的,所以在循环中(例如 string s = "a"; for (int i = 1, i < 1000; i++) { s += "a";}),每次循环都会创建一个新的字符串实例,这将影响内存分配并影响整体性能(是否显著取决于连接次数)。 - GarethD
3
你是否使用过性能分析工具(C#或SQL)来查找性能瓶颈所在?如果你向同事展示了使用UsersInRoles()方法和用+连接字符串的方法的统计数据,并证明它们是瓶颈的原因,那么他们更可能相信你。说不定,不正确地使用StringBuilder也导致了速度变慢。 量化分析,不要猜测。 - Jesse Webb
1
谢谢大家 - 是的,我已经做了研究并且是一名开发人员超过12年了 - 但是 - 我是自学的,缺乏一些团队用来反驳我的论点的“语言”。有时候我会依赖于SO专家给我一些方向,并有时加强我已经想到的东西,这就是我在这里做的 :) 对于那些提供建设性评论/帮助/指导的人(咳咳不包括jakewilson咳咳),谢谢,我会再把这个问题保持一段时间,看看是否还有更多的答案。再次感谢! - Losbear
9个回答

5
分支代码,进行更改并针对当前代码库进行基准测试和性能分析。然后你就有了一些证据来支持你的说法。
至于你的问题,以下是答案:
1. 你应该始终手动处理实现IDisposable接口的类,GC不会实际调用dispose方法,但如果该类还实现了finalizer,则会调用finalizer方法,但在大多数实现中,它们仅清除非托管资源。 2. .NET框架已经做到了连接池,我不确定默认值是什么,但连接字符串值只是为了让你可以更改它们。 3. SQL语句的执行时间只是故事的一部分,在SQL profiler中,你只能看到数据库引擎执行查询所需的时间,你错过的是Web服务器连接到数据库服务器并接收结果所需的时间,因此,虽然查询可能很快,但通过批量查询可以节省大量IO和网络延迟。 4. 这个问题很好做一些性能分析,以证明连接符使用比字符串构建器使用更多的内存。

1
根据此网站的说明,连接池的默认值为最小值0,最大值100。 - Jesse Webb
+1 这个答案在彻底回答每个问题方面做得最好。我也喜欢分支和基准测试的建议。 - Jesse Webb
对于#1 - 你们很多人都说了同样的话 - GC不会立即清理它。那么这是否会占用一个连接池?几百个这样的打开连接会减慢服务器速度吗?对于#3,你刚给了我一个巨大的争论点。我将尝试隔离连接时间并为每个点击的2000多个数据库调用添加它,看看结果如何 - 谢谢好主意! - Losbear
不仅应考虑连接时间(即使该操作非常昂贵),还应考虑网络往返时间。您的Web服务器和DB服务器之间通信需要多长时间?将其乘以调用次数,您就知道可以从减少DB调用中获得多少好处... - HansLindgren

4

嘿。当然,你不能让GC为你关闭数据库连接。GC可能需要很长时间才能进行...有时候需要几个小时。它不会在变量超出范围后立即发生。大多数人使用IDisposable using() { }语法,这很好,但至少需要有某种方式调用connection.Close()


3
  1. 实现IDisposable并持有非托管资源的对象还会实现一个finalizer,以确保在GC期间调用dispose方法。问题是,当它被调用时,GC可能需要很长时间进行操作,并且在此之前您可能需要这些资源。使用using语句可以在完成后立即调用dispose方法。

  2. 您可以在webconfig中修改池的参数,但默认情况下已启用,因此如果您保留默认参数,则不会获得任何收益。

  3. 您不仅需要考虑查询执行的时间,还需要考虑应用程序服务器和数据库之间的连接时间,即使它们在同一台计算机上也会增加开销。

  4. 在大多数Web应用程序中,StringBuilder不会影响性能。只有在您将许多内容连接到同一个字符串时,它才会产生重要作用。但我认为使用StringBuilder会更容易阅读。


1
另外:具有终结器的对象(且未调用SuppressFinalize)需要两次垃圾回收才能完全删除。 - Andre Loker
除了我的评论之外:具有终结器并正确实现可处理模式的类型将在其Dispose函数中调用SuppressFinalize。通过处理此类类型,因此可以消除第二次垃圾回收的需要。(这应该更好地涉及到我的第一个评论与问题) - Andre Loker

2

我认为你有两个分开的问题。

  1. 你的代码性能
  2. SQL Server 数据库的性能

SQL Server

你是否对 SQL Server 进行了任何监控?你知道具体是哪些查询导致死锁吗?

我建议阅读this article on deadlocks并考虑安装出色的Who is active以了解 SQL Server 中实际发生了什么。你还可以考虑安装 Brent Ozar 的sp_Blitz。这应该给你一个关于数据库中发生了什么,并提供工具解决问题的良好想法。

其他代码问题

我无法立即对其他代码问题进行评论。因此,我会先查看 SQL server。

记住

  1. 监控
  2. 识别问题
  3. 分析
  4. 修复
  5. 回到步骤1

是的,我已经查看了现场服务器上的SQL Profiler,并试图向他们展示“看,这个点击产生了2,000多个调用!”但他们的论点是“但它在2秒内完成了所有这些2k次调用”。我正在努力超越这一点,并用“这2秒钟确实会累加”的反驳,但我不是100%确定,因为这2秒钟与其他200个并发用户混合在一起。 - Losbear

1

好的,我不是专家,但我有一个建议:如果他们说你错了,告诉他们,“证明一下!给我写一个测试!展示4000个调用和200个调用一样快,并且对服务器产生同样的影响!”

对于其他事情也是一样。如果你没有能力让他们证明你是正确的,就用清晰、充分记录的测试来证明他们是错误的,这样可以说明你的观点是正确的。

如果他们甚至不接受来自他们自己服务器的硬证据,也不看代码和检查,那么你可能在这个团队上浪费时间。


我不能说“证明一下”(尽管我很想这么做),因为这个应用程序是在我的加入之前编写的。所以现在我的任务是向他们证明为什么他们应该回去修复他们的代码 :) - Losbear
@Losbear 这可能是一场艰苦的战斗!但你在其他评论中提到的方向听起来不错。测试,再测试,并展示给他们硬数据。祝你好运! - Ann L.

1

冒着重复其他人所说的风险,这是我对此问题的看法。

首先,你应该谨慎选择你的战斗……我不会在所有四个点上与你的同事开战,因为一旦你无法证明其中一个,就结束了,从他们的角度来看,他们是正确的,而你是错误的。 还要记住,没有人喜欢被告知他们美丽的代码是一个丑陋的婴儿,所以我假设你会很有外交手腕——不要说“这很慢”,而是说“我找到了一种使这更快的方法”……(当然,你的团队可能非常合理,所以我也基于我的经验做出这个假设:)所以你需要先选择上述四个领域中的一个来解决。

我的选择是第三个选项。1、2和4也有一定作用,但根据我的经验,作用不是很大——但你描述的第三个选项听起来像是对可怜的老服务器进行千刀万剐!这些查询可能执行得很快,因为它们是参数化的,所以它们被缓存了,但你需要记住,“0秒”在分析器中可能是900毫秒,如果你明白我的意思...对于许多查询来说,这些时间加起来就会变慢;这也可能是锁的主要来源,因为如果每个嵌套查询都在反复地命中同一张表,无论它运行得多么快,根据你提到的用户数量,肯定会有争用。

获取SQL并在SSMS中运行它,但包括客户端统计信息,这样你不仅可以看到执行时间,还可以看到发送回客户端的数据量;这将给你一个更清晰的图片,说明涉及了什么样的开销。

实际上,唯一能证明这些的方法就是设置一个测试,并进行测量,正如其他人所提到的那样,但还要确保在服务器上运行一些分析——锁定、IO队列等,这样你就可以展示你的方法不仅更快,而且对服务器的负载更小。

谈到你的第五个问题 - 我不确定,但我猜测任何未被自动释放(通过使用)的SqlConnection都被视为仍然“活动”,并且无法再从池中获取。 话虽如此 - 除非连接实际上正在执行任何操作,否则服务器上的连接开销非常低 - 但您可以通过使用SQL性能计数器来证明这一点。

祝你好运,迫不及待地想知道你的进展如何。


谢谢Stephen。是的,我计划对此保持外交礼仪,哈哈:) 我也怀疑是第三个问题,但我想知道所有这些问题共同会产生什么样的影响 - 还是只有一个问题会导致整体缓慢。再次感谢你的建议 - 明天我运行数据后会发布我的发现 :) - Losbear

0

using语句只是一种语法糖,实质上你所做的是

try
{
    resouce.DoStuff();
}
finally
{
     resource.Dispose()
}

Dispose方法可能会在对象被垃圾回收时调用,但前提是框架程序员实现了可处理模式。因此,反对你同事的观点有:

i)如果我们养成使用using的习惯,我们可以确保释放非托管资源,因为并不是所有的框架程序员都能聪明地实现可处理模式。

ii)是的,垃圾回收器最终会清理该对象,但这可能需要一段时间,具体取决于该对象的年龄。第二代垃圾回收仅每秒执行一次清理。

简而言之:

  1. 见上文

  2. 是的,默认情况下池设置为 true,最大池大小为 100

  3. 你说得对,肯定是改进的最佳领域。

  4. 过早优化是万恶之源。先完成 #1 和 #3。使用 SQL Profiler 和特定于数据库的方法(添加索引、碎片整理、监视死锁等)。

  5. 是的,可能是这样。最好的方法是测量它--查看性能计数器 SQLServer: General Statistics – User Connections; here 这篇文章描述了如何做到这一点。

始终测量您的改进效果,不要在没有证据的情况下更改代码!


同意 - 我和我的团队唯一的共识就是我们都不想花太多时间在那些对帮助不大的事情上;比如将字符串变量转换为StringBuilder对象。哈哈。我想最终只有来自性能分析器的具体数据才能说服这些人 :) 谢谢 Bogdan - Losbear

0

最近我遇到了一个问题,我们的Web应用程序和电子邮件提供商之间的交互出现了错误。当发送电子邮件时,会发生协议错误。但不是立即发生。

我能够确定只有在关闭SmtpClient实例时才会发生错误,这是在SmtpClient被处理时发生的,而这只会在垃圾回收期间发生。

我注意到这通常需要在单击“发送”按钮后两分钟左右...

不用说,现在代码已经正确地实现了使用块,用于SmtpClientMailMessage实例。

只是提醒一下聪明人...


两分钟比我想象的要好呢,哈哈 - 我原本以为垃圾回收每5或10分钟循环一次(这真的很糟糕)。感谢您让我了解垃圾回收的频率(我知道它可能会有几分钟的误差)。 - Losbear

0

1已经得到了很好的解决(我同意它可以很好地处理,而且我发现这是一个很好的实践)。

2是ODBC以前版本的一种保留方式,在那里SQL Server连接是独立配置的。它曾经是非默认的;现在是默认的。

至于3和4,4不会影响您的SQL Server性能-StringBuilder可能有助于加快UI中的过程,当然,这可能会导致更快地关闭SQL资源,但它们不会减轻SQL Server的负载。

对我来说,3听起来是最合适的地方。我尽可能快地关闭我的数据库连接,并尽可能少地进行调用。如果您正在使用LINQ,请将所有内容提取到IQueryable或其他内容(列表、数组等),以便您可以操纵它并构建所需的任何UI结构,同时在进行任何操作之前释放连接。

所有这些都说了,听起来你需要花更多的时间与分析器一起度过。不要只看每个执行所花费的时间,而是要看处理器和内存使用情况。仅仅因为它们很快并不意味着它们不是“饥饿”的执行。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接