<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      服務的擴展性

        在編寫一個應用時,我們常常考慮的是該應用應該如何實現特定的業務邏輯。但是在逐漸發展出越來越多的用戶后,這些應用常常會暴露出一系列問題,如不容易增大容量,容錯性差等等。這常常會導致這些應用在市場的拓展過程中無法快速地響應用戶的需求,并最終失去商業上的先機。

        通常情況下,我們將應用所具有的用來避免這一系列問題的特征稱為非功能性需求。相信您已經能夠從字面意義上理解這個名詞了:功能性需求用來提供對業務邏輯的支持,而非功能性需求則是一系列和業務邏輯無關,卻可能影響到產品后續發展的一系列需求。這些需求常常包括:高可用性(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.rzrgm.cn/loveis715/p/5097475.html

      商業轉載請事先與我聯系:silverfox715@sina.com

      posted @ 2016-01-03 23:23  loveis715  閱讀(13837)  評論(22)    收藏  舉報
      主站蜘蛛池模板: av日韩在线一区二区三区| 猫咪AV成人永久网站在线观看| 成人欧美日韩一区二区三区| 亚洲精品乱码久久久久久蜜桃图片 | 国产精品无码无需播放器| 我国产码在线观看av哈哈哈网站| 中文字幕无码av激情不卡| 天天躁夜夜躁狠狠喷水| 中文字幕亚洲国产精品| bt天堂新版中文在线| 亚洲成人免费一级av| 精品国产美女福到在线不卡| 午夜精品久久久久久| 久久精品av国产一区二区| 国产亚洲综合欧美视频| 欧美浓毛大泬视频| 好吊视频在线一区二区三区| 东京热tokyo综合久久精品| 精品女同一区二区三区在线| 国产av黄色一区二区三区| 欧美人禽杂交狂配| 亚洲日韩中文字幕在线播放| 一区二区三区四区自拍视频 | 二区三区亚洲精品国产| 少妇精品亚洲一区二区成人| 久久久亚洲欧洲日产国码αv| 亚洲综合成人av在线| 国产人妻精品午夜福利免费 | 国产三级精品福利久久| 精品久久久久久亚洲综合网 | 亚洲精品一区久久久久一品av| 亚洲欧美综合一区二区三区| 亚洲国产美女精品久久久久| 国产一区日韩二区欧美三区| 久久毛片少妇高潮| 国内精品无码一区二区三区| 亚洲中文字幕无码av永久| 变态另类视频一区二区三区| 国产一区二区三区粉嫩av| 这里只有精品在线播放| 亚洲精品一区二区制服|