組件分離
DNS負載均衡
不同的web內容分布到不同的服務器上,并劃分子域,利用DNS將請求自然轉移到不同的服務器上。主要可以分為兩大內容:
1.動態內容,CPU、IO密集型
2.靜態內容,IO密集型
通過在DNS中配置多個A記錄,將請求轉移到集群中不同的服務器,這有助于具有地域性問題的大型web站點上,DNS可以使用戶就近訪問相應的web服務器。像BIND這樣的DNS服務軟件提供豐富的調度策略。但是,如果集群中的主機出現故障的話,需要更新DNS緩存,這通常需要一定的時間。另外,客戶端也可以通過設置host來繞開DNS調度。
跨域共享cookie:將cookie的范圍擴大到父域。
HTTP重定向
通過使客戶端重定向,來分散和轉移請求壓力,比如一些下載服務通常都有幾個鏡像服務器。
分布式緩存
無法使用頁面級別緩存時,需要考慮直接緩存數據,比如使用memcached作為緩存。此時,需要考慮并發寫memcached的問題。 另外,當memcached橫向規模擴大,服務器數量增加時,需要一種對應算法,能夠使應用程序知道應該鏈接哪個memcached服務器(比如,取模運算)。分布式緩存能夠自動重建緩存,不必擔心down機。
負載均衡
負載均衡就是將請求分散,這涉及到應當如何設計調度策略,以讓集群發揮最大的性能。當集群中的主機能力相當時應當盡量平均調度,能力不均時應當能者多勞。隨著問題的復雜,要時刻關注調度的性能,不要讓調度成為性能瓶頸。
反向代理負載均衡
反向代理服務器工作在HTTP層,類似代理服務器,與普通的代理服務器不同的是,服務器在代理的后端,而不是客戶端在代理的后端,這類似于NAT,只是NAT工作在網絡層。同樣是負載均衡,反向代理服務器強調"轉發"而不是"轉移",因為它不僅要轉發客戶端的請求,還要轉發服務端的響應。可以用作反向代理服務器的軟件有Nginx、lighttp、Apache,另外目前也有一些專業的代理轉發設備能夠工作在應用層,例如A10。
使用代理轉發要注意以下問題:
由于反向代理的轉發特性,使得代理本身很可能成為性能瓶頸。一般對于CPU密集型請求,使用代理比較合適,如果是IO密集型的話,這種集群方式很可能無法發揮最大性能
在代理上要開啟健康檢查,及時發現集群中的故障機,從而調整轉發策略,這通常比DNS方式實時性更好
黏滯會話:對于啟動session保存用戶信息,或者后端服務器使用動態內容緩存的應用,必須將用戶在一段會話中的的請求保持在同一臺服務器上。代理服務器一般支持類似的配置。然而,盡量不要使應用過于本地化,比如可以使用cookie保存用戶數據,或者分布式Session或分布式緩存。
IP負載均衡
字面上看,便是利用網絡層進行請求轉發,類似NAT網關。然而,使用網關轉發在帶寬上可能出現瓶頸,因為出口只有一個,所以出口的帶寬要求較高。Linux中的Netfilter模塊可以通過iptables的配置。比如:對外網端口8001的請求轉發給內網某臺服務器,而對外網端口8002的請求轉發給內網另一臺服務器。這種方式簡單易行,但是無法對調度做太多配置。LVS-NAT同樣是Linux中的在網絡層進行轉發的方式,與Netfilter不同,它支持一些動態調度算法,比如最小鏈接、帶權重的最小鏈接、最短期望延遲等。
直接路由
直接路由是通過調度器修改數據包的目的MAC地址,轉發請求數據包,但是響應數據包可以直接發送給外網的方式。這樣做顯而易見的好處就是無需擔心網關瓶頸,但是實際的服務器和調度服務器都需要鏈接在WAN交換機上,并且擁有獨立的外網IP地址。
這種方式的工作原理略微復雜:
首先每臺服務器都需要設置一個IP別名,這個IP別名是面向客戶端的一個虛擬IP,只有代理服務器對這個IP別名的ARP請求做出響應,這樣客戶端發給這個IP的請求包首先會到代理服務器。然后代理服務器將這個請求包的目的MAC地址填寫為實際服務器的MAC地址(通過某種調度算法決定目的服務器),由于目標服務器也具有這個IP別名,因此,轉發過來的數據包能夠被實際的服務器接收并處理。最后由于數據包的源IP地址還是客戶端請求的IP地址,因此,實際的服務器將通過交換機直接將響應包轉發給客戶端而無需通過代理服務器。
Linux下可以通過LVS-DR實現直接路由方式
IP隧道
IP隧道的意思是,調度器將原始的IP數據包封裝在新的IP數據包中,以實現調度,實際的服務器可以將響應數據包直接轉發給用戶端。
共享文件系統
對于一些簡單的提供文件下載的服務(包括html中靜態資源等),自然要考慮利用集群來減壓,但是如何使這些資源在集群中的主機上同步呢。
NFS
一種方案是讓這些主機從同一個地方取數據。比如采用NFS(Network File System),基于PRC。這種方式簡單易行,但是由于NFS服務器本身的磁盤吞吐率,或者并發處理能力以及帶寬等問題,往往很有局限性。
冗余分發
另一個方案就是在主機上冗余存儲資源,這樣主機無需訪問共享文件系統,只需讀取本地磁盤上的資源即可。但是這也帶來了一個同步的問題,如何同步這些數據呢:
主動分發式,還分為單級分發和多級分發,分發可以借助SCP、SFTP、HTTP擴展協議WebDAV
單級分發:通過一次分發,就達到目的,這樣的方案簡單易行,但是性能瓶頸會出現在磁盤壓力和網絡帶寬,難以擴展
多級分發:通過多次分發,才達到目的地,這樣的方案能夠分散磁盤壓力和網絡帶寬壓力,而且容易擴展,壞處是成本高
被動同步式容易理解,可以使用rsync,rsync同步時是根據最后更新時間進行判定是否需要同步的條件的,因此,如果一個文件夾中有的文件數量太多的話,rsync掃描的時間就很長了,可以通過給文件夾設置最后更新時間,并合理的規劃文件目錄,來加快rsync的掃描時間。即使不使用rsync,自己開發同步程序也可以借助這樣的思想來提升性能。
分布式文件系統
分布式文件系統工作在用戶進程層面上,它是一個管理文件的平臺,內部維護冗余,檢索,追蹤、調度等工作,通常包含一個物理層面的組織結構和邏輯層面的組織結構。物理層面的組織結構由分布式文件系統自行維護,邏輯層面的組織結構面向用戶。其中"追蹤器"起到了關鍵的作用。
MogileFS就是一個開源分布式文件系統,用Perl編寫,包含追蹤器、存儲節點、管理工具,它使用MySQL分布式文件系統的所有信息、使用WebDAV實現文件復制。其他著名的還有Hadoop。
每個文件由一個key定義,需要讀取文件時,指定一個key,追蹤器會返回一個實際的路徑,在訪問這個地址即可獲得文件。甚至可以將這個key對應的path用分布式緩存緩存起來,這樣可以減少追蹤器的查詢開銷,但這樣也會失去分布式文件系統的調度策略的優越性。另外,可以利用支持reproxy的反向代理服務器(比如:Perlbal)讓路徑重定向的工作由反向代理服務器完成。
數據庫擴展
1.主從復制,讀寫分離
這種方式是指利用數據庫的復制或鏡像功能,同時在多臺數據庫上保存相同的數據,并且將讀操作和寫操作分開,寫操作集中在一臺主數據庫上,讀操作集中在多臺從數據庫上,對于讀取比寫更多的站點適合使用這種方式。如果不想在應用程序層面維護這種分離映射,那么可以使用數據庫反向代理來自動完成對讀寫的分離。
2.垂直分區
對于不需要進行聯合查詢的數據表可以分散到不同的數據庫服務器上,這稱為垂直分區;當然每個分區自身也可以使用讀寫分離。
3.水平分區
將同一個表的記錄拆分到不同的表甚至是服務器上,稱為水平分區,這往往需要一個穩定的算法來保證讀取時能正確從不同的服務器上取得數據,比如簡單的對ID取模、范圍劃分、亦或者是保存映射關系。 也可以使用類似代理的產品spock。
緩存
構建高性能web站點時,拋開基礎架構(數據庫分區的問題也包括在基礎架構中了),在應用程序、編碼層面主要要考慮的問題就是緩存的設計,合理的緩存設計可以使提供動態網頁服務的網站性能大幅度提高。當然,在架構階段設計緩存解決方案,絕非簡單的技術問題,需要從業務出發,再結合各種技術。下面按照一次HTTP請求的順序,對每個環節的緩存設計從技術角度進行討論。
1.客戶端緩存
可以利用客戶端瀏覽器的緩存機制,來減少瀏覽器對服務端的請求次數(當然在服務端進行圖片等資源合并,并結合css圖片定位技術,也可以減少HTTP請求),利用好HTTP的緩存協商,可以設計出靈活的客戶端緩存方案。在HTTP頭中下面的內容與緩存協商有關:
Last-Modified:動態頁面通過主動推送該值,暗示瀏覽器在下次請求同一個url的時候,優先使用If-Modified-Since值與服務端進行緩存協商,如果緩存沒有過期,那么服務端可以不用重新計算動態網頁,通過返回304通知瀏覽器。網站的靜態資源往往使用這種方法。但是該方法有一個缺點:有時,文件的最后更改時間雖然改了,但是內容卻沒有變,這樣無法充分發揮瀏覽器緩存的能力。
ETag:Web服務器為每個url生成一個散列值,增加在HTTP頭的ETag標記中,瀏覽器會優先使用If-None-Match加上這個散列值來協商緩存過期。通過對靜態文件的內容進行md5變換,可以生成散列值,這樣可以彌補Last-Modified的不足。但是隨之帶來的是服務端md5變換的計算開銷。
Expires:上述兩種方式,雖然可以使服務端多少避免了反復的動態網頁解析和計算,但瀏覽器還是必須通過HTTP請求來進行協商,并沒有真正意義上減少請求的次數。通過在HTTP頭中添加Expires標記可以明確的告知瀏覽器過期形式,瀏覽器會徹底減少請求的次數。
2.反向代理緩存
在web服務器前端,還有反向代理服務器緩存。反向代理服務器本質上就是代理服務器,只是將外網的請求轉發給內網的web服務器處理,他們都工作在應用層,能夠理解HTTP協議。正向代理服務器具有HTTP緩存、HTTP過濾等功能,反向代理服務器同樣具有HTTP緩存的能力,而且還具備一定程度上的安全性。一切HTTP友好的動態程序同樣能夠很好的在反向代理服務器上實現緩存。重量級的squid、輕量級的varnish、甚至是Nginx這樣的web服務器軟件,都可以勝任反向代理服務。
上述的代理服務器軟件產品,通過各種配置可以緩存基于HTTP協議的web響應。
3.Web服務器緩存
Web服務器有可能支持基于url的緩存(基于key-value對),這類似反向代理緩存。緩存通常可以通過配置存儲在內存或磁盤上,在緩存有效期的問題上,通常是基于HTTP協議中的頭部信息判斷。但是使用這樣的機制需要注意:
動態程序會可能變得依賴于特定的web服務器 注意編寫面向HTTP緩存友好的動態程序,會使你的動態程序更有生命力 web 服務器還具有緩存文件描述符(類似句柄)的能力,這樣可以減少文件的open操作,同樣是一種減少系統調用的措施,這對于一些小文件有些效果,因為文件越小花在open上的開銷將越來越占有重要的比例。
4.應用程序緩存
應用程序本身可以對動態內容進行緩存,這可以體現在三個層面:
動態腳本緩存:每次腳本解析都需要消耗一定的時間,為了加快這個速度,一些服務器端腳本語言都支持動態腳本的預編譯,比如我們熟悉的ASP.NET、JSP都有所謂的中間語言,解析這些中間語言會快很多。
動態腳本框架支持的緩存:一些動態腳本框架支持緩存,同樣在選型的時候要關注一下這些框架的緩存機制和原理,比如緩存如何存放,過期如何設置,是否支持局部緩存等。
應用程序自身實現緩存:應用程序根據特定的業務需求獨立設計緩存。
在緩存設計的具體技術上有以下幾點:
緩存在內存,這種方式的優點就是減少了磁盤的讀寫,但是內存有限,單機不易擴展。
緩存在分布式緩存,這種方式的優點是減少了磁盤讀寫,并提高了可靠性和擴展性,但是由于需要網絡IO,性能稍遜于單機緩存。
局部頁面緩存,對于一些頁面無法完整緩存的,可以考慮局部緩存。
靜態化,將網站靜態化可以極大的提高性能,因為用戶的請求不需要動態腳本處理。
服務器系統能力的制約因素:
這部分內容對于所有的服務器(無論是代理服務器、web服務器還是其他),都具有普遍適用的意義。
多進程、多線程的選擇和調度:進程切換和線程切換都需要一定的系統開銷,通常使用多線程模型的web服務器軟件比使用多進程,具備更優的性能。
系統調用:一些需要從用戶模式切換到內核模式的函數調用可以稱為系統調用,比如:打開文件。系統調用會有一定程度上的開銷,減少系統調用是可以加快處理速度的程序設計細節。
TCP鏈接保持:可以通過保持TCP鏈接來減少服務端和客戶端之間的創建和關閉TCP鏈接的操作。HTTP中的Connection:Keep-Alive就有這樣的功能
IO模型:由于CPU的速度遠遠比IO快,IO延遲往往成為性能瓶頸,因此,IO模型十分重要。
各種IO模型:
PIO:CPU直接干預磁盤和內存的數據交互,即無論是數據從內存到磁盤還是磁盤到內存都要經過CPU寄存器。這樣的模型,可想而知,CPU有很多時間都需要等待慢速設備。
DMA(Direct Memory Access):CPU通過向DMA控制器發送指令來控制處理數據,數據處理完之后通知CPU,這可以很大程度上釋放CPU資源。
同步阻塞I/O:對于進程來說,一些系統調用為了同步IO,會不同程度上阻塞進程,比如accept、send、read等。
同步非阻塞I/O:對于進程來說,一些系統調用可以在調用完之后立即返回,告知進程IO是否就緒,避免阻塞進程。
多路I/O就緒通知:對于同步非阻塞I/O的方式,進程仍然需要輪詢文件描述符(句柄)來得知哪些IO就緒了,而多路I/O就緒通知將這個過程改成回調通知。
內存映射:將文件與內存的某塊地址空間相映射,這樣可以想寫內存一樣寫文件。當然這種方式本質上跟寫文件沒有什么區別。
直接I/O:在用戶進程地址空間和磁盤中間通常都會有操作系統管轄的內核緩沖區,當寫入文件時,一般是寫入這個緩沖區,然后由一些延遲策略來寫入磁盤。這樣做可以提高寫效率。但是對于諸如數據庫這樣的應用來說,往往希望自己管理讀寫緩存,避免內核緩沖區的無畏內存浪費。Linux的open函數支持O_DIRECT參數來進行直接IO。
sendfile:如果web服務器想發送一個文件,將會經歷如下過程:打開文件,從磁盤中讀取文件內容(這通常涉及到內核緩沖區數據復制到用戶進程),然后進程通過socket發送文件內容(這通常設計到用戶進程數據復制到網卡內核緩沖區),可以看到重復的數據復制是可以避免的。sendfile可以支持直接從文件內核緩沖區復制到網卡內核緩沖區。