由于各种原因我们总是要与公司各种老项目打交道。天有不测风云,谁也不知道这坨屎山会从哪个方向把你的嘴塞的满满的,还不让你吐出来。既然如此...那只能细嚼慢咽的吞下去吧。

说实在话,只要业务不死,那些老大伯项目就还有价值。更何况这个本就没什么人关注的项目突然被公司高层盯住了。说好几个客户都会用到这个系统,并且必须要做好压测工作,不能有任何闪失。

然后这项工作任务就毫无征兆的落在我手上了,改造优化时间不到一周。既然如此,那就只好硬着头皮上了。

项目整体

整个项目很“老”,用的技术栈是 .net4.5 + 多层架构 + sqlsugar + mssql。为什么”老“要加引号呢?因为我很难想象这个项目只是3年前的项目(:摊手)。其中orm——sqlsugar我已经找不到开源的项目地址了(用的仅仅是静态dll),里面有很多写法我都找不到文档了。没关系,又不能不能用,我只要参照之前的写法不动就行了。

那么再来说现在这个项目要进行”手术“的地方:

首当其冲的就是目前这个项目经过测试人员压测,200并发,持续半小时以及100并发,增量并发到200持续1小时的压测结果是......

不到20吞吐量,CPU一直100%。根据目前产品给出的用户量,至少要达到120吞吐量。得到这个消息的我,当时人都麻了......真不夸张。我一度认为我要“死”在这个项目中了。

剖析项目

一边看代码一边骂人的过程就不说了,相信大家都是这么过来的。接下来要做的就是熟悉代码以及代码下的业务场景。涉及优化的业务场景看起来很简单,就是给定一个码,系统接收校验真伪,然后进行激活使用。

在经过非常艰辛的和跟我一样不熟悉这个业务的产品经理沟通下,确定业务方的需求和目的之后,剩下就是真正实施了。

代码层优化

首先我从最简单的开始着手,就是code review。找出能一眼看出问题的点,结果仅仅只是几处f12,就让我找到了”几坨屎“,虽然不愿意,但我还是只能捂着鼻子强迫自己掰开看看究竟。

层与层之间调用关系混乱

因为是多层,所以有BLL,DAL,Model三层。DAL引用ORM组建以及缓存组建,BLL引用DAL。DAL引用DBInstance。在实际查看中,我发现虽然BLL引用DAL,但是除了引用DAL之外,又初始化了DBInstance。缓存组建也是如此。在实际调用中,多次重复打开数据库连接以及缓存连接,这无疑是一笔不小的开销,而且还没有任何意义

看到这个我要做就是优化层之间的调用结构。本着对老项目最小更改原则,我重新建了ActivationBll和ActivationDal文件,去掉多余的对象以及无用的IO连接。

代码逻辑的一把嗦

往下就是具体代码问题了,首先我就在原来的OldActivationBLL文件中看到如下代码:

// OldActivationBll.cs
private List<T1> global_fields1;  //
private List<T2> global_fields2; //
private T3 field3;
...private void InitData(string code) {var dataset = dal.GetInitData(code);global_fields1 = dataset[0];global_fields2 = dataset[1];T3 = dataset[2];...
}public void Activate(string code) {// 略过判断InitData(code);// 引用类全局变量进行各种操作field3.Property1 = ...;...
}

有很多细节我都忽略了,大致就是现在一个类中定义一堆变量,然后在InitData方法中对这些变量一一赋值。这样在其它地方,我都可以任意调用这些变量了。

这种有什么问题呢?其实这种webform式的写法对程序运行结果没太大的影响。只是我个人不喜欢这种编程模式了,因为这样非常容易造就意大利面条式的混乱。让人看的非常头痛,维护起来很苦难。特别是换人之后,因为类全局变量哪里都能被修改,不熟的人很容易导致非预期的结果与错误。

当我正阅读代码并尝试优化这种结果时,发现事情并不是那么简单。

这是dal.GetInitData的代码

// OldActivationDal.cs
public DataSet GetInitData(string code)
{string sql = @"declare @code nvarchar(250)
declare @bid int
declare @aid int
declare @usedId uniqueidentitfier
declare ...
select top 1 * from table1 where code=@code
select @bid = bid, @aid= aid from table1 inner join table2 on ...
select ...
-- 此处省略余下10几行select";var dbset = dbhelper.ExecuteDataSet(sql, new parameter[] { ...});return dbset;
}

看到这里是不是很惊讶,我当时是震惊的。我当时的反应是正常人应该不会这么写吧。这真是“一把嗦”的写法,把所有业务场景用到的前置对象一次性查出来赋值给对应的字段,然后有需要的就引用这些对象。这个方法的引用数是12......。

毫无疑问,这种写法问题很大,因为将多种业务场景的数据一次性查出来,也不管到底用不用得上,这是种对资源的绝对浪费。况且这对于数据库来说也是很大的浪费,因为将多个语句合并成了一个大事务执行

这种优化手段就简单了,就是将一个大事务的sql语句,拆分成多个小事务的sql语句。不偷懒,多写几个方法按需给对象赋值。

这里面还有一个优化点是用到了缓存,在原来十几个sql查询中,还有3个查询语句是基础数据(如渠道以及资源等一些基础数据)。

具体代码错误

前面提到的都还是设计上与流程的问题,还有一些明显的错误就是属于代码的写法错误了。在做了上面的改造措施之后,在我自己的本机做了同样的压测,结果令人尴尬。吞吐量只有100左右。这明显在我的意料之外的,这说明我优化效果不好。然后我继续详细找代码的问题,同时我写了个慢查询语句给db同事查看,让其导出测试同学压测的那个时间段的结果。期间还真让我发现了一些比较明显的问题,如下面的多任务写法:

List<Task> taskList = new List<Task>();
object lockObj = new object();
string[] requestIds = bookId.Split(",");
List<Resource> result = new List<Resource>();
foreach (var id in requestIds) {taskList.Add(Task.Factory.StartNew(delegate() {var resource = _resourceService.GetBookAsync(id).Result;if (resource != null) {lock (lockObj) {result.Add(r);}}}));
}
Task.WaitAll(taskList.ToArray());
return result;

大家来看下这段代码都有哪些问题呢?如何优化呢?这个后面我再给出我实际中的优化方法

数据库方面的优化

找不到其它明显的代码问题就开始着手是不是数据库,sql语句的问题了。

与此同时,db也已经把结果导出给到我了,好家伙,排名第一(最耗时)的就是前面我说的那个十几个查询合并为大事务的那个方法sql语句。紧追其后的就是另一个查询语句,就是查询该用户是否已经使用过该资源。该语句join了多个表,并且关联的表都是百万级数据量的,并且条件很多(有5个),写法如下

select a.Id,a.Code,a.Status,b.Type,a.ChannelId,c.ActivateTypeId,a.Bid,a.UserId,b.Name,d.Did,d.Dtype
from a
inner join b on a.Id = b.Id
inner join c on b.uid = c.uid
left join d on d.Bid = b.Id
where a.UserId = @userId and a.Bid = @bid and a.ChannelId = @channelId and a.Status = 1 and d.DeviceCode = @deviceCode;

看到这个语句的第一想法是什么?

语句有问题?NO,而是检查数据库对应的字段是否有索引,如果没有命中索引,则会导致全表扫描,特别还join的是大表。结果也让我有点失望,索引每个字断都建了。我随即断点将那些条件的值拼成sql语句到线上环境执行,结果发现速度非常慢,足足有15-30秒波动。想了大概几分钟,立马得出了一个结论——索引的问题,给目标字段建立索引针对这种情况效果不大,而是要针对这种热调用场景有针对性的建索引——即联合索引。我给a这个大表建立idx_UserId_Bid_ChannelId_Status的联合索引,然后去掉了无用的字段,这样就减少了要join的表和潜在的回表。建好之后再次执行,只用了300ms左右。

此时压测的结果已经提升到了200左右(真就无脑建索引就完事了!-_-!)。

其实除此之外,还有几个查询也是很慢的。就不细举例了,解决方案除了联合索引,还有一种优化手段是包含列的索引。这种手段常见于select子表join是非常有效果的,其目的是为了减少回表的次数,争取一次查询就能将数据在多叉树的节点上直接返回

总结

自此,完成这些改造手术之后的压测结果在我本机机器上是达到了200多吞吐。算是完成了领导临时交给我的任务吧。在部署到线上时,测试同学压测出来的结果到达了500。不过让我有点意外的是,技术总监还是毅然决定给服务器升配加负载。(小声嘀咕:我还以为可以减配呢)

那么总结这次的性能优化点可以简单的概括三点:

  • 架构层面(即分层要明确,减少重复的对象构造)

  • 代码层面(减少明显的编程常识错误,如尽量避免多任务共享变量;还有不要偷懒...)

  • 数据库层面(不要执行大的sql语句,要将大的拆成多个小事务sql语句,建对索引会省很多事)

关于具体实施,特别对手是老项目时,一定要本着“能不改原来的代码就不改为第一定律”。把这些老酒用新瓶包装起来。因为你永远也不知道你改动了其中一处地方,会给项目造成多大的伤害。

最后

在结束本文之前,我给出之前代码的优化版本。在优化之前我们先清楚代码有问题。

很明显的有两个问题:

  1. 多任务并行调用异步方法,在遍历中共享了result对象,并通过上锁添加方法返回的结果

  2. 直接调用了异步方法GetBookAsync.Result

这两点碰到一起了,这让本不富裕的服务器资源更是雪上加霜。

下面是我优化的版本

string[] requestIds = bookId.Split(",");
var taskList = new Task[requestIds.Length];
var result = new Resource[requestIds.Length];
for (int i = 0; i < requestIds.Length; i++) {var idx = i;taskList[idx] = Task.Run(() => {}).ContinueWith(t => {result[idx] = t.Result;});
}
Task.WaitAll(taskList);
return result.ToList();

这是我想到的优化的版本,这样既能做到无锁编程,又可以不用阻塞异步方法。硬要说其它的问题的话,那就是requestIds的数量是潜在的问题点,因为数量非常多的时候,这个时候就会给系统带来很大的负担,最终也会引起API服务或数据库宕机的情况。这个时候其实我们可以通过PLINQ解决这点,通过分区来取得最佳性能。

好了这篇文章就到这里了。

.NET遗留应用改造——性能优化篇相关推荐

  1. 秋色园QBlog技术原理解析:性能优化篇:用户和文章计数器方案(十七)

    2019独角兽企业重金招聘Python工程师标准>>> 上节概要: 上节 秋色园QBlog技术原理解析:性能优化篇:access的并发极限及分库分散并发方案(十六)  中, 介绍了 ...

  2. 秋色园QBlog技术原理解析:性能优化篇:数据库文章表分表及分库减压方案(十五)...

    文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文件的作用 2: 秋色园QBlog技术原理解析:认识整站处理流程(二) --介绍秋色园业务处理流程 3: 秋色 ...

  3. C语言嵌入式系统编程修炼之道——性能优化篇

    C语言嵌入式系统编程修炼之道--性能优化篇 作者:宋宝华  e-mail:[email]21cnbao@21cn.com[/email] 1.使用宏定义 在C语言中,宏是产生内嵌代码的唯一方法.对于嵌 ...

  4. 在网上看到和篇关于sql server 2005的性能优化篇,觉得写得很好。

    在网上看到和篇关于sql server 2005的性能优化篇,觉得写得很好. SQL Server2005扩展函数已经不是一件什么新鲜的事了,但是我看网上的大部分都是说聚合函数,例子也比较浅,那么这里 ...

  5. 秋色园QBlog技术原理解析:性能优化篇:打印页面SQL,全局的SQL语句优化(十三)...

    文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文件的作用 2: 秋色园QBlog技术原理解析:认识整站处理流程(二) --介绍秋色园业务处理流程 3: 秋色 ...

  6. Android进阶:性能优化篇 Android进阶:性能优化篇

    Android进阶:性能优化篇 分类:Android 性能优化2011-08-09 17:06585人阅读评论(0)收藏举报 一.在使用Gallery控件时,如果载入的图片过多,过大,就很容易出现Ou ...

  7. 秋色园QBlog技术原理解析:性能优化篇:access的并发极限及超级分库分散并发方案(十六)...

    上节回顾: 上节 秋色园QBlog技术原理解析:性能优化篇:数据库文章表分表及分库减压方案(十五) 中, 介绍了 秋色园QBlog 在性能优化方面,从技术的优化手段,开始步入数据库设计优化,并从数据的 ...

  8. 我在大厂写React学到了什么?性能优化篇

    前言 我工作中的技术栈主要是 React + TypeScript,这篇文章我想总结一下如何在项目中运用 React 的一些技巧去进行性能优化,或者更好的代码组织. 性能优化的重要性不用多说,谷歌发布 ...

  9. C++ 性能优化篇二《影响优化的计算机行为》

    撒谎,即讲述美丽而不真实的故事,乃是艺术的真正目的. ​ --奥斯卡 • 王尔德,"谎言的衰朽",<意图集>,1891 年 本篇的目的是为大家提供与优化技术相关的计算机 ...

最新文章

  1. 73. 解决ExtJS TreePanel 的 iconCls设置问题
  2. 【Bootloader】探究bootloader,分析u-boot源码
  3. pyqt5从子目录加载qrc文件_实战PyQt5: 045-添加资源文件
  4. ubuntu14.04下安装qt4.8.6 +qt creator
  5. algorithm -- 选择排序
  6. 织梦task_do.php,织梦20160906更新后栏目空白问题
  7. zend studio mysql_Zend Studio的一些常用配置和使用帮助手册
  8. 如何将外链接向内连接转换?
  9. 内置模块(time、random、hashlib、os)
  10. SENT (Single Edge Nibble Transmission) 协议 接口
  11. 单径瑞利信道中的BPSK相干解调的(理论)误码率性能
  12. iis6 元数据库与iis6 配置的兼容 出错问题
  13. (二)java项目中的文档转换案例实战——PDF转换为JPG图片压缩包
  14. Matlab机器人工具箱(Robotics Toolbox)学习笔记
  15. 错误处理panic和recover
  16. Linux用户家目录与根目录
  17. 【web前端】20.手机端网页禁止长按图片保存图片
  18. Android Sunflower 带您玩转 Jetpack
  19. 泰坦尼克号生还者预测
  20. 多商家父订单子订单_70多份订单被退回,商家查看信息傻眼了,美团:封店180天...

热门文章

  1. Oracle interview
  2. java获取apk启动activity_兼容 Android 10 启动 APK 实现方案
  3. 绑定dictionary 给定关键字不再字典中_VBA代码集锦-利用字典做两列数据的对比并对齐...
  4. 用 git 同步 Colab 与 Gitlab、Github 之间的文件
  5. Linux操作系统备份之二:通过tar拷贝分区实现Linux操作数据的在线备份
  6. 浏览器要是能这么做就好了
  7. HTML标题h,HTML H标题标签
  8. java 最后的异常_java – 最后不要抛出堆栈溢出异常
  9. 火狐ok谷歌适配_“ OK Google”在锁定手机上的安全性越来越高
  10. java发送gmail_如何在Gmail中轻松通过电子邮件发送人群