高性能Web應用的優化技術

前端優化 并發策略 緩存優化 數據庫優化

如何構建一個高性能的Web應用, 其實是一個很大的話題。
之前在公司內部對此做過一個課題分享,將一些主流的優化技術羅列了一二,基于當時的PPT本文力圖在廣度方面延展開來,每個技術點若要深度挖掘需要大量的篇幅,待日后可系列寫寫。
部分內容參考《構建高性能Web站點》

# 一個HTTP請求過程

一個完整HTTP的請求過程,大約可以抽象為下面一張圖,相信比較容易看懂。
主要說下OSI七層網絡那里有個第七層的DNS負載均衡,和第四層的TCP/UDP層負載均衡(通過第三層的IP及第四層的端口)

HTTP請求.png

經典問題《當你打開一個URL到底發生了什么?》
http://igoro.com/archive/what-really-happens-when-you-navigate-to-a-url/

OSI七層協議.png

# 前端相關優化技術

該章節的前端優化大部分時候不僅僅是前端,還包括后端服務的配合。故聲明為前端“相關”優化技術。

1、瀏覽器緩存(強緩存 VS 協商緩存)

1.1、強緩存

強緩存是利用http的返回頭中的Expires或者Cache-Control兩個字段來控制的,用來表示資源的緩存時間。

Expires
該字段是http1.0時的規范,它的值為一個絕對時間的GMT格式的時間字符串,比如Expires:Mon,30 Jan 2017 23:59:59 GMT。這個時間代表著這個資源的失效時間,在此時間之前,即命中緩存。這種方式有一個明顯的缺點,由于失效時間是一個絕對時間,所以當服務器與客戶端時間偏差較大時,就會導致緩存混亂。

Cache-Control
該字段是http1.1時出現的header信息,主要是利用該字段的max-age值來進行判斷,它是一個相對時間,例如Cache-Control:max-age=3600,代表著資源的有效期是3600秒。cache-control除了該字段外,還有下面幾個比較常用的設置值:
no-cache:不使用本地緩存。需要使用緩存協商,先與服務器確認返回的響應是否被更改,如果之前的響應中存在ETag,那么請求的時候會與服務端驗證,如果資源未被更改,則可以避免重新下載。
no-store:直接禁止游覽器緩存數據,每次用戶請求該資源,都會向服務器發送一個請求,每次都會下載完整的資源。
public:可以被所有的用戶緩存,包括終端用戶和CDN等中間代理服務器。
private:只能被終端用戶的瀏覽器緩存,不允許CDN等中繼緩存服務器對其緩存。
Cache-Control與Expires可以在服務端配置同時啟用,同時啟用的時候Cache-Control優先級高。

1.2、 協商緩存

協商緩存就是由服務器來確定緩存資源是否可用,所以客戶端與服務器端要通過某種標識來進行通信,從而讓服務器判斷請求資源是否可以緩存訪問,這主要涉及到下面兩組header字段,這兩組搭檔都是成對出現的,即第一次請求的響應頭帶上某個字段(Last-Modified或者Etag),則后續請求則會帶上對應的請求字段(If-Modified-Since或者If-None-Match),若響應頭沒有Last-Modified或者Etag字段,則請求頭也不會有對應的字段。

Last-Modify/If-Modify-Since
瀏覽器第一次請求一個資源的時候,服務器返回的header中會加上Last-Modify,Last-modify是一個時間標識該資源的最后修改時間,例如Last-Modify: Thu,31 Dec 2037 23:59:59 GMT。
當瀏覽器再次請求該資源時,request的請求頭中會包含If-Modify-Since,該值為緩存之前返回的Last-Modify。服務器收到If-Modify-Since后,根據資源的最后修改時間判斷是否命中緩存。
如果命中緩存,則返回304,并且不會返回資源內容,并且不會返回Last-Modify。

ETag/If-None-Match
與Last-Modify/If-Modify-Since不同的是,Etag/If-None-Match返回的是一個校驗碼。ETag可以保證每一個資源是唯一的,資源變化都會導致ETag變化。服務器根據瀏覽器上送的If-None-Match值來判斷是否命中緩存。
與Last-Modified不一樣的是,當服務器返回304 Not Modified的響應時,由于ETag重新生成過,response header中還會把這個ETag返回,即使這個ETag跟之前的沒有變化。

ETag VS Last-Modified
ETag實體標簽(Entity Tag)的縮寫,在HTTP 1.1協議中并沒有規范如何計算ETag。ETag值可以是唯一標識資源的任何東西,如持久化存儲中的某個資源關聯的版本、一個或者多個文件屬性,實體頭信息和校驗值、也可以計算實體信息的散列值。
ETag關心的是實體內容,Last-Modified 關心的是實體修改時間
ETag的出現主要是為了解決幾個Last-Modified比較難解決的問題:

一些文件也許會周期性的更改,但是他的內容并不改變(僅僅改變的修改時間),這個時候我們并不希望客戶端認為這個文件被修改了,而重新請求;

  • 某些文件修改非常頻繁,比如在秒以下的時間內進行修改,(比方說1s內修改了N次),If-Modified-Since能檢查到的粒度是s級的,這種修改無法判斷(或者說UNIX記錄MTIME只能精確到秒);
  • 某些服務器不能精確的得到文件的最后修改時間。

Last-Modified與ETag是可以一起使用的,服務器會優先驗證ETag,一致的情況下,才會繼續比對Last-Modified,最后才決定是否返回304。

強緩存與協商緩存的區別可以用下表來表示:

緩存類型 獲取資源形式 狀態碼 發送請求到服務器
強緩存 從緩存取 200 (From Cache) 否,直接從本地緩存取
協商緩存 從緩存取 304 (Not Modified) 是,通過服務器響應來確定本地緩存是否仍有效

2、Keep-Alive 持久連接

Connection: Keep-Alive
Keep-Alive: timeout=5, max=100

非KeepAlive模式時,每個請求/應答客戶和服務器都要新建一個連接,完成之后立即斷開連接;當使用Keep-Alive模式(又稱持久連接、連接重用)時,Keep-Alive功能使客戶端到服務器端的連接持續有效,當出現對服務器的后繼請求時,Keep-Alive功能避免了建立或者重新建立連接。

HTTP 1.0中默認是關閉的,需要在HTTP頭加入"Connection: Keep-Alive",才能啟用Keep-Alive;HTTP 1.1中默認啟用Keep-Alive,如果加入"Connection: close "才關閉。

3、CDN緩存加速

CDN是什么,搞互聯網應該都曉得。這里主要說下CDN的幾個要點和注意事項:

  • CDN所緩存的內容類型包括web對象、可下載的對象(媒體文件、軟件、文檔)、應用程序和實時媒體流。
  • 工作原理分為DNS解析方式和非DNS解析方式(嵌入SDK),其中前者可能會有DNS劫持問題,后者會有很多不兼容問題。很多視頻播放軟件用的就是非DNS方式
  • 合理規劃CDN的回源頻率,既不對源站產生負擔,也不會緩存失效
  • 研究透徹CDN廠商的技術手冊,比如某些響應header不緩存導致緩存失效等

4、GZip壓縮

開啟Gzip ,那么服務器端響應后,會將HTML/JS/CSS等文本文件或者其他文件通過高壓縮算法將其壓縮,然后傳輸到客戶端,由客戶端的瀏覽器負責解壓縮、渲染及展現。

5、合并壓縮多個 CSS/JS

將多個CSS 或者 JavaScript 文件合并壓縮為一個小文件,減少HTTP請求次數,減輕網絡流量提高響應速度。

以上,可以滿足大部分的優化需求了,更多詳情可參見Google & Yahoo的最佳實踐。

更多詳情可參見Google & Yahoo的最佳實踐
https://developers.google.com/speed/docs/insights/rules
https://developer.yahoo.com/performance/rules.html

# IO 模型

計算機的重要工作之一便是負責各種設備的數據輸入/輸出,即Input/Output操作。
IO的類型根據設備的不同分為很多類型,比如內存IO,網絡IO,磁盤IO。

內存IO大部分情況不是我們需要關注的瓶頸點,它的速度遠遠高于網絡和磁盤IO,而后兩者才是我們需要重點關注的。當然如果你的系統需要像LMAX那種需要把性能調優到極致的架構,對于內存甚至CPU緩存的使用都是需要優化的,比如Disruptor magic cache line padding。

我們所關注的IO操作主要是網絡數據的接收和發送,以及磁盤文件的訪問,這里我們歸納為多種IO模型,它們的本質區別就是CPU的參與方式:如何協調高速的CPU與慢速的IO設備。

PIO VS DMA

簡單說下慢速IO設備和內存之間的數據傳輸方式。以磁盤為例,以前磁盤和內存之間的數據傳輸需要CPU控制,也就是如果讀取磁盤文件到內存中,數據要經過CPU存儲轉發,需要占用大量的CPU時間來讀取文件,造成文件訪問時系統幾乎停止響應,該模型當前已經基本沒有了。
DMA(Direct Memory Access,直接內存訪問)取代了PIO, 該模式下CPU只需要向DMA控制器下達指令,讓DMA控制器通過系統總線來傳送數據,傳輸完畢再通知CPU,大大降低了CPU占有率。

內存映射

內存映射是操作系統的提供的一種機制,可以減少這種不必要的數據拷貝,從而提高效率。它由mmap()將文件直接映射到用戶空間,mmap()并沒有進行數據拷貝,真正的數據拷貝是在缺頁中斷處理時進行的,由于mmap()將文件直接映射到用戶空間,所以中斷處理函數根據這個映射關系,直接將文件從硬盤拷貝到用戶空間,所以只進行了一次數據拷貝 ,比read進行兩次數據拷貝(多經過一次內核緩沖區)要少一次,因此,內存映射的效率要比read/write效率高。

直接IO

Linux2.6中,內存映射和直接訪問文件沒有本質上的差異。
引入內核緩沖區的目的在于提高磁盤文件的訪問性能。比如當需要讀取磁盤文件,如果文件內容已經在內核緩沖區,那么就不需要再次訪問磁盤;而寫入磁盤文件,實際上用戶進程只是寫到了內核緩沖區便標志進程已經寫成功,真正的寫入磁盤是通過一定的策略延遲處理的。
Linux提供了繞過內核緩沖區的需求支持,在open()系統調用中增加參數選項 O_DIRECT,這樣便避免了CPU和內存的多余開銷。

同步 VS 異步

同步和異步關注的是消息通信機制。所謂同步,就是在發出一個*** 調用 時,在沒有得到結果之前,該 調用 就不返回。但是一旦調用返回,就得到返回值了。換句話說,就是由 調用者 主動等待這個 調用 ***的結果。

而異步則是相反,*** 調用 在發出之后,這個調用就直接返回了,所以沒有返回結果。換句話說,當一個異步過程調用發出后,調用者不會立刻得到結果。而是在 調用 發出后, 被調用者 ***通過狀態、通知來通知調用者,或通過回調函數處理這個調用。

阻塞 VS 非阻塞

阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態.

阻塞調用是指調用結果返回之前,當前線程會被掛起。調用線程只有在得到結果之后才會返回。
非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞當前線程。

非阻塞IO一般指針對網絡IO有效,比如接收網絡數據的時候,如果網卡緩沖區中沒有可接收的數據,函數就及時返回告訴進程沒有數據可讀了,而阻塞IO在這里就會阻塞住只到網卡緩沖區有數據可以處理。
對于** 磁盤IO **,非阻塞IO無效果!!!

本文不詳細分析常見的幾種IO模型了(同步阻塞,同步非阻塞,多路IO復用,異步IO),大家可以自行了解。

同步阻塞IO(Blocking IO)

傳統的IO模型。首先IO等待是不可避免的,那么既然有等待就會有阻塞。

請注意,我們說的阻塞指的是當前發起IO操作的進程被阻塞,并不是CPU被阻塞,事實上沒有什么能讓CPU阻塞,CPU只知道拼命的計算,對于阻塞一無所知。

# 負載均衡

對于Web應用的水平擴展能力,負載均衡是關鍵的一環。主要的技術有:

DNS負載均衡:OSI第七層(應用層),一個域名可以對應多個IP(但最終只能解析出唯一一個)。性能極佳,但不靈活,生效延遲。

反向代理負載均衡: OSI第七層(應用層),定制轉發策略,分配流量權重等。

反向代理的功能遠不僅于此
HTTPS Offload / SSL Accelerator: SSL加解密處理,釋放服務器處理
能力。
Session會話保持: Session Stikcy, Session Copy.
頁面流量壓縮: GZip
靜態文件緩存: Nginx本地存儲,減少服務器帶寬負擔

IP負載均衡: OSI第四層(傳輸層), 通過對IP數據包的IP地址和端口進行修改達到負載均衡目的。 常見應用LVS-NAT/DR

不能在第三層做負載: 因為軟負載面向的對象應該是一個已經建 立連接的用戶,而不是一個孤零零的IP包, 所以LVS是需要關心 「連接」級別的狀態的。

反向代理

# 服務器并發策略

所有到達服務器的請求本質上都是封裝到IP包中(位于網卡接收緩沖區),Web服務器IO讀取請求數據,進行CPU計算,然后IO寫到發送緩沖區。服務器的并發策略就是讓IO操作和CPU計算盡量重疊。

  • 一個進程處理一個連接: prefork模式,安全獨立進程,子進程崩潰不會影響服務器本身,但受系統進程數的限制,并發連接數相應的受限。
  • 一個線程處理一個連接: worker 多線程+多進程混合方式,進程數較少則上下文切換消耗很大,此模式很少用
  • 一個進程處理多個連接: 結合多路復用IO才能發揮威力。
    進程數是不是越多越好?NO, 進程越多CPU的上下文切換成本越高
  • 一個線程處理多個連接: 結合異步IO。線程讀取的數據若不在緩沖區,則磁盤必須從物理設備讀取數據,這時候整個進程都是阻塞的。那多線程也就沒意義了。

# 緩存

緩存本質上就是避免重復計算,是一種犧牲數據的實時性以換取
效率的方式。更是互聯網應用各個環節都避不開的技術手段,用好緩存技術可以讓應用性能提高N個數量級,成為性能銀彈大殺器,用不好會使性能急劇下降,成為巨坑。

緩存 Cache VS 緩沖 Buffer
二者都是臨時存儲,都具備改善系統 I/O 吞吐量的能力,但是這兩個概念是有區別的。

緩沖區,一個用于存儲速度不同步的設備或優先級不同的設備之間傳輸數據的區域。通過緩沖區,可以使進程之間的相互等待變少,從而使從速度慢的設備讀入數據時,速度快的設備的操作進程不發生間斷。

業內經常會引用Phil Karlton的一句話:

There are only two hard things in Computer Science: 
cache invalidation and naming things.

大神 Martin Fowler 在此基礎上又加上了第三個難事

off-by-one errors(OBOE) 大小差一錯誤

關于緩存有如下幾個關鍵指標或者說概念,而上面提到的緩存失效只是其一:
**緩存命中(Cache Hits) **: 該項指標的調優是個很大的課題,不管是數據庫,還是CDN,亦或是Redis,都值得專門的章節來剖析。
沒有萬能的調優策略,只有符合場景的策略:

  • 選擇合理的緩存淘汰算法
  • 緩存粒度越小,命中率越高
  • 緩存有效時長,越長命中率越高
  • 緩存容量(內存/磁盤),越大命中率越高
  • 一致性哈希(Consistent Hashing):
    緩存集群服務器節點的增減會影響緩存的命中率
  • 緩存預熱
  • 空節點查詢問題(緩存穿透):
    設計適當的空節點過濾機制,對于一張簡單的記錄映射表,不可能的數據直接過濾掉。

cache-hit-ratio.gif

緩存淘汰/失效(Cache Eviction/Invalidation): 緩存淘汰算法無非是老套的
LRU(Least Recently Used)
LFU(Least Frequently Used)
FIFO(First In First Out)
緩存失效問題背后的根本問題則是異步系統的狀態同步問題。

緩存穿透(Cache Penetration): 指查詢一個一定不存在的數據(緩存或者數據庫都不存在),這樣第一次查詢發現緩存不存在,就會穿透到數據庫進行再次查詢,數據庫也查不到,那么緩存也不會記錄任何返回結果;這樣后續的對這個“不可能存在”數據的查詢,永遠會穿透緩存直接讀數據庫。
很多情況是來自于外部的惡意攻擊,是安全方面不得不重視的一個指標。

緩存并發/雪崩(Cache Concurrency): 緩存失效時大量并發請求對源數據服務器造成極大壓力,產生雪崩效應。可以結合業務場景適度的加鎖來避免。

緩存一致性(Cache Coherence):
如何保證緩存數據與持久化數據的一致性?
緩存失效時間控制:數據實時容忍度
數據變化觸發緩存更新:可以結合異步消息來處理
實時和體驗只能二者取其一,不能兼得,完全一致性是不可能的!

# 異步消息(削峰填谷)

  • 異步處理
  • 重試機制
  • 冪等性
  • 消息順序
  • 投遞/消費一次

# 數據庫優化

索引優化 Index
索引設計優化、索引使用優化、最左前綴原則、索引填充因子、索引碎片
數據庫連接線程池緩存 Pool
減少數據庫不斷的創建和銷毀連接的開銷,連接復用
分庫/分區/分表 Sharding
基本思想就要把一個數據庫切分成多個部分放到不同的數據庫(server)上,
從而緩解單一數據庫的性能問題。
缺點同樣明顯: 分布式事務、跨庫Join/聚合
讀寫分離(主從備份,主主備份)
數據同步的時效性受到考驗!

優化手段千千萬,你偏愛哪一支呢?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容