性能優化——應用服務器性能優化

核心知識點:前端

網站性能優化第必定律:優先使用緩存。算法

1.分佈式緩存數據庫

(1)緩存原理編程

  a.什麼是緩存?(將數據存儲在相對較高訪問速度的介質中,以供系統處理)設計模式

  b.緩存的優勢:訪問速度快,若是須要計算能夠減小計算時間瀏覽器

  c.緩存的本質是一張以鍵值對存儲的內存hash表緩存

  d.主要用來存儲:讀寫比例高,不多變化的數據安全

  e.網站的訪問遵循28定律性能優化

(2)合理使用緩存應該注意如下問題服務器

  a.頻繁更新的數據(數據尚未讀就已經失效,通常要求讀寫比在2:1以上纔有意義)

  b.沒有熱點數據(無疑浪費資源)

  c.數據不一致與髒讀(緩存被加載的過程當中可能會產生數據不一致,有效時間過程數據在內存中就會變成髒數據)

  d.緩存的可用性(若是太過依賴緩存,容易產生雪崩。使用緩存熱備並不能提升緩存的可用性,使用集羣能夠提升可用性)

  e.緩存預熱(LRU計算時間過長,有的數據須要提早加載)

  f.緩存穿透(解決部分數據沒法命中,而加劇數據庫壓力的問題,通常設置空值)

(3)分佈式緩存架構

  a.JBoss Cache:數據相同

  b.Memcached:數據不一樣互不通訊

  c.Memcached5大優勢:協議簡單、通用性強(支持各類語言)、Libevent網絡通訊、內存管理高效、互不通訊

 

 2.異步操做

  a.數據直接寫入數據庫會形成巨大的壓力

  b.異步能夠減緩響應時間,還能提高網站性能

  c.對於大型網站來講,異步能夠實現削峯

 

3.使用集羣:使單臺服務處於最佳性能區間

 4.代碼優化

  a.多線程

  (1)線程優於進程的緣由:更輕量級、資源消耗更少、切換更容易

  (2)使用多線程的緣由:IO阻塞和多CPU

  (3)保證線程安全的手段:無狀態對象、局部對象、鎖

  b.數據結構

  (1)Hash表數據結構:依賴於HashCode,越散列,衝突越少,讀寫性能越高。

  (2)優化手段:Time33和信息指紋

  c.資源複用:單例和線程池

  d.垃圾回收:理解JVM垃圾回收機制

 

 

 

上一章闡述瞭如何在Web前端進行性能優化,本章講闡述如何在應用服務器進行性能優化。

應用服務器就是處理網站業務的服務器,網站的業務代碼都部署在這裏,

是網站開發最複雜,變化最多的地方,優化手段主要有緩存、集羣、異步等

1.分佈式緩存

回顧網站架構演化歷程,當網站遇到性能瓶頸時,第一個想到的解決方案就是使用緩存。

在整個網站應用中,緩存幾乎無處不在,既存在於瀏覽器,也存在於應用服務器數據庫服務器

便可以對數據緩存,也能夠對文件緩存,還能夠對頁面片斷緩存,對網站性能優化意義重大。

網站性能優化第必定律:優先考慮使用緩存優化性能。

 

1)緩存基本原理

緩存指將數據存儲在相對較高訪問速度的存儲介質中,以供系統處理

一方面緩存訪問速度快,能夠減小數據訪問的時間,另外一方面若是緩存的數據是通過計算處理獲得的,

那麼被緩存的數據無需重複計算便可直接使用,所以緩存還起到減小計算時間的做用。

 

緩存的本質是一個內存Hash表,網站應用中,數據緩存以一對Key、Value的形式存儲在內存Hash表中。

Hash表數據讀寫的時間複雜度爲O(1)。下圖爲一對KV在Hash表中的存儲。

緩存主要用來存放那些讀寫比很高、不多變化的數據,如商品的類目信息,熱門詞的搜索信息,熱門商品信息等。

應用程序讀取數據時,先到緩存中讀取,若是讀取不到或數據已失效,再訪問數據庫,並將數據庫寫入緩存。

網站數據訪問一般遵循二八定律,即80%的訪問落在20%的數據上,所以利用Hash和內存的高速訪問特性,

將這20%的數據緩存起來,可很好的改善系統性能,提升數據讀取的速度,下降存儲訪問壓力。

 

2)合理使用緩存

使用緩存對提升系統性能有不少好處,可是不合理使用緩存非但不能提升系統的性能,還會成爲系統的累贅,甚至風險。

實踐中,緩存濫用的情景家常便飯——過度依賴低可用的緩存系統、不恰當地使用緩存的數據訪問特性等。

(1)頻繁修改的數據

若是緩存中保存的數據是 頻繁修改的數據,就會出現數據寫入緩存後應用還來不及讀取緩存,數據就已失效的情形,徒增系統負擔。

通常來講,數據的讀寫比在2:1以上,即寫入一次緩存,在數據更新前至少讀取兩次,緩存纔有意義。

實踐中,這個讀寫比一般很是高,好比新浪微博的熱門博客,緩存之後可能會被讀取數百萬次。

 

(2)沒有熱點的訪問

緩存使用內存做爲存儲,內存資源寶貴而有限,不可能將全部數據都緩存起來,

只能將最新的訪問數據緩存起來,而將歷史數據清理出緩存。

若是系統訪問數據沒有熱點,不遵循二八定律,即大部分數據訪問沒有集中在小部分數據上,

那麼緩存就沒有意義,由於大部分數據尚未被再次訪問就已經被擠出緩存了。

 

(3)數據不一致與髒讀

通常對緩存的數據設置失效時間,一旦超過失效時間,就要從數據庫中從新加載。

所以數據要容忍一時的數據不一致,如賣家已經編輯來商品屬性,可是須要過一段時間才能被賣家看到。

在互聯網應用中,這種延遲一般是能夠接受的,可是具體 應用仍需慎重對待。

還有一種策略是數據更新時當即更新緩存,不過這也會帶來更多系統開銷和事務一致性問題。

 

(4)緩存可用性

緩存是爲提升數據讀取性能的,緩存數據丟失或者緩存不可用不會影響到應用程序的處理——它能夠從數據庫直接獲取數據。

可是隨着業務的發展,緩存會承擔大部分數據訪問的壓力,數據庫已經習慣了有緩存的日子,

因此當緩存服務崩潰時,數據庫會由於徹底不能承受如此大的壓力而當宕機,進而致使整個網站不可用。

這種狀況被稱爲緩存雪崩,發生這種故障,甚至不能簡單地重啓緩存服務器和數據庫服務器來恢復網站訪問。

 

實踐中,有的網站經過緩存熱備等手段提升緩存可用性:將某臺緩存服務器宕機時,將緩存訪問切換到熱備服務器上。

可是這種設計顯然有違緩存的初衷,緩存根本就不該該被看成一個可靠的數據源來使用。

 

經過分佈式緩存服務器集羣,將緩存數據分佈到集羣多臺服務器上可在必定程度上改善緩存的可能性。

當一臺緩存服務器宕機 時,只有部分緩存數據丟失,從新從數據庫加載這部分數據不會對數據庫產生很大影響。

 

產品在設計之初就須要一個明確的定位:什麼是產品要實現的功能,什麼不是產品提供的特性。

在產品漫長的生命週期中,會有形形色色的困難和誘惑來改變產品的發展方向,

左右搖擺、什麼都想作的產品,最後有可能成爲一個失去生命力的四不像。

 

(5)緩存預熱

緩存中存放的是熱點數據,熱點數據又是緩存系統利用LRU(最近最久未用算法)對不斷訪問的數據篩選出來的, 這個過程須要花費較長的時間。

新啓動的緩存系統若是沒有任何數據,在重建緩存數據的過程當中,系統的性能和數據庫負載都不太好,

那麼最好在緩存系統啓動時就把熱點數據加載好,這個緩存預加載手段叫做預熱

對於一些元數據如城市地名列表、類目信息,能夠在啓動時加載數據庫中所有數據到緩存進行預熱。

 

(6)緩存穿透

若是由於不恰當的業務、或者惡意攻擊持續高併發地請求某個不存在的數據,因爲緩存沒有保存該數據,

全部的請求都會落在數據庫上,會對數據庫形成很大壓力,甚至崩潰。

一個簡單的對策是將不存在的數據也緩存起來(其value值爲null)。

 

3)分佈式緩存架構

分佈式緩存指緩存部署在多個服務器組成的集羣中,以集羣方式提供緩存服務,其架構方式有兩種,

一種是以JBoss Cache爲表明的須要更新同步的分佈式緩存,一種是以Memcached爲表明的互不通訊的分佈式緩存。

 

JBoss Cache的分佈式緩存在集羣中全部服務器中保存相同的緩存數據,當某臺服務器有緩存數據更新的時候,

會通知集羣中其餘機器更新緩存數據或清除緩存數據。

JBoss Cache一般將應用程序和緩存部署在同一臺服務器上,應用程序可從本地緩存讀取數據,

可是這種方式帶來的問題是緩存數據的數量受限於單一服務器的內存空間,

並且當集羣規模較大的時候,緩存更新信息須要同步到集羣全部機器,其代價驚人。

於是這種方案更多見於企業應用系統中,而不多在大型網站使用。

 

大型網站須要緩存的數據量通常都很龐大,可能須要數TB的內存作緩存,這時候就須要另外一種分佈式緩存。

Memcached採用一種集中式的緩存集羣管理,也被稱做互不通訊的分佈式架構方式。

緩存與應用分離部署,緩存系統部署在一種專門的服務器上,應用程序經過一致性hash等路由算法選擇緩存服務器遠程訪問緩存數據,

緩存服務器之間不通訊,緩存服務器的規模能夠很容易的實現擴容,具備良好的可伸縮性。

 

2.異步操做

使用消息隊列將調用異步化,可改善網站的擴展性,事實上。使用消息隊列還能夠改善網站系統的性能

在不使用消息隊列的狀況下,用戶的請求數據直接寫入數據庫,在高併發的狀況下,會對數據庫形成巨大的壓力,同時也使響應延遲加重。

在使用消息隊列後,用戶請求的數據發送給消息隊列以後當即返回,

再由消息隊列的消費者進程(一般狀況下該進程獨立部署在專門的服務器集羣上)從消息隊列中獲取數據,異步寫入數據庫。

因爲消息隊列服務器處理速度遠快於數據庫(消息隊列服務器比數據庫具備更好的伸縮性),所以用戶的響應延遲能夠獲得有效改善

 

消息隊列具備很好的削峯做用——即經過異步處理,將短期高併發產生的事務消息存儲在消息隊列中,從而削平高峯期的併發事務。

在電子商務網站促銷活動中,合理使用消息隊列,能夠抵禦促銷活動剛開始大量涌入的訂單對系統的形成的衝擊。

須要注意的是,因爲數據寫入消息隊列以後當即返回給用戶數據在後續的業務校驗、寫數據庫操做可能失敗,

所以在使用消息隊列進行業務的異步處理後,須要適當修改業務流程進行配合,

如提交訂單後,訂單數據寫入消息隊列,不能當即返回用戶訂單數據提交成功,

須要在消息隊列的訂單消費者進程真正處理完該訂單,甚至商品出庫後,在經過電子郵件或SMS消息通知用戶訂單成功,以避免交易糾紛。

 

3.使用集羣

在網站高併發的場景下,使用負載均衡技術爲一個應用構建一個由多臺服務器組成的一個集羣,

將併發訪問請求分發到多臺服務器上處理,避免單一服務器因負載壓力過大而響應緩慢,使用戶請求具備更好的響應延遲特性。

三臺Web服務器共同處理來自用戶瀏覽器的訪問請求,這樣每臺Web服務器須要處理的http請求只有併發請求總數的三分之一,

根據性能曲線測試,使服務器的併發請求數目控制在最佳的運行區間,獲取最佳的訪問請求延遲。

 

4.代碼優化

網站的業務邏輯實現代碼主要部署在應用服務器上,須要處理複雜的併發事務。

合理優化業務代碼,能夠很好地改善網站的性能。不一樣編程語言的代碼優化手段有不少,下面主要講幾個重要的方面。

1)多線程

多用戶併發訪問是網站的基本需求,大型網站的併發用戶數會達到數萬,單臺服務器的併發用戶數也會達到數百。

CGI編程時代,每一個用戶請求都會建立一個獨立的系統進程去處理。因爲線程比進程更輕量,更少佔用系統資源

切換代價更小,因此目前主要的Web應用服務器都採用多線程的方式響應併發用戶請求,所以網站開發自然就是多線程編程。

 

從資源利用的角度看,使用多線程的緣由主要有兩個:IO阻塞和多CPU。

當前線程進行IO處理的時候,會被阻塞釋放CPU以等待IO操做完成

因爲IO操做(無論是磁盤IO仍是網絡IO)一般都須要較長的時間,這時CPU能夠調度其它的線程進行處理。

前面咱們提到,理想的系統Load是既沒有進程(線程)等待也沒有CPU空閒,利用多線程IO阻塞與執行交替進行,能夠最大限度地利用CPU資源。

使用多線程的另外一個緣由是服務器有多個CPU,在這個連手機都有四核CPU的時代,除了最低配置的虛擬機,

通常數據中興的服務器至少16核CPU,要想最大限度地使用這些CPU,必須啓動多線程。

 

網站的應用程序通常被Web服務器容器管理,用戶請求的多線程也一般被Web服務器Web服務器容器管理,

但無論是Web容器管理的線程,仍是應用程序本身建立的線程,一臺服務器上啓動多少線程合適呢?

假設服務器上執行的都是相同類型的任務,針對該任務啓動的線程數有個簡化的估算公式可供參考:

啓動線程數=[任務執行時間/(任務執行時間-IO等待時間)] x CPU內核數

最佳啓動線程數和CPU內核數量成正比,和IO等待時間成正比

若是任務都是CPU計算型任務,那麼線程數最多不超過CPU內核數,由於啓動再多線程,CPU也來不及調度;

相反若是是任務須要等待磁盤操做,網絡響應,那麼多啓動線程有助於提升任務併發度,提升系統吞吐能力,改善系統性能

 

多線程編程一個須要注意的問題是線程安全問題,即多線程併發對某個資源進行修改,致使數據混亂

這也是缺少經驗的網站工程師最容易犯錯的地方,而線程安全Bug又難以測試和重現,

網站故障中,許多所謂偶然發生的「靈異事件「都和多線程併發問題有關。

對網站而言,無論有沒有進行多線程編程,工程師寫的每一行代碼都會被多線程執行,由於用戶請求是併發提交的,

也就是說,全部的資源——對象、內存、文件、數據庫,乃至另外一個線程均可能被多線程併發訪問。

 

編程上,解決線程安全的主要手段有以下幾點:

(1)將對象設計爲無狀態對象

所謂無狀態對象是指對象自己不存儲狀態信息(對象無成員變,或者成員變量也是無狀態對象),

這樣多線程併發訪問的時候就不會出現狀態不一致,Java Web開發中經常使用的Servlet對象就設計爲無狀態對象,

能夠被應用程序多線程併發調用處理用戶請求。而Web開發中經常使用的貧血模型對象都是些無狀態對象。

不過從面向對象設計的角度看,無狀態對象是一種不良設計。

(2)使用局部對象

即在方法內部建立對象,這些對象會被每一個進入該方法的線程建立

除非程序有意識地將這些對象傳遞給其它程序,不然不會出現對象被多線程訪問地情形。

(3)併發訪問資源時使用鎖

即多線程訪問資源的時候,一般鎖的方式使多線程併發操做轉化爲順序操做,從而避免資源被併發修改。

隨着操做系統和編程語言的進步,出現各類輕量級鎖,使得運行期線程獲取鎖和釋放鎖的代價都變得更小,

可是鎖致使線程同步順序執行,可能會對系統性能產生嚴重影響。

 

2)資源複用

系統運行時,要儘可能減小那些開銷很大的系統資源的建立和銷燬,好比數據庫鏈接、網絡通訊鏈接、線程、複雜對象等。

從編程角度,資源複用主要的兩種模式:單例(Singleton)和對象池(Object Pool)。

 

單例雖然是GoF經典設計模式中較多被詬病的一個模式,但因爲目前Web開發中主要使用貧血模式,

從Service到Dao都是些無狀態對象,無需重複建立,使用單例模式也就天然而然了。

事實上,Java開發經常使用的對象容器Spring默認構造的對象都是單例(須要注意的是Spring的單例是Spring容器管理的單例而不是用單例模式構造的單例)。

 

對象池模式經過複用對象實例,減小對象建立和資源消耗。

對於數據庫鏈接對象,每次建立鏈接,數據庫服務端都須要建立專門的資源以應對,所以頻繁建立關閉數據庫鏈接,

對數據庫服務器而言是災難性的,同時頻繁建立關閉鏈接也須要花費較長時間。

所以在實踐中,應用程序的數據庫鏈接基本都使用鏈接池(Connection Pool)的方式。

數據庫鏈接對象建立好之後,將鏈接對象放入對象池容器中,應用程序要鏈接的時候,

就從鏈接池中取一個空閒的鏈接使用,使用完畢再將對象歸還到對象池中便可,不須要在建立新的鏈接。

 

對於每一個Web請求(HTTP Request),Web應用程序都須要建立一個獨立的線程去處理,這方面,應用服務器也採用線程池的方式。

這些所謂的鏈接池、線程池,本質上都是對象池,即鏈接、線程都是對象,池管理方式也基本相同。

 

3)數據結構

早期關於程序的一個定義是,程序就是數據結構+算法,數據結構對於編程的重要性不言而喻。

在不一樣場景中合理使用恰當的數據結構,靈活組合各類數據結構讀寫和計算性能可極大優化程序的性能。

 

前面緩存部分已經描述過Hash表的基本原理,Hash表的讀寫性能在很大程度上依賴HashCode的隨機性

即HashCode越隨機散列,Hash表的衝突就越少讀寫性能也就越高,目前比較好的字符串Hash散列算法有Time33算法,

即對字符串逐字符迭代乘以33,求得Hash值,算法原型爲:

hash(i) = hash(i-1) * 33 + str[i]

Time33雖然能夠比較好的解決衝突,可是有可能類似字符串的HashCode也比較接近,

如字符串」AA「的HashCode是2210,字符串」AB「的HashCode是2211。

這在某些應用場景是不能接受的,這種狀況下,一個可行的方案是對字符串取信息指紋,再對信息指紋求HashCode,

因爲字符串微小的變化就能夠引發信息指紋的巨大不一樣,所以能夠得到較好的隨機散列。

 

4)垃圾回收

若是Web應用運行在JVM等具備垃圾回收功能的環境中,那麼垃圾回收可能對系統的性能特性產生極大影響。

理解垃圾回收機制有助於程序優化和參數調優,以及編寫內存安全的代碼。

 

以JVM爲例,其內存主要可劃分爲(heap)和堆棧(stack)。

堆棧用於存儲線程上下文信息,如方法參數、局部變量等

堆則是存儲對象的內存空間,對象的建立和釋放、垃圾回收就在這裏進行

經過對對象生命週期的觀察,發現大部分對象的生命週期都極其短暫,

這部分對象產生的垃圾應該被更快的收集,以釋放內存,這就是JVM分代垃圾回收。

在JVM分代垃圾回收機制中,將應用程序可用的堆棧空間分爲年輕代(Young Generation)和年老代(Old Generation),

又將年輕帶分爲Eden區(Eden Space)、From區和To區,新建對象老是在Eden區被建立,

當Eden區空間已滿,就觸發一次Young GC(Garbage Collection,垃圾回收),將還被使用的對象複製到From區,

這樣整個Eden區都是未被使用的空間,能夠繼續建立對象,當Eden區再次用完,再觸發一次Young GC,

將Eden區和From區還在被使用的對象複製到To區,下一次Young GC則是將Eden區和To區還未被使用的對象複製到From區。

所以通過屢次的young GC,某些對象會在From區和To區屢次複製,若是超過某個閥值,對象還未被釋放,則將該對象複製到Old Generation。

若是Old generation空間也已用完,那麼就會觸發Full GC,即所謂的全變量回收,全變量回收會對系統性能產生較大的影響,

所以應根據系統的業務特色和對象的生命週期合理設置Young generation和Old generation的大小,儘可能減小Full GC。

事實上,某些Web應用在整個運行期間能夠作到從不進行Full GC。

相關文章
相關標籤/搜索