在編寫一個應用時,我們常常考慮的是該應用應該如何實作特定的業務邏輯。但是在逐漸發展出越來越多的使用者後,這些應用常常會暴露出一系列問題,如不容易增大容量,容錯性差等等。這常常會導緻這些應用在市場的拓展過程中無法快速地響應使用者的需求,并最終失去商業上的先機。
通常情況下,我們将應用所具有的用來避免這一系列問題的特征稱為非功能性需求。相信您已經能夠從字面意義上了解這個名詞了:功能性需求用來提供對業務邏輯的支援,而非功能性需求則是一系列和業務邏輯無關,卻可能影響到産品後續發展的一系列需求。這些需求常常包括:高可用性(High Avalibility),擴充性(Scalability),維護性(Maintainability),可測試性(Testability)等等。
而在這些非功能性需求中,擴充性可能是最有趣的一種了。是以在本文中,我們将對如何編寫一個具有高可擴充性的應用進行講解。
什麼是擴充性
假設我們編寫了一個Web應用,并将其置于共有雲上以向使用者提供服務。該應用的創意非常新穎,并在短時間内就吸引了大量的使用者。但是由于我們在編寫該應用時并沒有期望它來處理這麼多使用者的請求,是以它的運作速度越來越慢,甚至可能出現服務沒有響應的情況。頻繁發生這種事情的結果就是,使用者将無法忍受該應用經常性地當機,并将尋找其它類似應用來獲得類似的服務。
該應用所缺少的能夠根據負載來對處理能力進行适當擴充的能力便是應用的擴充性,而其衡量的标準則是處理能力擴充的簡單程度。如果您的應用在添加了更多記憶體後就能運作得更好,或者通過添加一個額外的服務執行個體就能解決服務執行個體過載的問題,那麼我們就可以說該應用的擴充性非常好。如果為了處理更多的負載而不得不重寫整個應用,那麼應用的開發者就需要在多多注意應用的擴充性了。
較好的擴充性不僅可以省卻您重寫應用的麻煩,更重要的是,它會幫助您在市場的争奪中獲得先機。試想一下,如果您的應用已經出現了處理能力不夠的苗頭,卻沒有适當的解決方案來提高整個系統的處理能力,那麼您能做的事情隻能是重新編寫一個具有更高處理能力的具有同一個功能的應用。在該段時間内,您的應用的處理能力顯得越來越捉襟見肘。而展現在客戶層面上的,則是您的應用的響應速度越來越慢,甚至有時都無法正常工作。在新應用上線之前,您的應用将逐漸地流失客戶。而這些流失的客戶則很有可能變成類似軟體的忠實客戶,進而使得您的産品失去了市場競争的先機。反過來,如果您的應用具有非常良好的擴充性,而您的競争對手并沒有跟上使用者的增長速度,那麼的應用就有了完全超越甚至壓制競争對手的可能。
當然,一個成功的應用不應該僅僅擁有高擴充性,而是應該在一系列非功能性需求上都做得很好。例如您的應用不應該有太多的Bug,也不應該有特别嚴重的Bug,以避免由于這些Bug導緻您的使用者無法正常使用應用。同時您的應用需要擁有較好的使用者體驗,這樣才能讓這些使用者非常容易地熟悉您的應用,并産生使用者粘性。
當然,這些非功能性需求并不僅僅局限在使用者的角度。例如從開發團隊的角度來講,一個軟體的可測試性常常決定了測試組的工作效率。如果一個應用需要在幾十台機器上逐一安裝部署,那麼每次測試人員對新版本的驗證都需要幾個小時甚至成天的時間才能準備完畢。測試組也就很自然地成為了該軟體開發組中效率最為低下的一部分。為此我們就需要招入大量的測試人員,大大地增加了應用的整體開銷。
總的來說,一個應用所具有的非功能性需求非常多,如完整性(Completeness),正确性(Correctness),可用性(Availability),可靠性(Reliability),安全(Security),擴充性(Scalability),性能(Performance)等等。而這些需求都會對如何分析,設計以及編碼提出一定的要求。不同的非功能性需求所提出的要求常常會發生沖突。而到底哪個非功能性需求更為重要則需要根據您所編寫的應用類型來決定。例如在編寫一個大規模Web應用的時候,擴充性,安全以及可用性較為重要,而對于一個實時應用來說,性能以及可靠性則占據上風。在這篇文章中,我們的讨論将主要集中在擴充性上。是以其所提出的一系列建議可能會對其它的非功能性需求産生較大的影響。而到底如何取舍則需要讀者根據應用的實際情況自行決定。
應用的擴充方法
好的,讓我們重新回到擴充性這個話題上來。導緻一個軟體需要擴充的最根本原因實際上還是其所需要面對的吞吐量。在使用者的一個請求到達時,服務執行個體需要對它進行處理并将其轉化為對資料的操作。在這個過程中,服務執行個體以及資料庫都需要消耗一定的資源。如果使用者的請求過多進而導緻應用中的某個組成所無法應對,那麼我們就需要想辦法提高該組成的資料處理能力。
提高資料處理能力的方法主要分為兩類,那就是縱向擴充及橫向擴充。而這兩種方法所對應的操作就是Scale Up以及Scale Out。
縱向擴充表示在需要處理更多負載時通過提高單個系統處理能力的方法來解決問題。最簡單的情況就是為該系統提供更為強大的硬體。例如如果資料庫所在的伺服器執行個體隻有2G記憶體,進而導緻了資料庫不能高效地運作,那麼我們就可以通過将該伺服器的記憶體擴充至8G來解決這個問題:
上圖所展示的就是通過添加記憶體進行縱向擴充,以解決資料庫所在服務執行個體IO過高的情況:當運作資料庫服務的伺服器所包含的記憶體不能加載資料庫中所存儲的最為常見的資料時,其會不斷地從硬碟中讀取持久化到磁盤中的記憶體頁面,進而導緻資料庫的性能大幅下降。而在将伺服器的記憶體擴充到8G的情況下,那些常用資料就能夠長時間地駐留在記憶體中,進而使得資料庫所在服務執行個體的磁盤IO迅速回複正常。
除了通過硬體方法來提高單個服務執行個體的性能之外,我們還可以通過優化軟體的執行效率來完成應用的縱向擴充。最簡單的示例就是,如果原有的服務實作隻能使用單線程來處理資料,而不能同時利用伺服器執行個體中所包含的多個CPU核心,那麼我們可以通過将算法更改為多線程來充分利用CPU的多核計算能力,成倍地提高服務的執行效率。
但是縱向擴充并非總是最正确的選擇。影響我們選擇的最常見因素就是硬體的成本。我們知道,硬體的價格通常與該硬體所處的定位有關。如果一個硬體是目前市場上的主流配置,那麼由于它已經大量出貨,是以平攤的研發成本在每件硬體中已經變得非常小。反過來,如果一個硬體是剛剛投入市場的高端産品,那麼每件硬體所包含的研發成本将會非常多。是以縱向擴充的投入性能比曲線常常如下所示:
也就是說,在單個執行個體優化到一定程度以後,再花費大量的時間和金錢來對單個執行個體的性能進行提高已經沒有太多的意義了。在這個時候,我們就需要考慮橫向擴充,也就是使用多個服務執行個體來一起提供服務。
就以一個線上的圖像處理服務為例。由于圖像處理是一個非常消耗資源的計算過程,是以單個伺服器常常無法滿足大量使用者所發送的請求:
就像上圖中所展示的那樣,雖然我們的伺服器已經安裝了4個CPU,但是在單個伺服器執行個體提供服務的情況下,CPU使用率還是一直處于警戒線之上。如果我們再在應用中添加一個相同的伺服器來共同處理使用者的請求,那麼每台伺服器的負載将會降到原有負載的一半左右,進而使得CPU使用率保持在警戒線之下。
在這種情況下,該服務所提供的一系列其它功能也随之得到了擴充。例如對處理結果進行儲存的功能的性能也将變成原來的兩倍。隻是由于我們暫時并不需要這種擴充,是以該部分性能的增強實際上是毫無用處的,甚至造成了服務資源的浪費:
從上圖中可以看到,在沒有橫向擴充之前,橙色組成的負載已經達到了90%,接近單個服務執行個體的極限。為了解決這個問題,我們再引入一個伺服器執行個體來分擔工作。但是這樣會導緻其它幾個本來資源使用率就已經不高的組成的使用率降得更低。而更為正确的擴充方式則是隻擴充橙色組成:
從上面的講解中可以看出,橫向擴充實際上包含了很多種方式。相應地,《The Art of Scalability》一書則介紹了一個橫向擴充所需要遵守的AKF擴充模型。根據AKF擴充模型,橫向擴充實際上包含了三個次元,而橫向擴充解決方案則是這三個次元上所做工作的結合:
上圖中展示了AKF擴充模型的最通用的表示形式。在該圖中,原點O表示的是應用執行個體并沒有能力執行任何橫向擴充,而隻能通過縱向擴充來提高它的服務能力。如果您的系統朝着某個坐标軸的方向前進,那麼它就将得到一定程度的橫向擴充能力。當然,這三個坐标軸并不互斥,是以您的應用可能同時擁有XYZ三個軸向的擴充能力:
現在就讓我們來看一下AKF擴充模型中各個坐标軸的意義。首先要講解的就是X軸。在AKF擴充模型中,X軸表示的是應用可以通過部署更多的服務執行個體來解決擴充性的問題。在這種情況下,原本需要少量服務執行個體處理的大量負載就可以通過新添加的其它服務執行個體分擔,進而擴大了系統容量,降低了單個服務執行個體的壓力。
我們剛剛提到過,一個服務的擴充性可以同時由多個軸向的擴充性共同組成,是以在該服務中,這種X軸方向的擴充性不僅僅存在于服務這個層次上,更可以由子服務,甚至服務組成的擴充性來共同完成:
請注意上圖中的橙色方塊。在該服務中,橙色方塊作為一個子服務來向整個服務提供特定功能。在需要擴充時,我們可以通過添加一個新的橙色子服務執行個體來解決橙色服務負載過大的問題。是以就整個服務而言,其X軸的橫向擴充能力并不是通過重新部署整套服務來完成的,而是對獨立的子服務進行擴容。
相信您會問:既然隻通過添加新的服務或子服務執行個體就能夠完成對服務容量的擴充,那麼我們還需要其它兩個軸向的橫向擴充能力麼?
答案是肯定的。首先,最為現實的問題就是服務運作場景的限制。例如在對服務進行X軸橫向擴充的時候,我們常常需要一個負載平衡服務。在《企業級負載平衡簡介》一文中我們已經說過,負載平衡伺服器常常具有一定的性能限制。是以橫向擴充并非全無止境。除此之外,我們也看到了橫向擴充有時是使用在子服務上的,而将一個大的服務分割為多個子服務,本身也是沿着其它軸向的橫向擴充。
Y軸橫向擴充的意義則在于将所有的工作根據資料的類型或業務邏輯進行劃分。而就一個Web服務而言,Y軸橫向擴充所做的最主要工作就是将一個Monolith服務劃分為一系列子服務,進而使不同的子服務獨立工作并擁有獨立地進行橫向擴充的能力。這一方面可以将原本一個服務所處理的所有請求分擔給一系列子服務執行個體來運作,更可以讓您根據應用的實際運作情況來對某個成為系統瓶頸的子服務進行X軸橫向擴充,避免由于對整個服務進行X軸橫向擴充所造成的資源浪費:
這種組織各個子服務的方式被稱為Microservice。使用Microservice組織子服務還可以幫助您實作一系列其它非功能性需求,如高可用性,可測試性等等。具體内容詳見《Microservice架構模式簡介》一文。
相較而言,執行Y軸擴充要比執行X軸擴充困難一些。但是其常常會使得其它一系列非功能性需求具有更高的品質。
而在Z軸上的橫向擴充可能是大家所最不熟悉的情況。其表示需要根據使用者的某些特性對使用者的請求進行劃分。例如使用基于DNS的負載平衡。
當然,到底您的服務需要實作什麼程度的X,Y,Z軸擴充能力則需要根據服務的實際情況來決定。如果一個應用的最終規模并不大,那麼隻擁有X軸擴充能力,或者有部分Y軸擴充能力即可。如果一個應用的增長非常迅速,并最終演變為對吞吐量有極高要求的應用,那麼我們就需要從一開始就考慮這個應用在X,Y,Z軸的擴充能力。
服務的擴充
好了,介紹了那麼多理論知識,相信您已經迫不及待地想要了解如何令一個應用具有良好的擴充性了吧。那好,讓我們首先從服務執行個體的擴充性說起。
我們已經在前面介紹過,對服務進行擴充主要有兩種方法:橫向擴充以及縱向擴充。對于服務執行個體而言,橫向擴充非常簡單:無非是将服務分割為衆多的子服務并在負載平衡等技術的幫助下在應用中添加新的服務執行個體:
上圖展示了服務執行個體是如何按照AKF擴充模型進行橫向擴充的。在該圖的最頂層,我們使用了基于DNS的負載平衡。由于DNS擁有根據使用者所在位置決定距離使用者最近的服務這一功能,是以使用者在DNS查找時所得到的IP将指向距離自己最近的服務。例如一個處于美國西部的使用者在通路Google時所得到的IP可能就是64.233.167.99。這一功能便是AKF擴充模型中的Z軸:根據使用者的某些特性對使用者的請求進行劃分。
接下來,負載平衡伺服器就會根據使用者所通路位址的URL來對使用者的請求進行劃分。例如使用者在通路網頁搜尋服務時,服務叢集需要使用左邊的虛線方框中的服務執行個體來為使用者服務。而在通路圖檔搜尋服務時,服務叢集則需要使用右邊虛線方框中的服務執行個體。這則是AKF擴充模型中的Y軸:根據資料的類型或業務邏輯來劃分請求。
最後,由于使用者所最常使用的服務就是網頁搜尋,而單個服務執行個體的性能畢竟有限,是以服務叢集中常常包含了多個用來提供網頁搜尋服務的服務執行個體。負載平衡伺服器會根據各個服務執行個體的能力以及服務執行個體的狀态來對使用者的請求進行分發。而這則是沿着AKF擴充模型中的X軸進行擴充:通過部署具有相同功能的服務執行個體來分擔整個負載。
可以看到,在負載平衡伺服器的幫助下,對應用執行個體進行橫向擴充是非常簡單的事情。如果您對負載平衡功能比較感興趣,請檢視我的另一篇博文《企業級負載平衡簡介》。
相較于服務的橫向擴充,服務的縱向擴充則是一個常常被軟體開發人員所忽視的問題。橫向擴充誠然可以提供近乎無限的系統容量,但是如果一個服務執行個體本身的效能就十分低下,那麼這種無限的橫向擴充常常是在浪費金錢:
就像上圖中所展示的那樣,一個應用當然可以通過部署4台具有同樣功能的伺服器來為使用者提供服務。在這種情況下,搭建該服務的開銷是5萬美元。但是由于應用實作本身的品質不高,是以這四台伺服器的資源使用率并不高。如果一個肯于動腦的軟體開發人員能夠仔細地分析服務執行個體中的系統瓶頸并加以改正,那麼公司将可能隻需要購買一台伺服器,而員工的個人能力及薪水都會得到提升,并可能得到一筆額外的嘉獎。如果該員工為應用所添加的縱向擴充性足夠高,那麼該應用将可以在具有更高性能的伺服器上運作良好。也就是說,單個服務執行個體的縱向擴充性不僅僅可以充分利用現有硬體所能提供的性能,以輔助降低搭建整個服務的花費,更可以相容具有更強資源的伺服器。這就使得我們可以通過簡單地調整伺服器設定來完成對整個服務的增強,如添加更多的記憶體,或者使用更高速的網絡等方法。
現在就讓我們來看看如何提高單個服務執行個體的擴充性。在一個應用中,服務執行個體常常處于核心位置:其接受使用者的請求,并在處理使用者請求的過程中從資料庫中讀取資料。接下來,服務執行個體會通過計算将這些資料庫中得到的資料糅合在一起,并作為對使用者請求的響應将其傳回。在整個處理過程中,服務執行個體還可能通過服務端緩存取得之前計算過程中已經得到的結果:
也就是說,服務執行個體在運作時常常通過向其它組成發送請求來得到運作時所需要的資料。由于這些請求常常是一個阻塞調用,服務執行個體的線程也會被阻塞,進而影響了單個線程在服務中執行的效率:

從上圖中可以看到,如果我們使用了阻塞調用,那麼在調用另一個組成以獲得資料的時候,調用方所在的線程将被阻塞。在這種情況下,整個執行過程需要3份時間來完成。而如果我們使用了非阻塞調用,那麼調用方在等待其它組成的響應時可以執行其它任務,進而使得其在4份時間内可以處理兩個任務,相當于提高了50%的吞吐量。
是以在編寫一個高吞吐量的服務實作時,您首先需要考慮是否應該使用Java所提供的非阻塞IO功能。通常情況下,由非阻塞IO組織的服務會比由阻塞IO所編寫的服務慢,但是其在高負載的情況下的吞吐量較非阻塞IO所編寫的服務高很多。這其中最好的證明就是Tomcat對非阻塞IO的支援。
在較早的版本中,Tomcat會在一個請求到達時為該請求配置設定一個獨立的線程,并由該線程來完成該請求的處理。一旦該請求的處理過程中出現了阻塞調用,那麼該線程将挂起直至阻塞調用傳回。而在該請求處理完畢後,負責處理該請求的線程将被送回到線程池中等待對下一個請求進行處理。在這種情況下,Tomcat所能并行處理的最大吞吐量實際上與其線程池中的線程數量相關。反過來,如果将線程數量設定得過大,那麼作業系統将忙于處理線程的管理及切換等一系列工作,反而降低了效率。而在一些較新版本中,Tomcat則允許使用者使用非阻塞IO。在這種情況下,Tomcat将擁有一系列用來接收請求的線程。一旦請求到達,這些線程就會接收該請求,并将請求轉給真正處理請求的工作線程。是以在新版Tomcat的運作過程中将隻包括幾十個線程,卻能夠同時處理成千上萬的請求。當然,由于非阻塞IO是異步的,而不是在調用傳回時就立即執行後續處理,是以其處理單個請求的時間較使用阻塞IO所需要的時間長。
是以在服務少量的使用者時,使用非阻塞IO的Tomcat對于單個請求的響應時間常常是Tomcat的2倍以上,但是在使用者數量是成千上萬個的時候,使用非阻塞IO的Tomcat的吞吐量則非常穩定:
是以如果想要提高您的單個服務性能,首先您需要保證您在Tomcat等Web容器中正确地使用了非阻塞模式:
<Connector connectionTimeout="20000" maxThreads="1000" port="8080"
protocol="org.apache.coyote.http11.Http11NioProtocol" redirectPort="8443"/>
當然,使用非阻塞IO并不僅僅是通過配置Tomcat就完成了。試想在一個子服務實作中調用另一個子服務的情況:如果在調用子服務時調用方被阻塞,那麼調用方的一個線程就被阻塞在那裡,而不能處理其它待處理的請求。是以在您的應用中包含了較長時間的的阻塞調用時,您需要考慮使用非阻塞方式組織服務的實作。
在使用非阻塞方式組織服務之前,您最好詳細地閱讀《Enterprise Integration Pattern》。Spring旗下的項目Spring Integration則是Enterprise Integration Pattern在Spring體系中的一種實作。因為它是在是一個非常大的話題,是以我會在其它博文中對它們進行簡單地介紹。
在通過使用非阻塞模式提高了并發連接配接數之後,我們就需要考慮是否其它硬體會成為單個服務執行個體的瓶頸了。首先,更大的并發會導緻更大的記憶體占用。是以如果您所開發的應用對記憶體大小較為敏感,那麼您首先要做的就是為系統添加記憶體。而且在您的記憶體敏感應用的實作中,記憶體管理也會變成您需要考慮的一項任務。雖然說很多語言,如Java等,已經通過提供垃圾回收機制解決了野指針,記憶體洩露等一系列問題,但是在這些垃圾回收機制啟動的時候,您的服務會暫時挂起。是以在服務實作的過程中,您需要考慮通過一些技術來盡量避免記憶體回收。
另外一個和硬體有關的話題可能就是CPU了。一個伺服器常常包含多個CPU,而這些CPU可以包含多個核,是以在該台服務執行個體上常常可以同時運作十幾個,甚至幾十個線程。但是在實作服務時,我們常常忽略了這種資訊,進而導緻某些服務隻能由少數幾個線程并行執行。通常情況下,這都是因為服務過多地通路同一個資源,如過多地使用了鎖,同步塊,或者是資料庫性能不夠等一系列原因。
還有一個需要考慮的事情就是服務的動靜分離。如果一個應用需要提供一系列靜态資源,那麼那些常用的Servlet容器可能并不是一個最優的選擇。一些輕量級的Web伺服器,如nginx在服務靜态資源時的效率就将明顯高于Apache等一系列動态内容伺服器。
由于這篇文章的主旨并不是為了講解如何編寫一個具有較高性能的服務,是以對于上面所述的各種增強單個服務性能的技巧将不再進行深入講解。
除了從服務自身下功夫來增強一個服務執行個體的縱向擴充性之外,我們還有一個重要的用來提高服務執行個體工作效率的武器,那就是服務端緩存。這些緩存通過将之前得到的計算結果記錄在緩存系統中,進而盡可能地避免對該結果再次進行計算。通過這種方式,服務端緩存能大大地減輕資料庫的壓力:
那它和服務的擴充性有什麼關系呢?答案是,如果服務端緩存能夠減輕系統中每個服務的負載,那麼它實際上相當于提高了單個服務執行個體的工作效率,減少了其它組成對擴容的需求,變相地增加了各個相關組成的擴充性。
現在市面上較為主流的服務端緩存主要分為兩種:運作于服務執行個體之上并與服務執行個體處于同一個程序之内的緩存,以及在服務執行個體之外獨立運作的緩存。而後一種則是現在較為流行的解決方案:
從上圖中可以看出,由于程序内緩存與特定的應用執行個體綁定,是以每個應用執行個體将隻能通路特定的緩存。這種綁定一方面會導緻單個服務執行個體所能夠通路的緩存容量變得很小,另一方面也可能導緻不同的緩存執行個體中存在着備援的資料,降低了緩存系統的整體效率。相較而言,由于獨立緩存執行個體是獨立于各個應用伺服器執行個體運作的,是以應用服務執行個體可以通路任意的緩存執行個體。這同時解決了服務執行個體能夠使用的緩存容量過小以及備援資料這兩個問題。
如果您希望了解更多的有關如何搭建服務端緩存的知識,請檢視我的另一篇博文《Memcached簡介》。
除了服務端緩存之外,CDN也是一種預防服務過載的技術。當然,它的最主要功能還是提高距離服務較遠的使用者通路服務的速度。通常情況下,這些CDN服務會根據請求分布及實際負載等衆多因素在不同的地理區域内搭建。在提供服務時,CDN會從服務端取得服務的靜态資料,并緩存在CDN之内。而在一個距離該服務較遠的使用者嘗試使用該服務時,其将會從這些CDN中取得這些靜态資源,以提高加載這些靜态資料的速度。這樣伺服器就不必再處理從世界各地所發來的對靜态資源的請求,進而降低了伺服器的負載。
資料庫的擴充性
相較于服務執行個體,資料庫的擴充則是一個更為複雜的話題。我們知道,不同的服務對資料的使用方式常常具有很大的差異。例如不同的服務常常具有非常不同的讀寫比,而另一些服務則更強調擴充性。是以如何對資料庫進行擴充并沒有一個統一的方法,而常常決定于應用自身對資料的要求。是以在本節中,我們将采取由下向上的方法講解如何對資料庫進行擴充。
通常情況下,對一個話題自上而下的講解常常能夠形成較好的知識系統。在使用該方式對問題進行講解的時候,我們将首先提出問題,然後再以該問題為中心講解組成該問題的各個子問題。在講解中我們需要逐一地解決這些子問題,并将這些子問題的解決方案進行關聯和比較。通過這種方式,讀者常常能夠更清晰地認識到各個解決方案的優點和缺點,進而能夠根據問題的實際情況對解決方案進行取舍。這種方法較為适合問題較為簡單并且清晰的情況。
而在問題較為複雜,包含情況較多的情況下,我們就需要将這些問題拆分為子問題,并在講清楚各個子問題之後再去分析整個問題如何通過這些子問題解決方案合作解決。
那麼如何将資料庫的擴充性分割為子問題呢?在決定一個資料庫應該擁有哪些特性時,常常用來作為評判标準的就是CAP理論。該理論指出我們很難保證資料庫的一緻性(Consistency),可用性(Availability)以及分區容錯性(Partition tolerance):
是以一系列資料庫都選擇了其中的兩個特性來作為其實作的重點。例如常見的關系型資料庫主要保證的是資料的一緻性及資料的可用性,而并不強調對擴充性非常重要的分區容錯性。這也便是資料庫的橫向擴充成為業界難題的一個原因。
當然,如果您的應用對一緻性或可用性的要求并不是那麼高,那麼您就可以選擇将分區容錯性作為重點的資料庫。這些類型的資料庫有很多。例如現在非常流行的NoSQL資料庫大多都将分區容錯性作為一個實作重點。
是以在本節中,我們将會以關系型資料庫作為重點進行講解。又由于對關系型資料庫進行橫向擴充常常較縱向擴充更為困難,是以我們将首先講解如何對關系型資料庫進行橫向擴充。
首先,最為常見也最為簡單的縱向擴充方法就是增加關系型資料庫所在服務執行個體的性能。我們知道,資料庫在運作時會将其所包含的資料加載在記憶體之中,而且最常通路的資料是否存在于記憶體之中是資料庫是否運作良好的關鍵。如果資料庫所在的服務執行個體能夠根據實際負載提供足夠的記憶體,以承載所有最常被通路的資料,那麼資料庫的性能将得到充分地發揮。是以在執行縱向擴充的第一步就是要檢查您的資料庫所在的服務執行個體是否擁有足夠的資源。
當然,僅僅從硬體入手是不夠的。在前面的章節中已經介紹過,縱向擴充需要從兩個方面入手:硬體的增強,以及軟體的優化。就資料庫本身而言,其最重要的保證運作性能的組成就是索引。在當代的各個資料庫中,索引主要分為聚簇索引以及非聚簇索引兩種。這兩種索引能夠加速對具有特定特征的資料的查找:
是以在資料庫優化過程中,索引可以說是最為重要的一環。從上圖中可以看出,如果一個查找能夠通過索引來完成,而不是通過逐個查找資料庫中所擁有的記錄來進行,那麼整個查找隻需要分析組成索引的幾個節點,而不是周遊資料庫所擁有的成千上萬條記錄。這将會大大地提高資料庫的運作性能。
但是如果索引沒有存在于記憶體中,那麼資料庫就需要從硬碟中将它們讀取到記憶體中再進行操作。這明顯是一個非常慢的操作。是以為了您的索引能夠正常工作,您首先要保證資料庫運作所在的服務執行個體擁有足夠的記憶體。
除了保證擁有足夠的記憶體之外,我們還需要保證資料庫的索引自身沒有過多的浪費記憶體。一個最常見的索引浪費記憶體的情況就是Index Fragmentation。也就是說,在經過一系列添加,更新和删除之後,資料庫中的資料在存儲中的實體結構中将變得不再規律。這主要分為兩種:Internal Fragmentation,即實體結構中可能存在着大量空白;External Fragmentation,即這些資料在實體結構中并不是有序排列的。Internal Fragmentation意味着索引所包含節點的增加。這一方面導緻我們需要更大的空間來存儲索引,進而占用更多的記憶體,另一方面也會讓資料尋找所需要周遊的節點數量增加,進而導緻系統性能的下降。而External Fragmentation則意味着從磁盤順序讀取這些資料時需要硬碟重新進行尋址等操作,也會顯著降低系統的執行性能。還有一個需要考慮的有關External Fragmentation的問題則是是否我們的服務與其它服務使用了共享磁盤。如果是,那麼其它服務對于磁盤的使用會導緻External Fragmentation的問題無法從根本上解決,巡道操作将常常發生。
另外一個常用的對索引進行優化的方法就是在非聚簇索引中通過INCLUDE子句包含特定列,以加快某些請求語句的執行速度。我們知道,聚簇索引和非聚簇索引的差别主要就存在于是否包含資料。如果從聚簇索引中執行資料的查找,那麼在找到對應的節點之後,我們就已經可以從該節點中得到需要查找的資料。而如果我們的查找是在非聚簇索引中進行的,那麼我們得到的則是目标資料所在的位置。為了找到真正的資料,我們還需要進行一次尋址操作。而在通過INCLUDE子句包含了所需要資料的情況下,我們就可以避免這次尋址,進而提高了查找的性能。
但是需要注意的是,索引是資料庫在其本身所擁有的資料之外額外建立的資料結構,是以其實際上也需要占用記憶體。在插入及删除資料的時候,資料庫同樣需要維護這些索引,以保證索引和實際資料的一緻性,是以其會導緻資料庫插入及删除操作性能的下降。
還有一個需要考慮的則是通過正确地設定Fill Factor來盡量避免Page Split。在常見的資料庫中,資料是記錄在具有固定大小的頁中。當我們需要插入一條資料的時候,目标頁中的可用空間可能已經不足以再添加一條新的資料。此時資料庫會添加一個新的頁,并将資料從一個頁分到這兩個頁中。在該過程中,資料庫不僅僅要添加及修改資料頁本身,更需要對IAM等頁進行更改,是以是一個較為消耗資源的操作。FillFactor是一個用來控制在葉頁建立時每頁所填充的百分比的全局設定。在設定了FillFactor的基礎之上,使用者還可以設定PAD_INDEX選項,來控制非葉頁也使用FillFactor來控制資料的填充。一個較高的FillFactor會使資料更加集中,由此擁有更高的讀取性能。而一個較低的FillFactor則對寫入較為友好,因為其防止了Page Split。
除了上面所述的各種方法之外,您還可以通過其它一系列資料庫功能來提高性能。這其中最重要的當然是各個資料庫所提供的執行計劃(Execution Plan)。通過執行計劃,您可以看到您正在執行的請求是如何被資料庫執行的:
由于如何提高單個資料庫的性能是一個龐大的話題,而我們的文章主要集中在如何提高擴充性,是以我們在這裡不再對如何提高資料庫的執行性能進行詳細的介紹。
反過來,由于單個伺服器的性能畢竟有限,是以我們并不能無限地對關系型資料庫進行縱向擴充。是以在必要條件下,我們需要考慮對關系型資料庫進行橫向擴充。而将AKF橫向擴充模型施行在關系型資料庫之上後,其各個軸的意義則如下所示:
現在就跟我來看看各個軸的含義。在AKF模型中,X軸表示的是應用可以通過部署更多的服務執行個體來解決擴充性的問題。而由于關系型資料庫要管理資料的讀寫并保證資料的一緻性,是以在X軸上的擴充将不能簡單地通過部署額外的資料庫執行個體來解決問題。在進行X軸擴充的時候,這些資料庫執行個體常常擁有不同的職責并組成特定的拓撲結構。這就是資料庫的Replication。
而相較于X軸,資料庫AKF模型中的Y軸和Z軸則較為容易了解。AKF模型中的Y軸表示的是将所有的工作根據資料的類型或業務邏輯進行劃分,而Z軸則表示根據使用者的某些特性對使用者的請求進行劃分。這兩種劃分實際上都是要将資料庫中的資料劃分到多個資料庫執行個體中,是以它們對應的則是資料庫的Partition。
讓我們先看看資料庫的Replication。簡單地說,資料庫的Replication表示的就是将資料存儲在多個資料庫執行個體中。讀請求可以在任意的資料庫執行個體上執行,而一旦某個資料庫執行個體上發生了資料的更新,那麼這些更新将會自動複制到其它資料庫執行個體上。在資料複制的過程中,資料源被稱為Master,而目标執行個體則被稱為Slave。這兩個角色并不是互斥的:在一些較為複雜的拓撲結構中,一個資料庫執行個體可能既是Master,又是Slave。
在關系型資料庫的Replication中,最為常見的拓撲模型就是簡單的Master-Slave模型。在該模型中,對資料的讀取可以在任意的資料庫執行個體上完成。而在需要對資料進行更新的時候,資料将隻能寫入特定的資料庫執行個體。此時這些資料的更改将沿着單一的方向從Master向Slave進行傳遞:
在該模型中,資料讀取的工作是由Master和Slave共同處理的。是以在上圖中,每個資料庫的讀負載将是原來的一半左右。但是在寫入時,Master和Slave都需要執行一次寫操作,是以各個資料庫執行個體的寫負載并沒有降低。如果讀負載逐漸增大,我們還可以加入更多的Slave節點以分擔讀負載:
相信您現在已經清楚了,關系型資料庫的橫向擴充主要是通過加入一系列資料庫執行個體來分擔讀負載來完成的。但是有一點需要注意的是,這種寫入傳遞關系是靠Master和Slave中的一個獨立的線程來完成的。也就是說,一個Master擁有多少個Slave,它的内部就需要維持多少個線程來完成對屬于它的Slave的更新。由于在一個大型應用中常常可能包含上百個Slave執行個體,是以将這些Slave都歸于同一個Master将導緻Master的性能急劇下降。
其中一個解決方法就是将其中的某些Slave轉化為其它Slave的Master,并将它們組織成為一個樹狀結構:
但是Master-Slave模型擁有一個缺點,那就是有單點失效的危險。一旦作為Master的資料庫執行個體失效了,那麼整個資料庫系統,至少是以該Master節點為根的子系統将會失效。
而解決該問題的一種方法就是使用多Master的Replication模型。在該模型中,每個Master資料庫執行個體除了可以将資料同步給各個Slave之外,還可以将資料同步給其它的Master:
在這種情況下,我們避免了單點失效的問題。但是如果兩個資料庫執行個體對同一份資料更新,那麼它們将産生資料沖突。當然,我們可以通過将對資料的劃分為毫不相幹的多個子集并由每個Master節點負責某個特定子集的更新的方式來防止資料沖突。
從上圖中可以看到,使用者對資料的寫入會根據特定條件來配置設定到不同的資料庫執行個體上。接下來,這些寫入會同步到其它執行個體上,進而保持資料的一緻性。但是既然我們能将這些資料獨立地切割為各個子集,那麼我們為什麼不去嘗試一下資料庫的Partition呢?
簡單地說,資料庫的Partition就是将資料庫中需要記錄的資料劃分為一系列子集,并由不同的資料庫執行個體來記錄這些資料子集所包含的資料。通過這種方法,對資料的讀取以及寫入負載都會根據資料所在的資料庫執行個體來進行劃分。而這也就是資料庫沿AKF擴充模型的Y軸進行橫向擴充的方法。
在執行資料庫的Partition時,資料庫原有的資料将被切分到不同的資料庫執行個體中。每個資料庫執行個體将隻包含原資料庫中幾個表的資料,進而将對整個資料庫的通路切分到不同的資料庫執行個體中:
但是在某些情況下,對資料庫中的資料按表切分并不能解決問題。切分完畢後的某個資料庫執行個體仍然可能承擔了過多的負載。那麼此時我們就需要将該資料庫再次切分。隻是這次我們所切分的是資料庫中的資料行:
在這種情況下,我們在對資料進行操作之前首先需要執行一次計算來決定資料所在的資料庫執行個體。
然而資料庫的Partition并不是沒有缺點。最常見的問題就是我們不能通過同一條SQL語句操作不同資料庫執行個體中記錄的資料。是以在決定對資料庫進行切分之前,您首先需要仔細地檢查各個表之間的關系,并确認被分割到不同資料庫中的各個表沒有過多的關聯操作。
好了。至此為止,我們已經講解了如何建立具有可擴充性的服務執行個體,緩存以及資料庫。相信您已經對如何建立一個具有高擴充性的應用有了一個較為清晰的認識。當然,在撰寫本文的過程中,我也發現了一系列可以繼續講解的話題,如Spring Integration,以及對資料庫Replication以及Partition(Sharding)的講解。在有些方面(如資料庫),我并不是專家。但是我會盡我所能把本文所寫的知識點一一陳述清楚。
轉載請注明原文位址并标明轉載:http://www.cnblogs.com/loveis715/p/5097475.html
商業轉載請事先與我聯系:[email protected]