工作中當一個業務系統被開發出來之后,經過多次迭代業務的發展處理邏輯會越來越復雜,同時訪問量以及處理的數據也會相應的增多,系統的響應時間就會開始得逐漸加長。終於有一天用戶忍受不了抱怨你的app或者頁面要等好幾十秒或者好幾分鍾才響應時你就迫切需要對你的系統進行一個性能的優化。
下面談一下我對性能優化方面的一些觀點和見解,包括分析系統為什么會慢,以及針對這些慢的情形如何選取更合理的解決方案。
系統為什么慢?
說起系統慢的原因能舉的例子太多了,比如某人寫的代碼寫得太爛了,這個sql查詢寫得太差了,循環調服務啊等等。那拋開個人編碼的能力問題有沒有一些是與系統本身的設計相關的呢。比如系統原先是一個單體應用,后面做了業務的垂直拆分拆出了N個子系統並且做了一個分布式的部署變成了一個分布式系統,那么系統的響應時間會不會因此變長呢。答案幾乎是肯定的!為什么呢
舉一個電商系統的例子,左邊是系統初期的樣子,基本上所有業務模塊都放在了一起,右邊是后面做了業務的垂直拆分之后的樣子。我們看到這些業務模塊間的調用是由進程/線程變成了一個遠程的RPC網絡調用,這里就多出了網絡響應時間(RT)的延遲 ,試想一個電商的成單過程要調商品、結算、庫存、用戶、積分等N個系統。假設每個RT時間都是2ms那么成單就有2Nms的時間是在等待數據傳輸。這里面還並沒有算上這些N子系統的本身的處理時間並且它們很可能也會調用其它外部的子系統。所以你知道像阿里這樣的公司在雙11的之前為什么做全鏈路的優化了吧,其中一個原因是因為當一個單體應用被拆成一個分布式系統后調用鏈路變長了,並且每個鏈路至少多了一個RT的響應延遲。所以我們知道有時候系統變慢並不是某個開發的鍋,而是因為系統架構導致的。對於這種情形我們就可以優化RT延遲,解決辦法可以是申請專線或者把應用部署在同一個物理機房里面,反正原理就是不能讓我們的數據走太長的路~。
從上面這個例子我們知道在做系統優化之前分析是第一步也是最重要的一步。說到分析有些問題可以在我們腦中進行思維邏輯推導分析就可以定位到問題的。但現實中絕大部分問題都是不行的!都是要用數據說話的!所以監控是很重要的,沒有全鏈路的跟蹤,只是靠猜做優化只會事倍功半!全鏈路的跟蹤工具這里推薦一下大眾點評cat https://github.com/dianping/cat 和 zipkin https://zipkin.io/
下面列幾個常見的問題分析是如何做優化選型的和一些系統中你可能沒有注意到有性能問題的地方。
大量讀多少量寫是主從還是緩存?
說到這個問題我相信很多人的第一反應的都是說主從,確實在我的工作經歷中絕大部分人在這個問題第一反應也是主從,其中不乏包括一些在大廠待過並且工作經驗是我一倍多的老前輩。我自己覺得主從還是緩存得看"量"有多大。這個"量"不是指有多少用戶在訪問,而是用戶訪問的這個數據到底有大。如果用戶訪問的這個數據很小,小到用緩存就可以放得下,我覺得這時用緩存是更好的方案。為什么?因為主從之間的數據同步延遲是一個很頭疼的問題!特別是一主多從的情況下。。。。並且弄從庫的成本會很高為了解決主從不同步的問題還需要調研一個數據中間件,並且性能還沒有緩存高,落地成本大,時間長!只有在用戶訪問的數據量太大,大到不能用緩存放得下我覺得這時候才考慮用從庫,並且要充分考慮數據不同步的影響!之前我看過58架構師沈劍的文章,他也提到58是用緩存來解決讀多寫少問題,讀緩存,寫主寫時更新緩存,從庫只作為數據備份作為容災。然而工作中我也確實遇到過一個庫的數據只有幾千條但弄出了一個從庫的經歷,汗。。。。
依賴非常多的外部RPC服務時
我曾經遇到服務器端返回一個頁面展示的數據要調用兩位數的外部RPC服務,並且這個頁面是用戶經常訪問到的而且頁面數據還真不能做緩存,數據必須得實時刷。這個頁面每次一有大促的時候必須得加機器並且加了還是有點扛不住的樣子。后面優化這塊代碼的時候我發現有幾個RPC服務不是相互依賴它們是各自返回部分的數據。我就把這幾個RPC換成異步調用的形式,后面的性能就好很多了,雖然大促時還是要加機器,但cpu和響應時間指標比優化之前降了許多。這里用到的秘訣就是化同步為異步,化串行為並行。並且這一招在很多情況都是很有效果的!
如何配置線程池
線程池也是一個性能優化重點,線程池配置得好的話就能很好利用現代計算機的多核處理器去提升系統的性能,但是配置得不好的話,就不好說了~。線程池參數很重要的一個參數就是設置多少個線程。一般來說線程的數量是與你處理的任務特性是相關的,比如你的任務是重計算輕IO的話,那么你的線程數最好不要太多一般Ncpu + 1 或者Ncpu就夠了。因為太多的線程會導致CPU競爭劇烈不停的進行上下文切換,這反而是事倍功半。如果是IO密集型任務的話則可以根據自已需要的配多一點線程比如2Ncpu。如果是混合型任務那最好把兩者分開,獨立用線程池是處理。另外如果你是用JAVA的話最好是去了解一下Executor的框架,看一下JDK的源碼實現,因為JAVA有好幾種線程池:FixedThreadPool,SingleThreadExecutor,CachedThreadPool,了解一下它們的適用情景對你的選型是很有好處的。同時你也要留意一下你部署機器的CPU核數,千萬不要在單核的機器上啟用多線程。另外要注意了單線程模型也並不一定代表性能底下哦,比如redis就是單線程模型。為什么高呢?其中一個原因是它沒有CPU上下文切換啊。
DB查詢性能優化
這一塊我覺得要講的東西非常多,有時間的話打算專門寫一篇文章來介紹。這里略過鳥。。。
系統中陳舊的框架
不知道大家的項目里面是不是還有用c3p0連接池的,這個東西我在出來工作那會兒用過之后就再也沒有用過了。因為這個線程池性能確實不高,並且工作中也經常報問題。后面我用的是druid再后來是spring-boot的tomcat-jdbc,hikaricp。因為技術總是不停的在前進,新的設計總是不斷改良舊的設計。並且一些陳舊的框架后面就再沒有更新了,比如c3p0吧,它是用JAVA語言寫的它出來那會兒當時JDK用來同步操作還只能是synchronized,所以意味着當時語言特性影響限制它的性能上限。現在JDK和當時的相比可不一樣了增加了並發包,從虛擬機到我們常用的數據結構和API都做了很多優化和改良。所以框架也是不斷更新的才好。
了解熟悉你常用數據結構、中間件和系統網絡知識。
比如redis是單線程模型,單線程意味着redis的所有任務都只能按順序的一個個去處理,前面的沒處理完后面就只能安靜的等待着。前面的卡住后面就會有堆積,就會產生超時影響redis的性能,所以redis里面不能有重計算的操作和存大key,如果要存大key最好應用服務器自己壓縮一下才存進去,一方面可以減少性能影響也可以節省帶寬~。網絡協議TCP和UDP這些就不用說了。HTTP和HTTPS兩者的性能差異據說是4倍左右,因為HTTPS每次發接收報文都要加解密,運算耗費CPU。所以一些數據不敏感的是不是考慮用HTTP會好一點~。JAVA常用的數據ArrayList,LinkedList,Set,HashMap,Array,TreeMap這些低層的源碼最好也看一下,了解一下適用場景,順便也學習一下別人的代碼。我舉個例子之前我做算法題的時候有道題用HashMap和Array都是可以AC的,但是兩者的出來的時間差了一個數量級,原因是HashMap和Array的低層實現導致的。HashMap低層是一個數組鏈表,並且存儲的對象是Node結點,Hash沖突大的時候其時間復雜度就由O(1) 退化成O(n),並且還有一個不為人知的擴容過程,Array數組就是確定了每一次讀取操作都O(1)。所以執行n次操作后兩者的耗費的時間就是O(n)平方與O(n)的差別了。。。。不過HashMap在JAVA8終於做了改良,當鏈表的長度大於8的時候就換成了紅黑樹的存儲這時復雜度降為了O(log(n)),這些你看下源碼就一目了然。另外操作系統這塊也是要了解的比如epoll select poll的區別,線程用戶態內核態的切換等等。
養成良好的個人編碼習慣和意識。
比如不要寄望在一個SQL里面做完所有的事情,因為通常是寫一個復雜的SQL一分鍾,可能優化你一年。調用N次RPC處理N個數據不如調一次RPC處理N個數據,因為可以節省了(N-1)的RT時間,同樣對SQL查詢也適用。多了解一下一些新語言的特性和設計思想比如forkJoin,JAVA8的lambda表達式,completefuture異步處理。或者有時間精力學習一下算法知識也是可以開闊一下你的思路。
另外普及一下性能優化逃不開的一個原理就是木桶原理,就是系統的整體性能的好壞不是取決於你最好的那部分,而是最差的那部分·-·。這部分可能是在你系統內部也可能是外部。所以性能優化的路很長很長很長很長長長。。。