.NET遗留应用改造——性能优化篇

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

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

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

项目整体

整个项目很“老”,用的技术栈是 .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解决这点,通过分区来取得最佳性能。

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

c2f8fc6aa89a5b638518ec36b8a4630e.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/290950.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

华为西安工业大学鸿蒙,培养百位将领、19位院士,这所211大学被誉为“华为人的母校”...

在社会上&#xff0c;985工程重点大学认可度是普遍要比211工程大学高的&#xff0c;并且985工程大学实力也比较强。但比较例外的是&#xff0c;华为这个让国人骄傲的企业&#xff0c;对一个211工程大学青睐程度却超过了绝大部分985工程大学&#xff0c;这所211大学甚至还被誉为…

Android Studio之Instant Run requires ‘Tools | Android | Enable ADB integration‘ to be enabled解决办法

1、问题 Android studio运行项目时候出现这个提示 Instant Run requires Tools | Android | Enable ADB integration to be enabled 2、原因 由于新版本中的Instant Run &#xff08;即时运行&#xff09;引起的 官方介绍 即使运行 https://developer.android.com/studio/r…

C# 11 预览,又增加了实用的语法糖

文 | 罗奇奇出品 | OSC开源社区&#xff08;ID&#xff1a;oschina2013&#xff09;.NET 首席项目经理凯瑟琳在博客中介绍了 C# 11 的一些预览性新功能&#xff0c;这些功能可以在 Visual Studio 17.1 和 .NET SDK 6.0.200 中体验&#xff0c;下面摘录一部分新特性作介绍&#…

java程序

This XML file does not appear to have any style information associated with it. The document tree is shown below.博客园_首页代码改变世界uuid:5de59c50-a92f-4447-96ed-ab86451ed183;id61182014-07-27T11:59:08Zfeed.cnblogs.comhttp://www.cnblogs.com/jianyus/p/386…

使用Blazor做个简单的时间戳在线转换工具

时间戳转换时间戳转换&#xff0c;关键点在于双向绑定bind-Value&#xff0c;就简单贴源码吧TimestampTool.razorpage "/timestamp" using BlazorComponent.I18n layout PublicLayout<PageTitle>T("TimestampToolTitle")</PageTitle><h2 st…

转载:Pixhawk源码笔记一:APM代码基本结构

转自 新浪微博WalkAnt 基础知识 详细参考&#xff1a;http://dev.ardupilot.com/wiki/learning-the-ardupilot-codebase/ 第一部分&#xff1a;介绍 详细参考&#xff1a;http://dev.ardupilot.com/wiki/learning-ardupilot-introduction/ ArduPilot 代码分为5个主要部分&…

nginx+tomcat8+memcached实现session共享具体操作

本次试验用到的软件包的版本如下&#xff1a;一、针对10.43.2.134的操作1.安装jdk环境tar zxf jdk-8u5-linux-x64.tar.gz mkdir /usr/javamv jdk1.8.0_05/ /usr/java/编辑/etc/profile在文档的末尾追加如下5行内容&#xff1a;JAVA_HOME/usr/java/jdk1.8.0_05JRE_HOME/usr/java…

.NET6之MiniAPI(十九):NLog

在本系例文章的第八篇中&#xff0c;我们聊过官方的日志实现&#xff0c;即《.NET6之MiniAPI(八)&#xff1a;日志》。但官方的日志功能更多是提供了一个实现基础&#xff0c;对于要求一个灵活&#xff0c;强大&#xff0c;方便的日志体系&#xff0c;官方的还是有差距的&#…

完美:C# Blazor中显示Markdown并添加代码高亮

昨天发了一篇介绍这个库&#xff1a;C# Blazor中显示Markdown文件&#xff0c;介绍怎么在Blazor中显示Markdown内容的文章&#xff0c;文章内的代码是没有高亮的&#xff0c;思来相去&#xff0c;还是要做好&#xff0c;于是百度到这篇文章.NET C# Blazor 服务端渲染Markdown&a…

使用 FieldMask 提高 C# gRpc 服务性能

前言想象一下&#xff0c;有一个服务提供个多个客户端调用&#xff0c;但不是所有客户端都需要全部的返回参数:比如商品列表服务返回商品的所有信息&#xff0c;而订单服务调用商品列表服务&#xff0c;但它其实只需要商品的编码和名称就够了。当然&#xff0c;我们可以为这个需…

HTML accesskey 属性

2019独角兽企业重金招聘Python工程师标准>>> 带有指定快捷键的超链接&#xff1a; <a href"http://www.w3school.com.cn/html/" accesskey"h">HTML</a><br /> <a href"http://www.w3school.com.cn/css/" access…

Linux 2.6 完全公平调度算法CFS(Completely Fair Scheduler) 分析

转会http://www.ibm.com/developerworks/cn/linux/l-completely-fair-scheduler/index.html?cadrs-cn-0125 Linux 调度器简史 早期的 Linux 调度器使用了最低的设计&#xff0c;它显然不关注具有非常多处理器的大型架构&#xff0c;更不用说是超线程了。1.2 Linux 调度器使用了…

在计算机技术方面用英语怎么说,“计算机应用技术”用英语怎么说?

计算机应用技术 :1. Computer Applications Technology中国搜学网-学苑论坛 - 教育话题 - 学习交流 ... ...专利文献检索 The Searches of Patent Literature计算机应用技术 Computer Applications Technology精密电磁测量 Precise Electromagnetic Measurement ...2. Computer…

linux之nautilus .命令浏览当前文件目录

1、比如我们在终端目前想打开这个路径下的文件目录&#xff0c;我们如果再去更加路径一个一个的打&#xff0c;就太浪费时间了&#xff0c;我们可以在终端这个路径下输入下面的命令就可以 ~/Desktop$ nautilus . 2、效果如下图

C# 异步与Windows应用程序

把 async 关键字用于 UWP 应用程序&#xff0c;需要注意&#xff0c;在 UI 线程中调用 await 之后&#xff0c;当异步方法返回时&#xff0c;将默认返回到 UI 线程中。这便于在异步方法完成后更新 UI 元素。注意为了创建 UWP 应用程序&#xff0c;需要 Windows 10&#xff0c;W…

重装系统后恢复oracle数据

2019独角兽企业重金招聘Python工程师标准>>> 由于前段时间重装了系统&#xff0c;今天重装了数据库oracle XE版本&#xff0c;用“移花接木”的手段将新装oracle的目录用原有目录直接给覆盖&#xff0c;于是顺利的启动了oracle服务&#xff0c;然后又打开plsql deve…

java Split 用法

在java.lang包中有String.split()方法,返回是一个数组 我在应用中用到一些,给大家总结一下,仅供大家参考: 1、如果用“.”作为分隔的话,必须是如下写法,String.split("\\."),这样才能正确的分隔开,不能用String.split("."); 2、如果用“|”作为分隔的话,必…

html三列布局源码,HTML三列布局 - 黄柳淞的个人页面 - OSCHINA - 中文开源技术交流社区...

1.三列布局1三列布局body{padding: 0;margin: 0;}.left{width:33.3%;height: 500px;background-color: #ccc;float: left;}.middle{width:33.3%;height: 500px;background-color: #bbb;float: left;}.right{width:33.3%;height: 500px;background-color: #aaa;float: left;}33.…

nagios的搭建及配置----(中)

上篇文章写到了我们布置的nagios监控本机的状态&#xff0c;下面我们来看下nagios监控服务器状态的配置。至于为什么要这么配置&#xff0c;大家可以自行去百度下看看使用NRPE插件实现对remote server 进行监控&#xff0c;远程服务器配置1&#xff0c;编译安装nagios-plugin&a…

iNeuOS工业互联网操作系统,顺利从NetCore3.1升级到Net6的过程汇报,发布3.7版本...

目 录1..... 概述... 12..... 升级的工程... 33..... 升级的代码差异... 44..... 系统运行... 51. 概述我们团队一直在工业领域从事实时系统和业务系统的开发与集成工作&#xff0c;开发语言使用过C51/C/VB/VC/C#(Net Framework)/C#(Netcore3.1)/C#(NET6)。前几天微软发…