热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

面向.Net程序员的后端性能优化实战熬夜的虫子

最近2个月没做什么新项目完全是对于旧的系统进行性能优化避免超时死锁数据处理能力不够等常见的性能问题这里不从架构方面出发毕竟动大手脚成本比较高那么我们以实例为前提从细节

面向.Net程序员的后端性能优化实战

2015-02-03 18:17  熬夜的虫子  阅读(5154)  评论(10)  编辑  收藏  举报

最近2个月没做什么新项目 完全是对于旧的系统进行性能优化 避免超时 死锁 数据处理能力不够等常见的性能问题

这里不从架构方面出发 毕竟动大手脚成本比较高 那么我们以实例为前提 从细节开始 


优化角度

一.业务逻辑优化

二.DB优化

三.数据处理优化

四.锁与性能

五.cpu飙高小结

六.crash现象分析


 业务逻辑优化

这一条不具有普遍性 不同的业务不同的场景 如果归纳起来 就是在不影响业务的前提下进行流程精简

1. 废弃冗余逻辑 

常见于各种基于数据库的检查 很多同学在维护别人代码的时候 没有深入理解别人的逻辑 也许别人在取数据的时候已经在查询条件中已经过滤了相关逻辑 而后来维护的同学又来了一次check

当然如果处于数据安全的角度 double check无可厚非,但是如果连锁都没有的double check 其实不做也罢。

毕竟 省一次dbcall 可能效果胜于你做的N多优化

2. 合并业务请求

出发点和上述一致 节省dbcall 但是存在一个矛盾的点 如果业务包在事务里 这条需要慎重考虑 事务的设计原则里 当然能小则小


DB优化

这个其实是比较核心的点

1. 索引优化

这个点比较泛泛 但是做好的人不多 一个专攻于索引优化的人也可以在运维方面独当一面了

我们拿实例来看一个索引优化例子

首先利其器 选中你需要的调试信息

然后打开自动统计信息更新 特别对于测试阶段 数据以及数据量频繁变更的时候 统计信息一定要记得刷新

  创建索引时,查询优化器自动存储有关索引列的统计信息。另外,当 AUTO_CREATE_STATISTICS 数据库选项设置为 ON(默认值)时, 数据库引擎自动为没有用于谓词的索引的列创建统计信息。随着列中数据发生变化,索引和列的统计信息可能会过时,从而导致查询优化器选择的查询处理方法不是最佳的。 当 AUTO_UPDATE_STATISTICS 数据库选项设置为 ON(默认值)时,查询优化器会在表中的数据发生变化时自动定期更新这些统计信息。 每当查询执行计划中使用的统计信息没有通过针对当前统计信息的测试时就会启动统计信息更新。 采样是在各个数据页上随机进行的,取自表或统计信息所需列的最小非聚集索引。从磁盘读取一个数据页后,该数据页上的所有行都被用来更新统计信息。 常规情况是:在大约有 20% 的数据行发生变化时更新统计信息。但是,查询优化器始终确保采样的行数尽量少。 对于小于 8 MB 的表,则始终进行完整扫描来收集统计信息。

最后执行 SET STATISTICS PROFILE ON,可以看到更详细的计划

随便拿个典型sql来作示例 相关值为虚假值 仅供参考

SELECT DISTINCT TOP 1000 a.CustomerID FROM TravelTicket(nolock) a
WHERE  a.TicketChargeDate  999
AND a.[Status] <>999
AND a.IsInCome <>999
AND a.IsInCome <>998 
AND NOT EXISTS 
(SELECT TOP 1 1 FROM TicketCharge(NOLOCK) b 
	WHERE   b.CustomerID = a.CustomerID
                AND b.chargetype = 999
		AND b.IsSuccessful = 999
		AND b.IsDeleted != 999
		AND b.FeeMOnth= \'999\'

)

在完全没有任何索引的前提下我们查询一遍看下各种io信息以及执行计划

如图中所示 存在2个聚集索引扫描 先介绍下基础知识

【Table Scan】:遍历整个表,查找所有匹配的记录行。这个操作将会一行一行的检查,当然,效率也是最差的。
【Index Scan】:根据索引,从表中过滤出来一部分记录,再查找所有匹配的记录行,显然比第一种方式的查找范围要小,因此比【Table Scan】要快。
【Index Seek】:根据索引,定位(获取)记录的存放位置,然后取得记录,因此,比起前二种方式会更快。
  在有聚集索引的表格上,数据是直接存放在索引的最底层的,所以要扫描整个表格里的数据,就要把整个聚集索引扫描一遍。在这里,聚集索引扫描【Clustered Index Scan】就相当于一个表扫描【Table Scan】。所要用的时间和资源与表扫描没有什么差别。并不是说这里有了“Index”这个字样,就说明执行计划比表扫描的有多大进步。当然反过来讲,如果看到“Table Scan”的字样,就说明这个表格上没有聚集索引。换句话说 上面那段sql存在2个表扫描。

【Clustered Index Seek】:直接根据聚集索引获取记录,最快!

所以我们优化的目标是将扫描(scan)变为查找(seek)

  先来尝试TicketCharge表 下面我们所新添并且讨论的索引都是非聚集索引

  江湖上流传着这么一篇秘诀,建符合索引根据where查询的顺序来,好吧我们姑且先尝试一下

/****** Object:  Index [rgyu_test1]    Script Date: 2015-2-2 17:52:50 ******/
CREATE NONCLUSTERED INDEX [chongzi_test] ON [dbo].[TicketCharge]
(
	[CustomerID] ASC,
	[ChargeType] ASC,
	[IsSuccessful] ASC,
	[IsDeleted] ASC,
	[FeeMonth] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, OnLINE= OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
ALTER INDEX [chongzi_test] ON [dbo].[TicketCharge] DISABLE
GO

  再看看新的执行计划

  没变!江湖秘诀果然还是得慎重点用,我们来分析下索引没命中的原因。

第一块是索引基本信息

列名 描述说明

Name

统计信息对象名称

Update

上一次更新统计信息的日期和时间

Rows

在目标索引、统计信息或列中的总行数。如果筛选索引或统计信息,此行数可能小于表的行数。
Rows Sampled 用于统计信息计算的抽样总行数。
Steps 统计信息对象第一个键列的直方图中的值范围数。每个步骤包括在直方图结果中定义的 RANGE_ROWS 和 EQ_ROWS。

Density

查询优化器不使用此值。显示此值的唯一目的是为了向后兼容。密度的计算公式为 1 / distinct rows,其中 distinct rows 是直方图输出中所有步骤的 DISTINCT_RANGE_ROWS 之和。如果对行进行抽样,distinct rows 则基于抽样行的直方图值。
Average Key Length 统计信息对象的键列中,所有抽样值中的每个值的平均字节数
String Index 如果为“是”,则统计信息中包含字符串摘要索引,以支持为 LIKE 条件估算结果集大小。仅当第一个键列的数据类型为charvarcharncharnvarcharvarchar(max)nvarchar(max)textntext 时,才会对此键列创建字符串索引。
Filter Expression 包含在统计信息对象中的表行子集的表达式。NULL = 未筛选的统计信息。有关详细信息,请参阅筛选统计信息。
Unfiltered Rows 应用筛选器表达式前表中的总行数。如果 Filter Expression 为 NULL,Unfiltered Rows 等于行标题值。

 第二块对指定 DENSITY_VECTOR 时结果集中所返回的列进行了说明。

列名 说明
All Density 针对统计信息对象中的列的每个前缀计算密度(1/ distinct_rows)。
Average Length 每个列前缀的列值向量的平均长度(按字节计)。例如,如果列前缀为列 A 和 B,则长度为列 A 和列 B 的字节之和。
Columns 为其显示 All densityAverage length 的前缀中的列的名称。

第三块对指定 HISTOGRAM 选项时结果集中所返回的列进行了说明。

列名 说明
RANGE_HI_KEY 直方图步骤的上限值。

RANGE_ROWS

表中位于直方图步骤内(不包括上限)的行的估算数目。
EQ_ROWS 表中值与直方图步骤的上限值相等的行的估算数目。
DISTINCT_RANGE_ROWS 直方图步骤内(不包括上限)非重复值的估算数目。
AVG_RANGE_ROWS 直方图步骤内(不包括上限)重复值的频率或平均数目(如果 DISTINCT_RANGE_ROWS > 0,则为 RANGE_ROWS / DISTINCT_RANGE_ROWS)。

越小的SQL Server索引密度意味着具有更高的索引选择性。当密度趋近于1,索引就变得有更少的选择性,基本上没有用处了。当索引的选择性低的时候,优化器可能会选择一个表扫描(table scan),或者叶子级的索引扫描(Index scan),而不会进行索引查找(index seek),因为这样会付出更多的代价。当心你的数据库中低选择性的索引。这样的索引通常是对系统的性能是一个损害。它们通常不仅不会用来进行数据的检索,而且也会使得数据修改语句变得缓慢,因为需要额外的索引维护。识别这些索引,考虑删除掉它们。

而上图我们的密度已经达到0.33,因为这不是一个好的方案。我们调整索引顺序,将feemonth提到第一列。

再看执行io和执行计划

 

搞定,我们再看下统计分析

到此为止或许你以为已经搞定了这个索引问题 ticketcharge的读取从1w3降低到了13 但是友情提请一下 密度会随着数据分布的变化而变化 本次的demo数据具有特殊性 具体的问题还需要具体来分析

我们看一下更详细的计划

另外针对本文这种sql写法 还有另外一个点需要关注 那就是索引第一列不可以是于第一张表关联的列

调整我们的索引顺序同样可以让sql命中索引 我们来看下效果

 

 执行一下

 

虽然命中了索引 但是由于关联键的问题 导致ticketcharge进行了循环。

所以说 江湖上流传的按照where查询条件设计索引顺序是完全错误的 第一列的选择要根据密度选择性来判断

另外非聚集索引列分为键值列和包含列(include) 

复合索引的键值列不是越多越好,首先索引本身有长度的限制。但是使用非键值列就不算在索引长度内。键列存储在索引的所有级别中,而非键列仅存储在叶级别中。简单来说本来索引类似于字典的部首查找,你根据部首查找以后还要根据该汉字对应的页数去查看详细,但是如果你只是要一个简单的该汉字的拼音,那么包含列相当于把拼音直接附加在部首查找后面,你就不需要再去详细页查看其它你不需要的信息了。

换个角度来说,当查询中的所有列都作为键列或非键列包含在索引中时,带有包含性非键列的索引可以显著提高查询性能。这样可以实现性能提升,因为查询优化器可以在索引中找到所有列值;不访问表或聚集索引数据,从而减少磁盘 I/O 操作。

至于travelticket的索引设计也类似,不过需要注意的点是不等于运算 以及like运算 都是不能使用索引的。需要结合业务来调整。


除了上述人为的添加索引 还有一种取巧的办法

  打开sql server profiler

  

  按照自己的需求新建一个跟踪脚本

  

  写个脚本循环跑这个语句然后保存跟踪脚本。打开推荐sql优化器

  

  导入刚才的跟踪脚本,并且选择索引优化

  

  执行分析

  

  


数据处理优化

  对于db优化在程序员能力已经到瓶颈的前提下,可以着手从应用程序上的细节出发,例如并行。

  所谓并行也就是多线程针对同任务分区分块协同处理。多线程的技术大家都很了解,这里突出以下线程同步的问题。例如我有1000个人我分10组任务执行,如何正确的保证当前10组任务正确的完成互相不冲突并且等到所有任务完成后才开启下一轮1000.

  这里介绍一个比较通用的方法,首先申明一个信号量队列List();

  取出1000条后对于1000条进行添加顺序标识表示并且模余分组。标识从1开始递增就可以,模余分组方法如下

  testInfos.GroupBy(i => i.index % workTaskCount).Select(g => g.ToList()).ToList();    

  其中index为刚才添加的顺序标识, workTaskCount为分组的任务数。

//循环处理批次任务
foreach (var testGroup in testGroups)
{
    var mre = new ManualResetEvent(false);
    manualEvents.Add(mre);
    var testMethodParam = new TestMethodParam
    {
        mrEvent = mre,
        testGroup = TestGroup,
        testParam = testParam
    };
    //线程池处理计划任务
    ThreadPool.QueueUserWorkItem(DoTestMethod, testMethodParam);
}
if (manualEvents.Count != 0)
{
    //等待所有信号量完成 切记这里最大值为64
    WaitHandle.WaitAll(manualEvents.ToArray(), 30 * 60 * 1000);
}

  DoTestMethod就是旧的任务处理逻辑,testMethodParam负责涵盖你旧逻辑中所需要的参数并且包含一个完成信号量。

  这里需要牢记的是WaitHandle等待的信号量最大值为64.

  如果你需要的任务分组数超过64那么这里推荐在DoTestMethod方法中不适用信号量,而是使用原子操作的标识,例如Interlocked.Increment(taskCount)。当taskCount累加到1000(你设计的当前批次值)就结束一轮。不过比起WaitHandle性能上要慢一些。

  另外如果你使用线程池来管理线程,最好加上最大线程限制。过多的线程是导致cpu资源消耗的原因之一,最好的线程数是服务器cpu个数的2倍-1或者-2,


死锁问题

  锁超时的问题大多是因为表锁产生。解决表锁的问题说难也不难,不过需要牺牲性能。mssql针对主键的更新不会产生表锁而是产生行锁。针对这个问题那么死锁的问题初步解决起来就简单了。

  这是比较通用的处理方法

DECLARE @step INT 
DECLARE @id CHAR(12)
create table #tmpTest  --创建临时表
(
	rec_index INT ,
	id CHAR(12)
);
INSERT INTO #tmpTest  (rec_index,ID) 
SELECT ROW_NUMBER() OVER(ORDER BY ID) AS rec_index,ID FROM TestTable(nolock) WHERE BID = @bID
SET @rowcount=0
SET @step=1
SELECT @rowcount=COUNT(*) FROM #tmpTest AS tt
WHILE(@step<=@rowcount)
BEGIN
	SELECT @id=Id
	FROM #tmpTest  AS tt
	WHERE @step=rec_index
	UPDATE TestTable
    SET
		UpdateUser = @UpdateUser,
		UpdateTime = @UpdateTime,
	WHERE  ID =@id
	SET @step=@step+1
END

cpu飙高小结

对于cpu飙高分2类,应用服务器和db服务器的飙高原因优先排查点不同

对于应用服务器,首先排查while true等死循环,其次看多线程问题。不排除其他原因,这里只介绍主要的情况

在本机就可以根据源代码来调试 

 

如图所示我们的cpu问题是由于线程过得多导致 因为我的demo用的Threadpool所以一个简单的ThreadPool.SetMaxThreads(5, 5);即可搞定.

如果上述情况检查不出你的程序导致cpu飙高的原因,那么就需要借助于其他工具 例如dotTrace.

 

根据自己的程序类型加载不同的探查器

截图不具有典型性 操作很简单 没有什么过多值得介绍的 官方文档

http://www.jetbrains.com/profiler/features/index.html

我们随便找个程序 


 

下面我们看看db服务器cpu飙高的一些原因。

 最大的一个坑是隐式数据类型转换,之所以为坑是因为他不是显示的出现问题,而是当你的数据分布以及量达到一定条件后才会产生问题。

什么是隐式数据类型转换:

当我们在语句的where 条件等式的左右提供了不同数据类型的列或者变量,SQL Server在处理等式之前,将其中一端的数据转换成跟另一端数值的数据类型一致,这个过程叫做隐式数据类型转换。

比如 char(50)=varchar(50), char(50)=nchar(50), int=float, int=char(20) 这些where 条件的等式都会触发隐式数据类型转换。

但是,对于某些数据类型转换过程中,可以转换的方向只是单向的。例如:

如果你试图比较INT和FLOAT的列,INT数据类型必须被转换成FLOAT型 "CONVERT(FLOAT,C_INT) = C_FLOAT".

如果你试图比较char和nchar的列,char数据类型必须被转换成unicode型 "CONVERT(nchar,C_char) = C_nchar"

因此,我们在.net 或者java的程序中,会经常出现由于隐式数据类型转换而产生的性能问题。

最简单的 我们来做个试验

CREATE TABLE [Chongzi_Test] (
[TAB_KEY] [varchar] (5)  NOT NULL ,
[Data] [varchar] (10)  NOT NULL ,
CONSTRAINT [Chongzi_Test_PK] PRIMARY KEY  CLUSTERED 
(
[TAB_KEY]
)  ON [PRIMARY] 
) ON [PRIMARY]
GO

然后插入几百条数据

我们执行

declare @p1 int
set @p1=0
exec sp_prepexec @p1 output,N\'@P0 varchar(5)\',N\'select TAB_KEY,Data from Chongzi_Test where TAB_KEY = @P0\',N\'0\'
select @p1

然后我们换一种不匹配类型来看一下

declare @p1 int
set @p1=0
exec sp_prepexec @p1 output,N\'@P0 nvarchar(4000)\',N\'select TAB_KEY,Data from Chongzi_Test where TAB_KEY = @P0\',N\'0\'
select @p1

这里出现了一个操作叫做GetRangeThroughConvert(),在这里,SQL Server由于不能直接对varchar(5)的列用nvarchar(4000)的值进行seek,因此,SQL Server必须将nvarchar转换成varchar。

这个过程中不同的应用场景可以带来性能的损耗可能会很大,因为在转换过程中可能会存在表扫描。

例如之前项目中有个nvarchar(max)向varchar(50)转换,由于包含有特别的字符,如全双工字符<, 该字符直接转换成varchar(50)不会那么顺利。需要根据参数的实际长度和表结构中定义的字段长度进行比较,如果小于表结构中定义的字段长度,Range依旧会比较小,但是如果大于表结构中定义的字段长度,GetRangeMismatchedTypes函数会把Range设得很宽,查询很慢。

如图执行计划是一样的,返回数据行不同。

 附上c# dbtype 与sql 类型的对应表

AnsiString:VarChar
Binary:VarBinary
Byte:TinyInt
Boolean:Bit
Currency:Money
Date:DateTime
DateTime:DateTime
Decimal:Decimal
Double:Float
Guid:UniqueIdentifier
Int16:SmallInt
Int32:Int
Int64:BigInt
Object:Variant
Single:Real
String:NVarChar
Time:DateTime
AnsiStringFixedLength:Char
StringFixedLength:NChar
Xml:Xml
DateTime2:DateTime2
DateTimeOffset:DateTimeOffset


Crash现象分析

目前还是用老办法 抓dump分析堆栈

使用过程可以参考我之前的博文 http://www.cnblogs.com/dubing/p/3878591.html


推荐阅读
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 基于事件驱动的并发编程及其消息通信机制的同步与异步、阻塞与非阻塞、IO模型的分类
    本文介绍了基于事件驱动的并发编程中的消息通信机制,包括同步和异步的概念及其区别,阻塞和非阻塞的状态,以及IO模型的分类。同步阻塞IO、同步非阻塞IO、异步阻塞IO和异步非阻塞IO等不同的IO模型被详细解释。这些概念和模型对于理解并发编程中的消息通信和IO操作具有重要意义。 ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 本文介绍了在rhel5.5操作系统下搭建网关+LAMP+postfix+dhcp的步骤和配置方法。通过配置dhcp自动分配ip、实现外网访问公司网站、内网收发邮件、内网上网以及SNAT转换等功能。详细介绍了安装dhcp和配置相关文件的步骤,并提供了相关的命令和配置示例。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • PHP设置MySQL字符集的方法及使用mysqli_set_charset函数
    本文介绍了PHP设置MySQL字符集的方法,详细介绍了使用mysqli_set_charset函数来规定与数据库服务器进行数据传送时要使用的字符集。通过示例代码演示了如何设置默认客户端字符集。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
author-avatar
XIE绍雄
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有