.NET遗留应用改造——性能优化篇
由于各種原因我們總是要與公司各種老項(xiàng)目打交道。天有不測風(fēng)云,誰也不知道這坨屎山會從哪個方向把你的嘴塞的滿滿的,還不讓你吐出來。既然如此...那只能細(xì)嚼慢咽的吞下去吧。
說實(shí)在話,只要業(yè)務(wù)不死,那些老大伯項(xiàng)目就還有價值。更何況這個本就沒什么人關(guān)注的項(xiàng)目突然被公司高層盯住了。說好幾個客戶都會用到這個系統(tǒng),并且必須要做好壓測工作,不能有任何閃失。
然后這項(xiàng)工作任務(wù)就毫無征兆的落在我手上了,改造優(yōu)化時間不到一周。既然如此,那就只好硬著頭皮上了。
項(xiàng)目整體
整個項(xiàng)目很“老”,用的技術(shù)棧是 .net4.5 + 多層架構(gòu) + sqlsugar + mssql。為什么”老“要加引號呢?因?yàn)槲液茈y想象這個項(xiàng)目只是3年前的項(xiàng)目(:攤手)。其中orm——sqlsugar我已經(jīng)找不到開源的項(xiàng)目地址了(用的僅僅是靜態(tài)dll),里面有很多寫法我都找不到文檔了。沒關(guān)系,又不能不能用,我只要參照之前的寫法不動就行了。
那么再來說現(xiàn)在這個項(xiàng)目要進(jìn)行”手術(shù)“的地方:
首當(dāng)其沖的就是目前這個項(xiàng)目經(jīng)過測試人員壓測,200并發(fā),持續(xù)半小時以及100并發(fā),增量并發(fā)到200持續(xù)1小時的壓測結(jié)果是......
不到20吞吐量,CPU一直100%。根據(jù)目前產(chǎn)品給出的用戶量,至少要達(dá)到120吞吐量。得到這個消息的我,當(dāng)時人都麻了......真不夸張。我一度認(rèn)為我要“死”在這個項(xiàng)目中了。
剖析項(xiàng)目
一邊看代碼一邊罵人的過程就不說了,相信大家都是這么過來的。接下來要做的就是熟悉代碼以及代碼下的業(yè)務(wù)場景。涉及優(yōu)化的業(yè)務(wù)場景看起來很簡單,就是給定一個碼,系統(tǒng)接收校驗(yàn)真?zhèn)?#xff0c;然后進(jìn)行激活使用。
在經(jīng)過非常艱辛的和跟我一樣不熟悉這個業(yè)務(wù)的產(chǎn)品經(jīng)理溝通下,確定業(yè)務(wù)方的需求和目的之后,剩下就是真正實(shí)施了。
代碼層優(yōu)化
首先我從最簡單的開始著手,就是code review。找出能一眼看出問題的點(diǎn),結(jié)果僅僅只是幾處f12,就讓我找到了”幾坨屎“,雖然不愿意,但我還是只能捂著鼻子強(qiáng)迫自己掰開看看究竟。
層與層之間調(diào)用關(guān)系混亂
因?yàn)槭嵌鄬?#xff0c;所以有BLL,DAL,Model三層。DAL引用ORM組建以及緩存組建,BLL引用DAL。DAL引用DBInstance。在實(shí)際查看中,我發(fā)現(xiàn)雖然BLL引用DAL,但是除了引用DAL之外,又初始化了DBInstance。緩存組建也是如此。在實(shí)際調(diào)用中,多次重復(fù)打開數(shù)據(jù)庫連接以及緩存連接,這無疑是一筆不小的開銷,而且還沒有任何意義。
看到這個我要做就是優(yōu)化層之間的調(diào)用結(jié)構(gòu)。本著對老項(xiàng)目最小更改原則,我重新建了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);// 引用類全局變量進(jìn)行各種操作field3.Property1 = ...;... }有很多細(xì)節(jié)我都忽略了,大致就是現(xiàn)在一個類中定義一堆變量,然后在InitData方法中對這些變量一一賦值。這樣在其它地方,我都可以任意調(diào)用這些變量了。
這種有什么問題呢?其實(shí)這種webform式的寫法對程序運(yùn)行結(jié)果沒太大的影響。只是我個人不喜歡這種編程模式了,因?yàn)檫@樣非常容易造就意大利面條式的混亂。讓人看的非常頭痛,維護(hù)起來很苦難。特別是換人之后,因?yàn)轭惾肿兞磕睦锒寄鼙恍薷?#xff0c;不熟的人很容易導(dǎo)致非預(yù)期的結(jié)果與錯誤。
當(dāng)我正閱讀代碼并嘗試優(yōu)化這種結(jié)果時,發(fā)現(xiàn)事情并不是那么簡單。
這是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; }看到這里是不是很驚訝,我當(dāng)時是震驚的。我當(dāng)時的反應(yīng)是正常人應(yīng)該不會這么寫吧。這真是“一把嗦”的寫法,把所有業(yè)務(wù)場景用到的前置對象一次性查出來賦值給對應(yīng)的字段,然后有需要的就引用這些對象。這個方法的引用數(shù)是12......。
毫無疑問,這種寫法問題很大,因?yàn)閷⒍喾N業(yè)務(wù)場景的數(shù)據(jù)一次性查出來,也不管到底用不用得上,這是種對資源的絕對浪費(fèi)。況且這對于數(shù)據(jù)庫來說也是很大的浪費(fèi),因?yàn)閷⒍鄠€語句合并成了一個大事務(wù)執(zhí)行。
這種優(yōu)化手段就簡單了,就是將一個大事務(wù)的sql語句,拆分成多個小事務(wù)的sql語句。不偷懶,多寫幾個方法按需給對象賦值。
這里面還有一個優(yōu)化點(diǎn)是用到了緩存,在原來十幾個sql查詢中,還有3個查詢語句是基礎(chǔ)數(shù)據(jù)(如渠道以及資源等一些基礎(chǔ)數(shù)據(jù))。
具體代碼錯誤
前面提到的都還是設(shè)計(jì)上與流程的問題,還有一些明顯的錯誤就是屬于代碼的寫法錯誤了。在做了上面的改造措施之后,在我自己的本機(jī)做了同樣的壓測,結(jié)果令人尷尬。吞吐量只有100左右。這明顯在我的意料之外的,這說明我優(yōu)化效果不好。然后我繼續(xù)詳細(xì)找代碼的問題,同時我寫了個慢查詢語句給db同事查看,讓其導(dǎo)出測試同學(xué)壓測的那個時間段的結(jié)果。期間還真讓我發(fā)現(xiàn)了一些比較明顯的問題,如下面的多任務(wù)寫法:
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;大家來看下這段代碼都有哪些問題呢?如何優(yōu)化呢?這個后面我再給出我實(shí)際中的優(yōu)化方法
數(shù)據(jù)庫方面的優(yōu)化
找不到其它明顯的代碼問題就開始著手是不是數(shù)據(jù)庫,sql語句的問題了。
與此同時,db也已經(jīng)把結(jié)果導(dǎo)出給到我了,好家伙,排名第一(最耗時)的就是前面我說的那個十幾個查詢合并為大事務(wù)的那個方法sql語句。緊追其后的就是另一個查詢語句,就是查詢該用戶是否已經(jīng)使用過該資源。該語句join了多個表,并且關(guān)聯(lián)的表都是百萬級數(shù)據(jù)量的,并且條件很多(有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,而是檢查數(shù)據(jù)庫對應(yīng)的字段是否有索引,如果沒有命中索引,則會導(dǎo)致全表掃描,特別還join的是大表。結(jié)果也讓我有點(diǎn)失望,索引每個字?jǐn)喽冀恕N译S即斷點(diǎn)將那些條件的值拼成sql語句到線上環(huán)境執(zhí)行,結(jié)果發(fā)現(xiàn)速度非常慢,足足有15-30秒波動。想了大概幾分鐘,立馬得出了一個結(jié)論——索引的問題,給目標(biāo)字段建立索引針對這種情況效果不大,而是要針對這種熱調(diào)用場景有針對性的建索引——即聯(lián)合索引。我給a這個大表建立idx_UserId_Bid_ChannelId_Status的聯(lián)合索引,然后去掉了無用的字段,這樣就減少了要join的表和潛在的回表。建好之后再次執(zhí)行,只用了300ms左右。
此時壓測的結(jié)果已經(jīng)提升到了200左右(真就無腦建索引就完事了!-_-!)。
其實(shí)除此之外,還有幾個查詢也是很慢的。就不細(xì)舉例了,解決方案除了聯(lián)合索引,還有一種優(yōu)化手段是包含列的索引。這種手段常見于select子表join是非常有效果的,其目的是為了減少回表的次數(shù),爭取一次查詢就能將數(shù)據(jù)在多叉樹的節(jié)點(diǎn)上直接返回。
總結(jié)
自此,完成這些改造手術(shù)之后的壓測結(jié)果在我本機(jī)機(jī)器上是達(dá)到了200多吞吐。算是完成了領(lǐng)導(dǎo)臨時交給我的任務(wù)吧。在部署到線上時,測試同學(xué)壓測出來的結(jié)果到達(dá)了500。不過讓我有點(diǎn)意外的是,技術(shù)總監(jiān)還是毅然決定給服務(wù)器升配加負(fù)載。(小聲嘀咕:我還以為可以減配呢)
那么總結(jié)這次的性能優(yōu)化點(diǎn)可以簡單的概括三點(diǎn):
架構(gòu)層面(即分層要明確,減少重復(fù)的對象構(gòu)造)
代碼層面(減少明顯的編程常識錯誤,如盡量避免多任務(wù)共享變量;還有不要偷懶...)
數(shù)據(jù)庫層面(不要執(zhí)行大的sql語句,要將大的拆成多個小事務(wù)sql語句,建對索引會省很多事)
關(guān)于具體實(shí)施,特別對手是老項(xiàng)目時,一定要本著“能不改原來的代碼就不改為第一定律”。把這些老酒用新瓶包裝起來。因?yàn)槟阌肋h(yuǎn)也不知道你改動了其中一處地方,會給項(xiàng)目造成多大的傷害。
最后
在結(jié)束本文之前,我給出之前代碼的優(yōu)化版本。在優(yōu)化之前我們先清楚代碼有問題。
很明顯的有兩個問題:
多任務(wù)并行調(diào)用異步方法,在遍歷中共享了result對象,并通過上鎖添加方法返回的結(jié)果
直接調(diào)用了異步方法GetBookAsync.Result
這兩點(diǎn)碰到一起了,這讓本不富裕的服務(wù)器資源更是雪上加霜。
下面是我優(yōu)化的版本
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();這是我想到的優(yōu)化的版本,這樣既能做到無鎖編程,又可以不用阻塞異步方法。硬要說其它的問題的話,那就是requestIds的數(shù)量是潛在的問題點(diǎn),因?yàn)閿?shù)量非常多的時候,這個時候就會給系統(tǒng)帶來很大的負(fù)擔(dān),最終也會引起API服務(wù)或數(shù)據(jù)庫宕機(jī)的情況。這個時候其實(shí)我們可以通過PLINQ解決這點(diǎn),通過分區(qū)來取得最佳性能。
好了這篇文章就到這里了。
總結(jié)
以上是生活随笔為你收集整理的.NET遗留应用改造——性能优化篇的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: FreeBSD大败局
- 下一篇: C# 11 预览,又增加了实用的语法糖