今天我們來談一談系統複雜度的根源之【高性能】 對性能的不懈追求一直是人類科技持續發展的核心動力。例如電腦,從電子管電腦到晶體管電腦,再到集成電路電腦,運算性能從每秒幾次提高到每秒幾億次。然而,隨著性能的提升,相應的方法和系統複雜度也逐漸增加。現代電腦CPU集成了數億顆晶體管,其邏輯複雜度和 ...
今天我們來談一談系統複雜度的根源之【高性能】
對性能的不懈追求一直是人類科技持續發展的核心動力。例如電腦,從電子管電腦到晶體管電腦,再到集成電路電腦,運算性能從每秒幾次提高到每秒幾億次。然而,隨著性能的提升,相應的方法和系統複雜度也逐漸增加。現代電腦CPU集成了數億顆晶體管,其邏輯複雜度和製造難度與最初的晶體管電腦相比,已經有了天壤之別。
軟體系統也呈現出類似的現象。近幾十年來,軟體系統性能得到了飛速發展,從最初的電腦僅能進行簡單科學計算,到如今Google能支撐每秒幾萬次的搜索。與此同時,軟體系統規模從單台電腦擴展到上萬台電腦;從最初的單用戶單任務的字元界面DOS操作系統,發展到現在的多用戶多任務的Windows 10圖形操作系統。
當然,技術進步帶來的性能提升,並不一定伴隨著複雜度的增長。例如,硬體存儲從紙帶、磁帶、磁碟發展到SSD,並未明顯增加系統複雜度。這是因為新技術逐步淘汰舊技術,我們可以直接使用新技術,而無需擔心系統複雜度的提升。只有那些不是用來取代舊技術,而是開拓全新領域的技術,才會給軟體系統帶來複雜性,因為軟體系統在設計時需要在這些技術之間進行判斷、選擇或組合。就像汽車的發明不能取代火車,飛機的出現也不能完全替代火車,所以在我們出行時,需要權衡選擇汽車、火車還是飛機,這個選擇過程相對複雜,涉及價格、時間、速度、舒適度等諸多因素。
軟體系統中性能提升帶來的複雜度主要體現在兩個方面:一方面是單台電腦內部為實現高性能所產生的複雜度;另一方面是多台電腦集群為實現高性能所引發的複雜度。
單機複雜度
電腦內部複雜度的關鍵在於操作系統。電腦性能的發展基本上是由硬體,尤其是CPU性能的發展所驅動的。著名的“摩爾定律”預測CPU處理能力每18個月翻一番;而充分發揮硬體性能的關鍵便是操作系統。因此,操作系統也是隨著硬體的發展而發展的,作為軟體系統的運行環境,操作系統的複雜度直接決定了軟體系統的複雜度。
操作系統與性能最相關的便是進程和線程。最早的電腦實際上是沒有操作系統的,僅具有輸入、計算和輸出功能。用戶輸入一個指令後,電腦完成操作。然而,大部分時間電腦都在等待用戶輸入指令,這種處理性能顯然是低效的,因為人的輸入速度遠不及電腦的運算速度。
為解決手工操作帶來的低效,批處理操作系統應運而生。簡單來說,批處理是先將要執行的指令預先編寫好(寫到紙帶、磁帶、磁碟等),形成一個指令清單,即我們常說的“任務”。將任務交給電腦執行,批處理操作系統負責讀取“任務”中的指令清單併進行處理。這樣,電腦執行過程中無需等待人工操作,從而大大提高性能。
儘管批處理程式大大提升了處理性能,但它存在一個明顯的缺點:電腦一次只能執行一個任務。如果某個任務需要從I/O設備(例如磁帶)讀取大量數據,在I/O操作過程中,CPU實際上是空閑的,而這段空閑時間本可以用於其他計算。
為進一步提升性能,人們發明瞭“進程”,將一個任務對應到一個進程,每個任務都有自己獨立的記憶體空間,進程間互不相關,由操作系統進行調度。當時的CPU尚無多核和多線程概念,為實現多進程並行運行,採用了分時方式,即將CPU時間劃分成許多片段,每個片段僅執行某個進程中的指令。儘管從操作系統和CPU角度看仍為串列處理,但由於CPU處理速度極快,用戶感覺上是多進程並行處理。
多進程要求每個任務都有獨立的記憶體空間,進程間互不相關,但從用戶角度看,若兩個任務在運行過程中能夠通信,則任務設計會更加靈活高效。為解決這個問題,各種進程間通信方式應運而生,包括管道、消息隊列、信號量、共用存儲等。
多進程使多任務能夠並行處理,但仍存在缺陷,單個進程內部僅能串列處理。實際上,許多進程內部的子任務並不要求嚴格按時間順序執行,也需要並行處理。例如,一個餐館管理進程,排位、點菜、買單、服務員調度等子任務必須並行處理,否則可能出現因某客人買單時間較長(如信用卡刷不出來)而導致其他客人無法點菜的情況。為解決這一問題,人們發明瞭線程,線程是進程內部的子任務,但這些子任務共用同一份進程數據。為保證數據的正確性,又發明瞭互斥鎖機制。有了多線程後,操作系統調度的最小單位變成了線程,而進程則成為操作系統分配資源的最小單位。
儘管多進程多線程讓多任務並行處理的性能大大提升,但本質上仍為分時系統,無法實現時間上真正的並行。解決這一問題的方法是讓多個CPU同時執行計算任務,從而實現真正意義上的多任務並行。目前這樣的解決方案有三種:SMP(對稱多處理器結構)、NUMA(非一致存儲訪問結構)和MPP(海量並行處理結構)。其中,SMP是我們最常見的,當前流行的多核處理器即採用SMP方案。
如今的操作系統發展已經相當成熟,若要完成一個高性能的軟體系統,需要考慮多進程、多線程、進程間通信、多線程併發等技術點。然而,這些技術並非最新的就是最好的,也不是非此即彼的選擇。在進行架構設計時,需要投入大量精力來結合業務進行分析、判斷、選擇和組合,這一過程同樣頗為複雜。舉一個簡單的例子:Nginx可以採用多進程或多線程,JBoss則採用多線程;Redis採用單進程,而Memcache則採用多線程,儘管這些系統都實現了高性能,但其內部實現卻大相徑庭。
集群的複雜度
儘管電腦硬體性能迅速發展,但與業務發展速度相比,仍顯得力不從心,特別是進入互聯網時代後,業務發展速度遠超硬體發展速度。例如:
2016年“雙11”支付寶每秒峰值達到12萬筆支付。
2017年春節微信紅包收發紅包每秒達到76萬個。
要支持如支付和紅包等複雜業務,單機性能無論如何都無法支撐。因此,必須採用機器集群的方式來實現高性能。例如,支付寶和微信這類規模的業務系統,後臺系統的機器數量均達到了萬台級別。
通過大量機器提升性能,並非僅僅是增加機器那麼簡單。讓多台機器協同完成高性能任務是一項複雜的任務。以下針對常見的幾種方式進行簡要分析:
1.任務分配
任務分配意味著每台機器都能處理完整的業務任務,將不同任務分配給不同機器執行。為實現有效的任務分配,負載均衡技術應運而生。負載均衡可以通過硬體或軟體實現,其目標是將任務平均分配到各個機器上,確保系統資源得到充分利用,從而提高整體性能。
2.數據分片
數據分片是指將數據切分成多個部分,每個部分分配到一個或多個機器上。這種方式可以實現數據的水平擴展,提高數據處理能力。例如,資料庫分片技術可以將一個大型資料庫分割成多個小型資料庫,每個小型資料庫承擔部分數據處理任務。
3.數據副本
為提高數據可靠性和可用性,可在多台機器上創建數據的副本。當一臺機器出現故障時,其他具有數據副本的機器可以立即接管服務,確保業務不間斷。數據副本技術在分散式存儲系統中尤為重要,如分散式文件系統、分散式資料庫等。
4.任務並行
任務並行是指將一個大任務分解成多個小任務,併在多台機器上同時執行。這種方式可以顯著減少任務執行時間,提高處理能力。例如,MapReduce是一種著名的任務並行計算模型,可以將大數據處理任務分解成多個小任務,在集群中並行執行,最後將結果彙總輸出。
總之,集群技術為應對業務需求提供了強大的支持,但實現高性能集群需要考慮任務分配、數據分片、數據副本、任務並行等多種技術,並根據具體業務需求進行合理選擇和組合。隨著雲計算的普及,集群技術將更加成熟,未來亦可期待更多創新和發展。
我從最簡單的一臺伺服器變兩台伺服器開始,來講任務分配帶來的複雜性,整體架構示意圖如下。
從圖中可以看到,1台伺服器演變為2台伺服器後,架構上明顯要複雜多了,主要體現在:
需要增加一個任務分配器,這個分配器可能是硬體網路設備(例如,F5、交換機等),可能是軟體網路設備(例如,LVS),也可能是負載均衡軟體(例如,Nginx、HAProxy),還可能是自己開發的系統。選擇合適的任務分配器也是一件複雜的事情,需要綜合考慮性能、成本、可維護性、可用性等各方面的因素。
任務分配器和真正的業務伺服器之間有連接和交互(即圖中任務分配器到業務伺服器的連接線),需要選擇合適的連接方式,並且對連接進行管理。例如,連接建立、連接檢測、連接中斷後如何處理等。
任務分配器需要增加分配演算法。例如,是採用輪詢演算法,還是按權重分配,又或者按照負載進行分配。如果按照伺服器的負載進行分配,則業務伺服器還要能夠上報自己的狀態給任務分配器。
這一大段描述,即使你可能還看不懂,但也應該感受到其中的複雜度了,更何況還要真正去實踐和實現。
上面這個架構只是最簡單地增加1台業務機器,我們假設單台業務伺服器每秒能夠處理5000次業務請求,那麼這個架構理論上能夠支撐10000次請求,實際上的性能一般按照8折計算,大約是8000次左右。
如果我們的性能要求繼續提高,假設要求每秒提升到10萬次,上面這個架構會出現什麼問題呢?是不是將業務伺服器增加到25台就可以了呢?顯然不是,因為隨著性能的增加,任務分配器本身又會成為性能瓶頸,當業務請求達到每秒10萬次的時候,單台任務分配器也不夠用了,任務分配器本身也需要擴展為多台機器,這時的架構又會演變成這個樣子。
這個架構比2台業務伺服器的架構要複雜,主要體現在:
任務分配器從1台變成了多台(對應圖中的任務分配器1到任務分配器M),這個變化帶來的複雜度就是需要將不同的用戶分配到不同的任務分配器上(即圖中的虛線“用戶分配”部分),常見的方法包括DNS輪詢、智能DNS、CDN(Content Delivery Network,內容分髮網絡)、GSLB設備(Global Server Load Balance,全局負載均衡)等。
任務分配器和業務伺服器的連接從簡單的“1對多”(1台任務分配器連接多台業務伺服器)變成了“多對多”(多台任務分配器連接多台業務伺服器)的網狀結構。
機器數量從3台擴展到30台(一般任務分配器數量比業務伺服器要少,這裡我們假設業務伺服器為25台,任務分配器為5台),狀態管理、故障處理複雜度也大大增加。
上面這兩個例子都是以業務處理為例,實際上“任務”涵蓋的範圍很廣, 可以指完整的業務處理,也可以單指某個具體的任務。例如,“存儲”“運算”“緩存”等都可以作為一項任務,因此存儲系統、運算系統、緩存系統都可以按照任務分配的方式來搭建架構。此外,“任務分配器”也並不一定只能是物理上存在的機器或者一個獨立運行的程式,也可以是嵌入在其他程式中的演算法,例如Memcache的集群架構。
2.任務分解
通過任務分配的方式,我們能夠突破單台機器處理性能的瓶頸,通過增加更多的機器來滿足業務的性能需求,但如果業務本身也越來越複雜,單純只通過任務分配的方式來擴展性能,收益會越來越低。例如,業務簡單的時候1台機器擴展到10台機器,性能能夠提升8倍(需要扣除機器群帶來的部分性能損耗,因此無法達到理論上的10倍那麼高),但如果業務越來越複雜,1台機器擴展到10台,性能可能只能提升5倍。造成這種現象的主要原因是業務越來越複雜,單台機器處理的性能會越來越低。為了能夠繼續提升性能,我們需要採取第二種方式: 任務分解。
繼續以上面“任務分配”中的架構為例,“業務伺服器”如果越來越複雜,我們可以將其拆分為更多的組成部分,我以微信的後臺架構為例。
通過上面的架構示意圖可以看出,微信後臺架構從邏輯上將各個子業務進行了拆分,包括:接入、註冊登錄、消息、LBS、搖一搖、漂流瓶、其他業務(聊天、視頻、朋友圈等)。
通過這種任務分解的方式,能夠把原來大一統但複雜的業務系統,拆分成小而簡單但需要多個系統配合的業務系統。從業務的角度來看,任務分解既不會減少功能,也不會減少代碼量(事實上代碼量可能還會增加,因為從代碼內部調用改為通過伺服器之間的介面調用),那為何通過任務分解就能夠提升性能呢?
主要有幾方面的因素:
簡單的系統更加容易做到高性能
系統的功能越簡單,影響性能的點就越少,就更加容易進行有針對性的優化。而系統很複雜的情況下,首先是比較難以找到關鍵性能點,因為需要考慮和驗證的點太多;其次是即使花費很大力氣找到了,修改起來也不容易,因為可能將A關鍵性能點提升了,但卻無意中將B點的性能降低了,整個系統的性能不但沒有提升,還有可能會下降。
可以針對單個任務進行擴展
當各個邏輯任務分解到獨立的子系統後,整個系統的性能瓶頸更加容易發現,而且發現後只需要針對有瓶頸的子系統進行性能優化或者提升,不需要改動整個系統,風險會小很多。以微信的後臺架構為例,如果用戶數增長太快,註冊登錄子系統性能出現瓶頸的時候,只需要優化登錄註冊子系統的性能(可以是代碼優化,也可以簡單粗暴地加機器),消息邏輯、LBS邏輯等其他子系統完全不需要改動。
既然將一個大一統的系統分解為多個子系統能夠提升性能,那是不是劃分得越細越好呢?例如,上面的微信後臺目前是7個邏輯子系統,如果我們把這7個邏輯子系統再細分,劃分為100個邏輯子系統,性能是不是會更高呢?
其實不然,這樣做性能不僅不會提升,反而還會下降,最主要的原因是如果系統拆分得太細,為了完成某個業務,系統間的調用次數會呈指數級別上升,而系統間的調用通道目前都是通過網路傳輸的方式,性能遠比系統內的函數調用要低得多。我以一個簡單的圖示來說明。
從圖中可見,當系統拆分為2個子系統時,用戶訪問需要1次系統間請求和1次響應;當系統拆分為4個子系統時,系統間請求次數從1次增加到3次;若繼續拆分為100個子系統,為完成某次用戶訪問,系統間請求次數將達到99次。
為簡化描述,我們抽象出一個最簡模型:假設這些系統通過IP網路連接,在理想情況下,一次請求和響應在網路上耗時為1ms,業務處理本身耗時為50ms。我們還假設系統拆分對單個業務請求性能沒有影響。因此,當系統拆分為2個子系統時,處理一次用戶訪問耗時為51ms;而系統拆分為100個子系統時,處理一次用戶訪問耗時竟然達到了149ms。
儘管在一定程度上,系統拆分有助於提升業務處理性能,但性能提升是有限的。當系統未拆分時,業務處理耗時為50ms,系統拆分後業務處理耗時不可能僅為1ms。因為業務處理性能仍然受限於業務邏輯本身,而在業務邏輯沒有發生重大變化的情況下,理論上性能具有一個上限。系統拆分可以讓性能接近這一極限,但無法突破它。因此,任務分解所帶來的性能收益具有一定的度,任務分解不是越細越好。對於架構設計而言,如何把握這一粒度便顯得至關重要。
小結
今天我向你講述了軟體系統中高性能帶來的複雜度主要體現的兩方面;
一是單台電腦內部為了高性能帶來的複雜度; 二是多台電腦集群為了高性能帶來的複雜度