本文目錄如下:
2、2 后端優化
3、總結
最近看了很多關於系統性能調優的文章,發現很多文章都是介紹某一方面的,例如專門數據庫方面的優化、前端頁面的優化等等都不是很全面,這里結合我在工作中的一些實踐對系統性能調優技術來一個綜合性的分享。
如上圖,性能就是吞吐量加延遲,這兩個相互矛盾又相互協調構成了一個系統性能的定義:
- Throughput ,吞吐量。也就是每秒鍾可以處理的請求數,任務數。
- Latency, 系統延遲。也就是系統在處理一個請求或一個任務時的延遲。
一般來說,一個系統的性能受到這兩個條件的約束,缺一不可。比如,我的系統可以頂得住一百萬的並發,但是系統的延遲是2分鍾以上,那么,這個一百萬的負載毫無意義。系統延遲很短,但是吞吐量很低,同樣沒有意義。所以,一個好的系統的性能測試必然受到這兩個條件的同時作用。 有經驗的朋友一定知道,這兩個東西的一些關系:
- Throughput越大,Latency會越差。因為請求量過大,系統太繁忙,所以響應速度自然會低。
- Latency越好,能支持的Throughput就會越高。因為Latency短說明處理速度快,於是就可以處理更多的請求。
本文的目的是通過講解系統性能讓大家在后續的工作中能夠帶着產品化的思路去優化自己的代碼包括前后台、數據庫等,自測過程中我們可以利用壓力性能測試pylot、Fiddler、單元測試等工具去發現系統的問題從而去優化提高系統的質量,這樣通過團隊的配合和努力來提高增強用戶的體驗從而提高我們公司的競爭力!
以下性能優化技術需要我們在自己工作過程中不斷積累和總結,在工作中配合一些專業的測試工具去發現性能的瓶頸,這里把性能優化技術分為兩塊分別是前端和后端的優化。
2.1.1 負載均衡
通過DNS的負載均衡器(一般在路由器上根據路由的負載重定向)可以把用戶的訪問均勻地分散在多個Web服務器上。這樣可以減少Web服務器的請求負載。因為http的請求都是短作業,所以,可以通過很簡單的負載均衡器來完成這一功能。最好是有CDN網絡讓用戶連接與其最近的服務器(CDN通常伴隨着分布式存儲)。
CDN的全稱是Content Delivery Network,即內容分發網絡。其基本思路是盡可能避開互聯網上有可能影響數據傳輸速度和穩定性的瓶頸和環節,使內容傳輸的更快、更穩定。
CDN的通俗理解就是網站加速,可以解決跨運營商,跨地區,服務器負載能力過低,帶寬過少等帶來的網站打開速度慢等問題。
CDN的特點和優勢:
1、本地Cache加速 提高了企業站點(尤其含有大量圖片和靜態頁面站點)的訪問速度,並大大提高以上性質站點的穩定性
2、鏡像服務 消除了不同運營商之間互聯的瓶頸造成的影響,實現了跨運營商的網絡加速,保證不同網絡中的用戶都能得到良好的訪問質量。
3、遠程加速 遠程訪問用戶根據DNS負載均衡技術智能自動選擇Cache服務器,選擇最快的Cache服務器,加快遠程訪問的速度
4、帶寬優化 自動生成服務器的遠程Mirror(鏡像)cache服務器,遠程用戶訪問時從cache服務器上讀取數據,減少遠程訪問的帶寬、分擔網絡流量、減輕原站點WEB服務器負載等功能。
2.1.2 減少請求和2.1.3縮減網頁
減少請求數
(1)系統某個頁面的加載往往伴隨着多個請求的發生,請求越多吞吐量越大,延遲就會變大,這里就要考慮優化請求數了,我們可以使用Fiddler等工具查看某個網頁的請求數,如下圖,如果我們的一個網頁引用了很多樣式和js,例如一個頁面引用了10個css和10個js,那么我們應該考慮把某些樣式和js合並起來;
(2)Css Sprites:有很多圖片我們其實可以用一張圖片來代替的,一般需要跟美工或UI設計器配合一起來做的,美工或UI設計師去設計出來之后告訴我們圖片中具體元素的位置或者封裝在css中,研發這邊直接調用即可。
異步
系統某個頁面中如果有一個請求的響應超過0.5秒以上或者請求的響應量大於300KB的話我們應該考慮進行異步請求,還有就是一些服務的調用這些盡量不要用同步,一阻塞整個網站的體驗會非常差;
CSS/JS壓縮
可以借助一些開源的壓縮工具,像開源的yuicompressor,發布或發包時把js和css都壓縮一下,這樣js和css文件就會非常小了;
GZIP壓縮
使用GZIP壓縮可以降低服務器發送的字節數,能讓客戶感覺到網頁的速度更 快也減少了對帶寬的使用情況;
IIS里面也可以設置GZIP壓縮,可以壓縮應用程序文件和靜態文件,具體百度。
精簡代碼
最高效的程序就是不執行任何代碼的程序,所以,代碼越少性能就越高。關於代碼級優化的技術大學里的教科書有很多示例了。如:減少循環的層數,減少遞歸,在循環中少聲明變量,少做分配和釋放內存的操作,盡量把循環體內的表達式抽到循環外,條件表達的中的多個條件判斷的次序,盡量在程序啟動時把一些東西准備好,注意函數調用的開銷(棧上開銷),注意面向對象語言中臨時對象的開銷,小心使用異常。
開源框架
現在開源的好東西太多了,關鍵是你要有一雙慧眼,向大家推薦開源中國社區、github、codeplex,我發現現在比較厲害的開發者就是一個很牛逼的模仿者,消化掉成為自己的其實就是一種創新;
2.1.4 優化查詢
(1)SQL語句的優化
關於SQL語句的優化,首先也是要使用工具,比如:MySQL SQL Query Analyzer,Oracle SQL Performance Analyzer,或是微軟SQL Query Analyzer,基本上來說,所有的RMDB都會有這樣的工具,來讓你查看你的應用中的SQL的性能問題。 還可以使用explain來看看SQL語句最終Execution Plan會是什么樣的。
還有一點很重要,數據庫的各種操作需要大量的內存,所以服務器的內存要夠,優其應對那些多表查詢的SQL語句,那是相當的耗內存。
下面我根據我有限的數據庫SQL的知識說幾個會有性能問題的SQL:
- 全表檢索。比如:select * from user where lastname = “xxxx”,這樣的SQL語句基本上是全表查找,線性復雜度O(n),記錄數越多,性能也越差(如:100條記錄的查找要50ms,一百萬條記錄需要5分鍾)。對於這種情況,我們可以有兩種方法提高性能:一種方法是分表,把記錄數降下來,另一種方法是建索引(為lastname建索引)。索引就像是key-value的數據結構一樣,key就是where后面的字段,value就是物理行號,對索引的搜索復雜度是基本上是O(log(n)) ——用B-Tree實現索引(如:100條記錄的查找要50ms,一百萬條記錄需要100ms)。
- 索引。對於索引字段,最好不要在字段上做計算、類型轉換、函數、空值判斷、字段連接操作,這些操作都會破壞索引原本的性能。當然,索引一般都出現在Where或是Order by字句中,所以對Where和Order by子句中的子段最好不要進行計算操作,或是加上什么NOT之類的,或是使用什么函數。
- 多表查詢。關系型數據庫最多的操作就是多表查詢,多表查詢主要有三個關鍵字,EXISTS,IN和JOIN(關於各種join,可以參看圖解SQL的Join一文)。基本來說,現代的數據引擎對SQL語句優化得都挺好的,JOIN和IN/EXISTS在結果上有些不同,但性能基本上都差不多。有人說,EXISTS的性能要好於IN,IN的性能要好於JOIN,我各人覺得,這個還要看你的數據、schema和SQL語句的復雜度,對於一般的簡單的情況來說,都差不多,所以千萬不要使用過多的嵌套,千萬不要讓你的SQL太復雜,寧可使用幾個簡單的SQL也不要使用一個巨大無比的嵌套N級的SQL。還有人說,如果兩個表的數據量差不多,Exists的性能可能會高於In,In可能會高於Join,如果這兩個表一大一小,那么子查詢中,Exists用大表,In則用小表。這個,我沒有驗證過,放在這里讓大家討論吧。另,有一篇關於SQL Server的文章大家可以看看《IN vs JOIN vs EXISTS》
- JOIN操作。有人說,Join表的順序會影響性能,只要Join的結果集是一樣,性能和join的次序無關。因為后台的數據庫引擎會幫我們優化的。Join有三種實現算法,嵌套循環,排序歸並,和Hash式的Join。(MySQL只支持第一種)
- 嵌套循環,就好像是我們常見的多重嵌套循環。注意,前面的索引說過,數據庫的索引查找算法用的是B-Tree,這是O(log(n))的算法,所以,整個算法復法度應該是O(log(n)) * O(log(m)) 這樣的。
- Hash式的Join,主要解決嵌套循環的O(log(n))的復雜,使用一個臨時的hash表來標記。
- 排序歸並,意思是兩個表按照查詢字段排好序,然后再合並。當然,索引字段一般是排好序的。
還是那句話,具體要看什么樣的數據,什么樣的SQL語句,你才知道用哪種方法是最好的。
- 部分結果集。我們知道MySQL里的Limit關鍵字,Oracle里的rownum,SQL Server里的Top都是在限制前幾條的返回結果。這給了我們數據庫引擎很多可以調優的空間。一般來說,返回top n的記錄數據需要我們使用order by,注意在這里我們需要為order by的字段建立索引。有了被建索引的order by后,會讓我們的select語句的性能不會被記錄數的所影響。使用這個技術,一般來說我們前台會以分頁方式來顯現數據,Mysql用的是OFFSET,SQL Server用的是FETCH NEXT,這種Fetch的方式其實並不好是線性復雜度,所以,如果我們能夠知道order by字段的第二頁的起始值,我們就可以在where語句里直接使用>=的表達式來select,這種技術叫seek,而不是fetch,seek的性能比fetch要高很多。
- 字符串。正如我前面所說的,字符串操作對性能上有非常大的惡夢,所以,能用數據的情況就用數字,比如:時間,工號,等。
- 全文檢索。千萬不要用Like之類的東西來做全文檢索,如果要玩全文檢索,可以嘗試使用Sphinx。
- 其它。
- 不要select *,而是明確指出各個字段,如果有多個表,一定要在字段名前加上表名,不要讓引擎去算。
- 不要用Having,因為其要遍歷所有的記錄。性能差得不能再差。
- 盡可能地使用UNION ALL 取代 UNION。
- 索引過多,insert和delete就會越慢。而update如果update多數索引,也會慢,但是如果只update一個,則只會影響一個索引表。
(2)DBCC DBREINDEX重建索引

下面舉例來說明DBCC SHOWCONTIG和DBCC REDBINDEX的使用方法。以應用程序中的Employee數據表作為例子,在 SQL Server的Query analyzer輸入命令:
use database_name
declare @table_id int
set @table_id=object_id('Employee')
dbcc showcontig(@table_id)
輸出結果:
DBCC SHOWCONTIG scanning 'Employee' table...
Table: 'Employee' (1195151303); index ID: 1, database ID: 53
TABLE level scan performed.
- Pages Scanned................................: 179
- Extents Scanned..............................: 24
- Extent Switches..............................: 24
- Avg. Pages per Extent........................: 7.5
- Scan Density [Best Count:Actual Count].......: 92.00% [23:25]
- Logical Scan Fragmentation ..................: 0.56%
- Extent Scan Fragmentation ...................: 12.50%
- Avg. Bytes Free per Page.....................: 552.3
- Avg. Page Density (full).....................: 93.18%
DBCC execution completed. If DBCC printed error messages, contact your system administrator.
通過分析這些結果可以知道該表的索引是否需要重構。如下描述了每一行的意義:
信息 描述
Pages Scanned 表或索引中的長頁數
Extents Scanned 表或索引中的長區頁數
Extent Switches DBCC遍歷頁時從一個區域到另一個區域的次數
Avg. Pages per Extent 相關區域中的頁數
Scan Density[Best Count:Actual Count]
Best Count是連續鏈接時的理想區域改變數,Actual Count是實際區域改變數,Scan Density為100%表示沒有分塊。
Logical Scan Fragmentation 掃描索引頁中失序頁的百分比
Extent Scan Fragmentation 不實際相鄰和包含鏈路中所有鏈接頁的區域數
Avg. Bytes Free per Page 掃描頁面中平均自由字節數
Avg. Page Density (full) 平均頁密度,表示頁有多滿
從上面命令的執行結果可以看的出來,Best count為23 而Actual Count為25這表明orders表有分塊需要重構表索引。下面通過DBCC DBREINDEX來重構表的簇索引。
3. DBCC DBREINDEX 用法
重建指定數據庫中表的一個或多個索引。
語法
DBCC DBREINDEX
( [ 'database.owner.table_name'
[ , index_name
[ , fillfactor ]
]
]
)
參數
'database.owner.table_name'
是要重建其指定的索引的表名。數據庫、所有者和表名必須符合標識符的規則。有關更多信息,請參見使用標識符。如果提供 database 或 owner 部分,則必須使用單引號 (') 將整個 database.owner.table_name 括起來。如果只指定 table_name,則不需要單引號。
index_name
是要重建的索引名。索引名必須符合標識符的規則。如果未指定 index_name 或指定為 ' ',就要對表的所有索引進行重建。
fillfactor
是創建索引時每個索引頁上要用於存儲數據的空間百分比。fillfactor 替換起始填充因子以作為索引或任何其它重建的非聚集索引(因為已重建聚集索引)的新默認值。如果 fillfactor 為 0,DBCC DBREINDEX 在創建索引時將使用指定的起始 fillfactor。
同樣在Query Analyzer中輸入命令:
dbcc dbreindex('database_name.dbo.Employee','',90)
然后再用DBCC SHOWCONTIG查看重構索引后的結果:
DBCC SHOWCONTIG scanning 'Employee' table...
Table: 'Employee' (1195151303); index ID: 1, database ID: 53
TABLE level scan performed.
- Pages Scanned................................: 178
- Extents Scanned..............................: 23
- Extent Switches..............................: 22
- Avg. Pages per Extent........................: 7.7
- Scan Density [Best Count:Actual Count].......: 100.00% [23:23]
- Logical Scan Fragmentation ..................: 0.00%
- Extent Scan Fragmentation ...................: 0.00%
- Avg. Bytes Free per Page.....................: 509.5
- Avg. Page Density (full).....................: 93.70%
DBCC execution completed. If DBCC printed error messages, contact your system administrator.
通過結果我們可以看到Scan Denity為100%。
2.1.5 靜態化
靜態化一些不常變的頁面和數據,並gzip一下。使用nginx的sendfile功能可以讓這些靜態文件直接在內核心態交換,可以極大增加性能。
一般我們可以做一個靜態文件管理功能,可以把我們網站的一些欄目直接通過請求/響應的方式在服務器上直接生成靜態文件,當然這里可以設置一個時間頻率,用戶直接訪問靜態頁面訪問效率肯定非常高!
2.1.6 緩存
通常,應用程序可以將那些頻繁訪問的數據,以及那些需要大量處理時間來創建的數據存儲在內存中,從而提高性能;它包括應用程序緩存和頁輸出緩存;
一般我們大部分用的是應用程序緩存
緩存的應用場景主要有:
OutputCache
我們可以用Fiddler找出一些內容幾乎不會改變的頁面,給它們設置OutputCache指令即可;
對於設置過OutputCache的頁面來說,瀏覽器在收到這類頁面的響應后,會將頁面響應內容緩存起來。 只要在指定的緩存時間之內,且用戶沒有強制刷新的操作,那么就根本不會再次請求服務端, 而對於來自其它的瀏覽器發起的請求,如果緩存頁已生成,那么就可以直接從緩存中響應請求,加快響應速度。 因此,OutputCache指令對於性能優化來說,是很有意義的(除非所有頁面頁面都在頻繁更新)。
應用程序緩存
應用程序緩存提供了一種編程方式,可通過鍵/值對將任意數據存儲在內存中,這里提供一個asp.net對緩存有效封裝的例子,見緩存機制理解及C#開發使用。
緩存可以用來緩存動態頁面,也可以用來緩存查詢的數據。緩存通常有那么幾個問題:
1)緩存的更新。也叫緩存和數據庫的同步。有這么幾種方法,一是緩存time out,讓緩存失效,重查,二是,由后端通知更新,一量后端發生變化,通知前端更新。前者實現起來比較簡單,但實時性不高,后者實現起來比較復雜 ,但實時性高。
2)緩存的換頁。內存可能不夠,所以,需要把一些不活躍的數據換出內存,這個和操作系統的內存換頁和交換內存很相似。FIFO、LRU、LFU都是比較經典的換頁算法。
3)緩存的重建和持久化。緩存在內存,系統總要維護,所以,緩存就會丟失,如果緩存沒了,就需要重建,如果數據量很大,緩存重建的過程會很慢,這會影響生產環境,所以,緩存的持久化也是需要考慮的。