性能優化指南:性能優化的一般性原則與方法
一般性原則
依據數據而不是憑空猜測
這是性能優化的第一原則,當我們懷疑性能有問題的時候,應該通過測試、日志、 profillig 來分析 出哪里 有問題,有的放矢,而不是憑感覺、撞運氣。一個系統有了性能問題,瓶頸有可能是 CPU ,有可能是內存,有可能是 IO (磁盤 IO ,網絡 IO ),大方向的定位可以使用 top 以及 stat 系列來定位( vmstat , iostat , netstat ... ),針對單個進程,可以使用 pidstat 來分析。
在本文中,主要討論的是 CPU 相關的性能問題。按照 80/20 定律,絕大多數的時間都耗費在少量的代碼片段里面,找出這些代碼唯一可靠的辦法就是 profile ,我所知的編程語言,都有相關的 profile 工具,熟練使用這些 profile 工具是性能優化的第一步。
忌過早優化
The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.
我并不十分清楚 Donald Knuth 說出這句名言的上下文環境,但我自己是十分認同這個觀念的。在我的工作環境(以及典型的互聯網應用開發)與編程模式下,追求的是快速的迭代與試錯,過早的優化往往是無用功。而且,過早的優化很容易拍腦袋,優化的點往往不是真正的性能瓶頸。
忌過度優化
As performance is part of the specification of a program – a program that is unusably slow is not fit for purpose
性能優化的目標是追求合適的性價比。
在不同的階段,我們對系統的性能會有一定的要求,比如吞吐量要達到多少 多少 。如果達不到這個指標,就需要去優化。如果能滿足預期,那么就無需花費時間精力去優化,比如只有幾十個人使用的內部系統,就不用按照十萬在線的目標去優化。
而且,后面也會提到,一些優化方法是 “ 有損 ” 的,可能會對代碼的可讀性、可維護性有副作用。這個時候,就更不能過度優化。
深入理解業務
代碼是服務于業務的,也許是服務于最終用戶,也許是服務于其他程序員。不了解業務,很難理解系統的流程,很難找出系統設計的不足之處。后面還會提及對業務理解的重要性。
性能優化是持久戰
當核心 業務方向明確之后,就應該開始關注性能問題,當項目上線之后,更應該持續的進行性能檢測與優化。
現在的互聯網產品,不再是一錘子買賣,在上線之后還需要持續的開發,用戶的涌入也會帶來性能問題。因此需要自動化的檢測性能問題,保持穩定的測試環境,持續的發現并解決性能問題,而不是被動地等到用戶的投訴。
選擇合適的衡量指標、測試用例、測試環境
正因為性能優化是一個長期的行為,所以需要固定衡量指標、測試用例、測試環境,這樣才能客觀反映性能的實際情況,也能展現出優化的效果。
衡量性能有很多指標,比如系統響應時間、系統吞吐量、系統并發量。不同的系統核心指標是不一樣的,首先要明確本系統的核心性能訴求,固定測試用例;其次也要兼顧其他指標,不能顧此失彼。
測試環境也很重要,有一次突然發現我們的 QPS 高了許多,但是程序壓根兒沒優化,查了半天,才發現是換了一個更牛逼的 物理機 做測試服務器。
性能優化的層次
按照我的理解可以分為需求階段,設計階段,實現階段;越上層的階段優化效果越明顯,同時也更需要對業務、需求的深入理解。
需求階段
不戰而屈人之兵,善之善者也
程序員的需求可能來自 PM 、 UI 的業務需求(或者說是功能性需求),也可能來自 Team Leader 的需求。當我們拿到一個需求的時候,首先需要的是思考、討論需求的合理性,而不是立刻去設計、去編碼。
需求是為了解決某個問題,問題是本質,需求是解決問題的手段。那么需求是否能否真正的解決問題,程序員也得自己去思考,產品經理(特別是知道一點技術的產品經理)的某個需求可能只是某個問題的解決方案,他認為這個方法可以解決他的問題,于是把解決方案當成了需求,而不是真正的問題。
需求討論的前提對業務的深入了解,如果不了解業務,根本沒法討論。即使需求已經實現了,當我們發現有性能問題的時候,首先也可以從需求出發。
需求分析對性能優化有什么幫助呢,第一,為了達到同樣的目的,解決同樣問題,也許可以有性能更優(消耗更小)的辦法。這種優化是無損的,即不改變需求本質的同時,又能達到 性能優化的效果;第二種情況,有損的優化,即在不明顯影響用戶的體驗,稍微修改需求、放寬條件,就能大大解決性能問題。 PM 退步 一 小步,程序前進一大步。
需求討論也有助于設計時更具擴展性,應對未來的需求變化,這里按下不表。
設計階段
高手都是花 80% 時間思考, 20% 時間實現;新手寫起代碼來很快,但后面是無窮無盡的修 bug
設計的概念很寬泛,包括架構設計、技術選型、接口設計等等。架構設計約束了系統的擴展、技術選型決定了代碼實現。編程語言、框架都是工具,不同的系統、業務需要選擇適當的工具集。如果設計的時候做的不夠好,那么后面就很難優化,甚至需要推到重來。
實現階段
實現是把功能翻譯成代碼的過程,這個層面的優化,主要是針對一個調用流程,一個函數, 一 段代碼的優化。各種 profile 工具也主要是在這個階段生效。除了靜態的代碼的優化,還有編譯時優化,運行時優化。后二者要求就很高了,程序員可控性較弱。
代碼層面,造成性能瓶頸的原因通常是高頻調用的函數、或者單次消耗非常高的函數、或者二者的結合。
下面介紹針對設計階段與實現階段的優化手段。
一般性方法
緩存
沒有什么性能問題是緩存解決不了的,如果有,那就再加一級緩存
a cache / k? ? / KASH,[1] is a hardware or software component that stores data so future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation, or the duplicate of data stored elsewhere.
緩存的本質是加速訪問,訪問的數據要么是其他數據的副本 -- 讓數據離用戶更近;要么 是之前 的計算結果 -- 避免重復計算 .
緩存需要用空間換時間,在緩存空間有限的情況下,需要優秀的置換換算來保證緩存有較高的命中率。
數據的緩存
這是我們最常見的緩存形式,將數據緩存在 離使用 者更近的地方。比如操作系統中的 CPU cache 、 disk cache 。對于一個 web 應用,前端會有瀏覽器緩存,有 CDN ,有反向代理提供的靜態內容緩存;后端則有本地緩存、分布式緩存。
數據的緩存,很多時候是設計層面的考慮。
對于數據緩存,需要考慮的是緩存一致性問題。對于分布式系統中有強一致性要求的場景,可行的解決辦法有 lease ,版本號。
計算結果的緩存
對于消耗較大的計算,可以將計算結果緩存起來,下次直接使用。
我們知道,對遞歸代碼的一個有效優化手段就是緩存中間結果, lookup table ,避免了重復計算。 python 中的 method cache 就是這種思想 .
對于可能重復創建、銷毀, 且創建 銷毀代價很大的對象,比如進程、線程,也可以緩存,對應的緩存形式如單例、資源池(連接池、線程池)。
對于計算結果的緩存,也需要考慮緩存失效的情況,對于 pure function ,固定的輸入有固定的輸出,緩存是不會失效的。但如果計算受到中間狀態、環境變量的影響,那么緩存的結果就可能失效
并發
一個人干不完的活,那就找兩個人干。 并發既 增加了系統的吞吐,又減少了用戶的平均等待時間。
這里的并發是指廣義的并發,粒度包括多機器(集群)、多進程、多線程。
對于無狀態(狀態是指需要維護的上下文環境,用戶請求依賴于這些上下文環境)的服務,采用集群就能很好的伸縮,增加系統的吞吐,比如掛載 nginx 之后的 web server
對于有狀態的服務,也有兩種形式,每個節點提供同樣的數據,如 mysql 的讀寫分離;每個節點只提供部分數據,如 mongodb 中的 sharding
分布式存儲系統中, partition ( sharding )和 replication ( backup )都有助于并發。
絕大多數 web server ,要么使用多進程,要么使用多線程來處理用戶的請求,以充分利用多核 CPU ,再有 IO 阻塞的地方,也是適合使用多線程的。比較新的協程( Python greenle 、 goroutine )也是一種并發。
惰性
將計算推遲到必需的時刻,這樣很可能避免了多余的計算,甚至根本不用計算
批量,合并
在有 IO (網絡 IO ,磁盤 IO )的時候,合并操作、批量操作往往能提升吞吐,提高性能。
我們最常見的是批量讀:每次讀取數據的時候多讀取一些,以備不時之需。如 GFS client 會從 GFS master 多讀取一些 chunk 信息;如分布式系統中,如果集中式節點復雜全局 ID 生成, 俺么應用 就可以一次請求一批 id 。
特別是系統中有單點存在的時候,緩存和批量本質上來說減少了與單點的交互,是減輕單點壓力的經濟有效的方法
在前端開發中,經常會有資源的壓縮和合并,也是這種思想。
當涉及到網絡請求的時候,網絡傳輸的時間可能遠大于請求的處理時間,因此合并網絡請求就很有必要,比如 mognodb 的 bulk operation , redis 的 pipeline 。寫文件的時候也可以批量寫,以減少 IO 開銷, GFS 中就是這么干的
更高效的實現
同一個算法,肯定會有不同的實現,那么就會有不同的性能;有的實現可能是時間換空間,有的實現可能是空間換時間,那么就需要根據自己的實際情況權衡。
程序員都喜歡早輪子,用于練手無可厚非,但在項目中,使用成熟的、經過驗證的輪子往往比自己造的輪子性能更好。當然不管使用別人的輪子,還是自己的工具,當出現性能的問題的時候,要么優化它,要么替換掉他。
比如,我們有一個場景,有大量復雜的嵌套對象的序列化、反序列化,開始的時候是使用 python ( Cpython )自帶的 json 模塊,即使發現有性能問題也沒法優化,網上一查,替換成了 ujson ,性能好了不少。
上面這個例子是無損的,但一些更高效的實現也可能是有損的,比如對于 python ,如果發現性能有問題,那么很可能會考慮 C 擴展,但也會帶來維護性與靈活性的喪失,面臨 crash 的風險。
縮小解空間
縮小 解空間 的意思是說,在一個更小的數據范圍內進行計算,而不是遍歷全部數據。最常見的就是索引,通過索引,能夠很快定位數據,對數據庫的優化絕大多數時候都是對索引的優化。
如果有本地緩存,那么使用索引也會大大加快訪問速度。不過,索引比較適合讀多寫少的情況,畢竟索引的構建也是需有消耗的。
另外在游戲服務端,使用的分線和 AOI (格子算法)也都是縮小 解空間 的方法。
性能優化與代碼質量
很多時候,好的代碼也是高效的代碼,各種語言都會有一本類似的書《 effective xx 》。比如對于 python , pythonic 的代碼通常效率都不錯,如使用迭代器而不是列表( python2.7 dict 的 iteritems (), 而不是 items()) 。
衡量代碼質量的標準是可讀性、可維護性、可擴展性,但性能優化有可能會違背這些特性,比如為了屏蔽實現細節與使用方式,我們會可能會加入接口層(虛擬層),這樣可讀性、可維護性、可擴展性會好很多,但是額外增加了一層函數調用,如果這個地方調用頻繁,那么也是一筆開銷;又如前面提到的 C 擴展,也是會降低可維護性、
這種有損代碼質量的優化,應該放到最后,不得已而為之,同時寫清楚注釋與文檔。
為了追求可擴展性,我們經常會引入一些設計模式, 如狀態 模式、策略模式、模板方法、 裝飾器模式 等,但這些模式不一定是性能友好的。所以,為了性能,我們可能寫出一些 反模式 的、定制化的、不那么優雅的代碼,這些代碼其實是脆弱的,需求的一點點變動,對代碼邏輯可能有至關重要的影響,所以還是回到前面所說,不要過早優化,不要過度優化。
任務調度 網絡
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。