效能之殤:從馮·諾依曼瓶頸談起

机器之心發表於2018-12-18

本文作者根據自己的認知,討論了人們為提高效能做出的種種努力,包括硬體層面的 CPU、RAM、磁碟,作業系統層面的併發、並行、事件驅動,軟體層面的多程序、多執行緒,網路層面的分散式等。

本文共分為七個部分:

  1. 天才馮·諾依曼與馮·諾依曼瓶頸

  2. 分支預測、流水線與多核 CPU

  3. 通用電子計算機的胎記:事件驅動

  4. Unix 程序模型的侷限

  5. DPDK、SDN 與大頁記憶體

  6. 現代計算機最親密的夥伴:區域性性與樂觀

  7. 分散式計算、超級計算機與神經網路共同的瓶頸

(一)天才馮·諾依曼與馮·諾依曼瓶頸

電子計算機與資訊科技是最近幾十年人類科技發展最快的領域,無可爭議地改變了每個人的生活:從生活方式到戰爭方式,從烹飪方式到國家治理方式,都被計算機和資訊科技徹底地改變了。如果說核武器徹底改變了國與國之間相處的模式,那麼計算機與資訊科技則徹底改變了人類這個物種本身,人類的進化也進入了一個新的階段。

簡單地說,生物進化之前還有化學進化。然而細胞一經誕生,中心法則的分子進化就趨於停滯了:38 億年來,中心法則再沒有新的變動,所有的蛋白質都由 20 種標準氨基酸連成,連鹼基與氨基酸對應關係也沿襲至今,所有現代生物共用一套標準遺傳密碼。正如中心法則是化學進化的產物,卻因為開創了生物進化而停止了化學進化,人類是生物進化的產物,也因為開創了文化進化和技術進化而停止了生物進化——進化已經走上了更高的維度。 ——《進化的階次 | 混亂博物館》

本文目標

本文的目標是在我有限的認知範圍內,討論一下人們為了提高效能做出的種種努力,這裡麵包含硬體層面的 CPU、RAM、磁碟,作業系統層面的併發、並行、事件驅動,軟體層面的多程序、多執行緒,網路層面的分散式,等等等等。事實上,上述名詞並不侷限於某一個層面,計算機從 CPU 內的閘電路到顯示器上瀏覽器中的某行字,是層層協作才得以實現的;電腦科學中的許多概念,都跨越了層級:事件驅動就是 CPU 和作業系統協作完成的。

天才 馮·諾依曼

馮·諾依曼 1903 年 12 月 28 日出生於奧匈帝國布達佩斯,1957 年 2 月 8 日卒於美國,終年 53 歲。在他短暫的一生中,他取得了巨大的成就,遠不止於世人熟知的「馮·諾依曼架構」。

約翰·馮·諾伊曼,出生於匈牙利的美國籍猶太人數學家,現代電子計算機與博弈論的重要創始人,在泛函分析、遍歷理論、幾何學、拓撲學和數值分析等眾多數學領域及計算機學、量子力學和經濟學中都有重大貢獻。 ——約翰·馮·諾伊曼的維基百科

除了對電腦科學的貢獻,他還有一個稱號不被大眾所熟知:「博弈論之父」。博弈論被認為是 20 世紀經濟學最偉大的成果之一。(說到博弈論,我相信大多數人第一個想到的肯定跟我一樣,那就是「納什均衡」)

馮·諾依曼架構

馮·諾依曼由於在曼哈頓工程中需要大量的運算,從而使用了當時最先進的兩臺計算機 Mark I 和 ENIAC,在使用 Mark I 和 ENIAC 的過程中,他意識到了儲存程式的重要性,從而提出了儲存程式邏輯架構。

「馮·諾依曼架構」定義如下:

  • 以運算單元為中心

  • 採用儲存程式原理

  • 儲存器是按地址訪問、線性編址的空間

  • 控制流由指令流產生

  • 指令由操作碼和地址碼組成

  • 資料以二進位制編碼

優勢

馮·諾依曼架構第一次將儲存器和運算器分開,指令和資料均放置於儲存器中,為計算機的通用性奠定了基礎。雖然在規範中計算單元依然是核心,但馮·諾依曼架構事實上導致了以儲存器為核心的現代計算機的誕生。

注:請各位在心裡明確一件事情:儲存器指的是記憶體,即 RAM。磁碟理論上屬於輸入輸出裝置。

該架構的另一項重要貢獻是用二進位制取代十進位制,大幅降低了運算電路的複雜度。這為電晶體時代超大規模積體電路的誕生提供了最重要的基礎,讓我們實現了今天手腕上的 Apple Watch 運算效能遠超早期大型計算機的壯舉,這也是摩爾定律得以實現的基礎。

馮·諾伊曼瓶頸

馮·諾依曼架構為計算機大提速鋪平了道路,卻也埋下了一個隱患:在記憶體容量指數級提升以後,CPU 和記憶體之間的資料傳輸頻寬成為了瓶頸。

效能之殤:從馮·諾依曼瓶頸談起

上圖是 i9-7980XE 18 核 36 執行緒的民用最強 CPU,其配合超頻過的 DDR4 3200MHz 的記憶體,測試出的記憶體讀取速度為 90GB/S。看起來很快了是不是?看看圖中的 L1 Cache,3.7TB/S。

我們再來算算時間。這顆 CPU 最大睿頻 4.4GHz,就是說 CPU 執行一個指令需要的時間是 0.000000000227273 秒,即 0.22ns(納秒),而記憶體的延遲是 68.1ns。換句話說,只要去記憶體裡取一個位元組,就需要 CPU 等待 300 個週期,何其的浪費 CPU 的時間啊。

CPU L1 L2 L3 三級快取是使用和 CPU 同樣的 14 奈米工藝製造的矽半導體,每一個 bit 都使用六個場效電晶體(通俗解釋成三極體)構成,成本高昂且非常佔用 CPU 核心面積,故不能做成很大容量。

除此之外,L1 L2 L3 三級快取對計算機速度的提升來源於計算機記憶體的「區域性性」,相關內容我們之後會專門討論。

(二)分支預測、流水線與多核 CPU

CPU 硬體為了提高效能,逐步發展出了指令流水線(分支預測)和多核 CPU,本文我們就將簡單地探討一下它們的原理和效果。

指令流水線

在一臺純粹的圖靈機中,指令是一個一個順序執行的。而現實世界的通用計算機所用的很多基礎演算法都是可以並行的,例如加法器和乘法器,它們可以很容易地被切分成可以同時執行的多個指令,這樣就可以大幅提升效能。

指令流水線,說白了就是 CPU 電路層面的併發。

Intel Core i7 自 Sandy Bridge(2010)架構以來一直都是 14 級流水線設計。基於 Cedar Mill 架構的最後一代奔騰 4,在 2006 年就擁有 3.8GHz 的超高頻率,卻因為其長達 31 級的流水線而成了為樣子貨,被 AMD 1GHz 的晶片按在地上摩擦。

RISC 機器的五層流水線示意圖

下圖形象地展示了流水線式如何提高效能的。

效能之殤:從馮·諾依曼瓶頸談起

缺點

指令流水線透過硬體層面的併發來提高效能,卻也帶來了一些無法避免的缺點。

  • 設計難度高,一不小心就成為了高頻低能的奔四

  • 併發導致每一條指令的執行時間變長

  • 最佳化難度大,有時候兩行程式碼的順序變動就可能導致數倍的效能差異,這對編譯器提出了更高的要求

  • 如果多次分支預測失敗,會導致嚴重的效能損失

分支預測

指令形成流水線以後,就需要一種高效的調控來保證硬體層面併發的效果:最佳情況是每條流水線裡的十幾個指令都是正確的,這樣完全不浪費時鐘週期。而分支預測就是幹這個的:

分支預測器猜測條件表示式兩路分支中哪一路最可能發生,然後推測執行這一路的指令,來避免流水線停頓造成的時間浪費。但是,如果後來發現分支預測錯誤,那麼流水線中推測執行的那些中間結果全部放棄,重新獲取正確的分支路線上的指令開始執行,這就帶來了十幾個時鐘週期的延遲,這個時候,這個 CPU 核心就是完全在浪費時間。

幸運的是,當下的主流 CPU 在現代編譯器的配合下,把這項工作做得越來越好了。

還記得那個讓 Intel CPU 效能跌 30% 的漏洞補丁嗎,那個漏洞就是 CPU 設計的時候,分支預測設計的不完善導致的。

多核 CPU

多核 CPU 的每一個核心擁有自己獨立的運算單元、暫存器、一級快取、二級快取,所有核心共用同一條記憶體匯流排,同一段記憶體。

多核 CPU 的出現,標誌著人類的積體電路工藝遇到了一個嚴酷的瓶頸,沒法再大規模提升單核效能,只能使用多個核心來聊以自慰。實際上,多核 CPU 效能的提升極其有限,遠不如增加一點點單核頻率提升的效能多。

優勢

多核 CPU 的優勢很明顯,就是可以並行地執行多個圖靈機,可以顯而易見地提升效能。只不過由於使用同一條記憶體匯流排,實際帶來的效果有限,並且需要作業系統和編譯器的密切配合才行。

題外話: AMD64 技術可以執行 32 位的作業系統和應用程式,所用的方法是依舊使用 32 位寬的記憶體匯流排,每計算一次要取兩次記憶體,效能提升也非常有限,不過好處就是可以使用大於 4GB 的記憶體了。大家應該都沒忘記第一篇文章中提到的馮·諾依曼架構擁有 CPU 和記憶體通訊頻寬不足的弱點。(注:AMD64 技術是和 Intel 交叉授權的專利,i7 也是這麼設計的)

劣勢

多核 CPU 劣勢其實更加明顯,但是人類也沒有辦法,誰不想用 20GHz 的 CPU 呢,誰想用這八核的 i7 呀。

  • 記憶體讀寫效率不變,甚至有降低的風險

  • 作業系統複雜度提升很多倍,計算資源的管理複雜了太多了

  • 依賴作業系統的進步:微軟以肉眼可見的速度,在這十幾年間大幅提升了 Windows 的多核效率和安全性:XP 只是能利用,7 可以自動調配一個程序在多個核心上游走,2008R2 解決了依賴 CPU0 排程導致當機的 bug(中國的銀行提的 bug 哦),8 可以利用多核心啟動,10 最佳化了殺程序依賴 CPU0 的問題。

超執行緒技術

Intel 的超執行緒技術是將 CPU 核心內部再分出兩個邏輯核心,只增加了 5% 的裸面積,就帶來了 15%~30% 的效能提升。

懷念過去

Intel 肯定懷念摩爾定律提出時候的黃金年代,只依靠工藝的進步,就能一兩年就效能翻番。AMD 肯定懷念 K8 的黃金一代,1G 戰 4G,靠的就是把記憶體控制器從北橋晶片移到 CPU 內部,提升了 CPU 和記憶體的通訊效率,自然效能倍增。而今天,人類的技術已經到達了一個瓶頸,只能透過不斷的提升 CPU 和作業系統的複雜度來獲得微弱的效能提升,嗚呼哀哉。

不過我們也不能放棄希望,AMD RX VAGA64 顯示卡擁有 2048 位的視訊記憶體位寬,理論極限還是很恐怖的,這可能就是未來記憶體的發展方向。

(三)通用電子計算機的胎記:事件驅動

Event-Driven(事件驅動)這個詞這幾年隨著 Node.js® 的大熱也成了一個熱詞,似乎已經成了「高效能」的代名詞,殊不知事件驅動其實是通用計算機的胎記,是一種與生俱來的能力。本文我們就要一起了解一下事件驅動的價值和本質。

通用電子計算機中的事件驅動

首先我們定義當下最火的 x86 PC 機為典型的通用電子計算機:可以寫文章,可以打遊戲,可以上網聊天,可以讀 U 盤,可以列印,可以設計三維模型,可以編輯渲染影片,可以作路由器,還可以控制巨大的工業機器。那麼,這種計算機的事件驅動能力就很容易理解了:

假設 Chrome 正在播放 Youtube 影片,你按下了鍵盤上的空格鍵,影片暫停了。這個操作就是事件驅動:計算機獲得了你單擊空格的事件,於是把影片暫停了。

假設你正在跟人聊 QQ,別人發了一段話給你,計算機獲得了網路傳輸的事件,於是將資訊提取出來顯示到了螢幕上,這也是事件驅動。

事件驅動的實現方式

事件驅動本質是由 CPU 提供的,因為 CPU 作為 控制器 + 運算器,他需要隨時響應意外事件,例如上面例子中的鍵盤和網路。

CPU 對於意外事件的響應是依靠 Exception Control Flow(異常控制流)來實現的。

強大的異常控制流

異常控制流是 CPU 的核心功能,它是以下聽起來就很牛批的功能的基礎:

時間片

CPU 時間片的分配也是利用異常控制流來實現的,它讓多個程序在宏觀上在同一個 CPU 核心上同時執行,而我們都知道在微觀上在任一個時刻,每一個 CPU 核心都只能執行一條指令。

虛擬記憶體

這裡的虛擬記憶體不是 Windows 虛擬記憶體,是 Linux 虛擬記憶體,即邏輯記憶體。

邏輯記憶體是用一段記憶體和一段磁碟上的儲存空間放在一起組成一個邏輯記憶體空間,對外依然表現為「線性陣列記憶體空間」。邏輯記憶體引出了現代計算機的一個重要的效能觀念:

記憶體區域性性天然的讓相鄰指令需要讀寫的記憶體空間也相鄰,於是可以把一個程序的記憶體放到磁碟上,再把一小部分的「熱資料」放到記憶體中,讓其作為磁碟的快取,這樣可以在降低很少效能的情況下,大幅提升計算機能同時執行的程序的數量,大幅提升效能。

虛擬記憶體的本質其實是使用 快取 + 樂觀 的手段提升計算機的效能。

系統呼叫

系統呼叫是程序向作業系統索取資源的通道,這也是利用異常控制流實現的。

硬體中斷

鍵盤點選、滑鼠移動、網路接收到資料、麥克風有聲音輸入、插入 U 盤這些操作全部需要 CPU 暫時停下手頭的工作,來做出響應。

程序、執行緒

程序的建立、管理和銷燬全部都是基於異常控制流實現的,其生命週期的鉤子函式也是作業系統依賴異常控制流實現的。執行緒在 Linux 上和程序幾乎沒有功能上的區別。

程式語言中的 try catch

C++ 編譯成的二進位制程式,其異常控制語句是直接基於異常控制流的。Java 這種硬虛擬機器語言,PHP 這種軟虛擬機器語言,其異常控制流的一部分也是有最底層的異常控制流提供的,另一部分可以由邏輯判斷來實現。

基於異常控制流的事件驅動

其實現在人們在談論的事件驅動,是 Linux kernel 提供的 epoll,是 2002 年 10 月 18 號伴隨著 kernel 2.5.44 釋出的,是 Linux 首次將作業系統中的 I/O 事件的異常控制流暴露給了程序,實現了本文開頭提到的 Event-Driven(事件驅動)。

Kqueue

FreeBSD 4.1 版本於 2000 年釋出,起攜帶的 Kqueue 是 BSD 系統中事件驅動的 API 提供者。BSD 系統如今已經遍地開花,從 macOS 到 iOS,從 watchOS 到 PS4 遊戲機,都受到了 Kqueue 的蒙蔭。

epoll 是什麼

作業系統本身就是事件驅動的,所以 epoll 並不是什麼新發明,而只是把本來不給使用者空間用的 api 暴露在了使用者空間而已。

epoll 做了什麼

網路 IO 是一種純非同步的 IO 模型,所以 Nginx 和 Node.js® 都基於 epoll 實現了完全的事件驅動,獲得了相比於 select/poll 巨量的效能提升。而磁碟 IO 就沒有這麼幸運了,因為磁碟本身也是單體阻塞資源:即有程序在寫磁碟的時候,其他寫入請求只能等待,就是天王老子來了也不行,磁碟做不到呀。所以磁碟 IO 是基於 epoll 實現的非阻塞 IO,但是其底層依舊是非同步阻塞,即便這樣,效能也已經爆棚了。Node.js 的磁碟 IO 效能遠超其他解釋型語言,過去幾年在 web 後端霸佔了一些對磁碟 IO 要求高的領域。

(四)Unix 程序模型的侷限

Unix 系統 1969 年誕生於 AT&T 旗下的貝爾實驗室。1971 年,Ken Thompson(Unix 之父)和 Dennis Ritchie(C 語言之父)共同發明了 C 語言,並在 1973 年用 C 語言重寫了 Unix。

Unix 自誕生起就是多使用者、多工的分時作業系統,其引入的「程序」概念是電腦科學中最成功的概念之一,幾乎所有現代作業系統都是這一概念的受益者。但是程序也有侷限,由於 AT&T 是做電話交換起家,所以 Unix 程序在設計之初就是延續的電話交換這個業務需求:保證電話交換的效率,就夠了。

1984 年,Richard Stallman 發起了 GNU 專案,目標是建立一個完全自由且向下相容 Unix 的作業系統。之後 Linus Torvalds 與 1991 年釋出了 Linux 核心,和 GNU 結合在了一起形成了 GNU/Linux 這個當下最成功的開源作業系統。所以 Redhat、CentOS、Ubuntu 這些如雷貫耳的 Linux 伺服器作業系統,他們的記憶體模型也是高度類似 Unix 的。

Unix 程序模型介紹

程序是作業系統提供的一種抽象,每個程序在自己看來都是一個獨立的圖靈機:獨佔 CPU 核心,一個一個地執行指令,讀寫記憶體。程序是電腦科學中最重要的概念之一,是程序使多使用者、多工成為了可能。

上下文切換

作業系統使用上下文切換讓一個 CPU 核心上可以同時執行多個程序:在宏觀時間尺度,例如 5 秒內,一臺電腦的使用者會認為他的桌面程序、音樂播放程序、滑鼠響應程序、瀏覽器程序是在同時執行的。

效能之殤:從馮·諾依曼瓶頸談起

圖片來自《CS:APP》

上下文切換的過程

以下就是 Linux 上下文切換的過程:

假設正在執行網易雲音樂程序,你突然想搜歌,假設焦點已經位於搜尋框內。

  • 當前程序是網易雲音樂,它正在優哉遊哉的播放著音樂

  • 你突然打字,CPU 接到鍵盤發起的中斷訊號(異常控制流中的一個異常),準備調起鍵盤處理程序

  • 網易雲音樂程序的暫存器、棧指標、程式計數器儲存到記憶體中

  • 將鍵盤處理程序的暫存器、棧指標、程式計數器從記憶體中讀出來,寫入到 CPU 內部相應的模組中

  • 執行程式計數器的指令,鍵盤處理程式開始處理鍵盤輸入

  • 完成了一次上下文切換

名詞解釋

  • 暫存器:CPU 核心裡的用於暫時儲存指令、地址和資料的電路,和核心頻率一樣,速度極快

  • 棧指標:該程序所擁有的棧的指標

  • 程式計數器:簡稱 PC,它儲存著核心將要執行的下一個指令的記憶體地址。程式計數器是圖靈機的核心組成部分。還記得馮·諾依曼架構嗎,它的一大創造就是把指令和資料都存在記憶體裡,讓計算機獲得了極大的自由度。

Unix 程序模型的侷限

Unix 程序模型十分的清晰,上下文切換使用了一個非常簡單的操作就實現了多個程序的宏觀同時執行,是一個偉大的傑作。但是它卻存在著一個潛在的缺陷,這個缺陷在 Unix 誕生數十年之後才漸漸浮出了水面。

致命的記憶體

程序切換過程中需要分別寫、讀一次記憶體,這個操作在 Unix 剛發明的時候沒有發現有什麼效能問題,但是 CPU 裹挾著摩爾定律一路狂奔,2000 年,AMD 領先 Intel 兩天釋出了第一款 1GHz 的微處理器「AMD Athlon 1GHz」,此時一個指令的執行時間已經低到了 1ns,而其記憶體延遲高達 60ns,這導致了一個以前不曾出現的問題:

上下文切換讀寫記憶體的時間成了整個系統的效能瓶頸。

軟體定義一切

我們將在下一篇文章探討 SDN(軟體定義網路),在這裡我們先來看一下「軟體定義一切」這個概念。當下,不僅有軟體定義網路,還有軟體定義儲存,甚至出現了軟體定義基礎架構(這不就是雲端計算嘛)。是什麼導致了軟體越來越強勢,開始傾入過去只有專業的硬體裝置才能提供的高效能高穩定性服務呢?我認為,就是通用計算機的發展導致的,確切地說,是 CPU 和網路的發展導致的。

當前的民用頂級 CPU 的效能已經爆表,因為規模巨大,所以其價格也要顯著低於同效能的專用處理器:自建 40G 軟路由的價格大約是 40G 專用路由價格的二十分之一。

(五)DPDK、SDN 與大頁記憶體

上文我們說到,當今的 x86 通用微處理器已經擁有了十分強大的效能,得益於其龐大的銷量,讓它的價格和專用 CPU 比也有著巨大的優勢,於是,軟體定義一切誕生了!

軟路由

說到軟路由,很多人都露出了會心的微笑,因為其擁有低廉的價格、超多的功能、夠用的效能和科學上網能力。現在網上能買到的軟路由,其本質就是一個 x86 PC 加上多個網口,大多是基於 Linux 或 BSD 核心,使用 Intel 低端被動散熱 CPU 打造出的千兆路由器,幾百塊就能實現千兆的效能,最重要的是擁有 QOS、多路撥號、負載均衡、防火牆、VPN 組網、科學上網等強大功能,傳統路由器拋開科學上網不談,其他功能也不是幾百塊就搞得定的。

軟路由的弱點

軟路由便宜,功能強大,但是也有弱點。它最大的弱點其實是效能:傳統 *UNIX 網路棧的效能實在是不高。

軟路由的 NAT 延遲比硬路由明顯更大,而且幾百塊的軟路由 NAT 效能也不夠,跑到千兆都難,而幾百塊的硬路由跑到千兆很容易。那怎麼辦呢?改作業系統啊。

SDN

軟體定義網路,其本質就是使用電腦科學中最常用的「虛擬機器」構想,將傳統由硬體實現的 交換、閘道器、路由、NAT 等網路流量控制流程交由軟體來統一管理:可以實現硬體不動,網路結構瞬間變化,避免了傳統的停機維護除錯的煩惱,也為大規模公有雲端計算鋪平了道路。

虛擬機器

虛擬機器的思想自底向上完整地貫穿了計算機的每一個部分,硬體層有三個場效電晶體虛擬出的 SRAM、多個記憶體晶片虛擬出的一個「線性陣列記憶體」,軟體層有 jvm 虛擬機器,PHP 虛擬機器(直譯器)。自然而然的,當網路成為了更大規模計算的瓶頸的時候,人們就會想,為什麼網路不能虛擬呢?

OpenFlow

最開始,SDN 還是基於硬體來實施的。Facebook 和 Google 使用的都是 OpenFlow 協議,作用在資料鏈路層(使用 MAC 地址通訊的那一層,也就是普通交換機工作的那一層),它可以統一管理所有閘道器、交換等裝置,讓網路架構實時地做出改變,這對這種規模的公司所擁有的巨大的資料中心非常重要。

DPDK

DPDK 是 SDN 更前沿的方向:使用 x86 通用 CPU 實現 10Gbps 甚至 40Gbps 的超高速閘道器(路由器)。

DPDK 是什麼

Intel DPDK 全稱為 Intel Data Plane Development Kit,直譯為「英特爾資料平面開發工具集」,它可以擺脫 *UNIX 網路資料包處理機制的侷限,實現超高速的網路包處理。效能之殤:從馮·諾依曼瓶頸談起

DPDK 的價值

當下,一臺 40G 核心網管路由器動輒數十萬,而 40G 網路卡也不會超過一萬塊,而一顆效能足夠的 Intel CPU 也只需要幾萬塊,軟路由的價效比優勢是巨大的。

實際上,阿里雲和騰訊雲也已經基於 DPDK 研發出了自用的 SDN,已經創造了很大的經濟價值。

怎麼做到的?

DPDK 使用自研的資料鏈路層(MAC 地址)和網路層(ip 地址)處理功能(協議棧),拋棄作業系統(Linux,BSD 等)提供的網路處理功能(協議棧),直接接管物理網路卡,在使用者態處理資料包,並且配合大頁記憶體和 NUMA 等技術,大幅提升了網路效能。有論文做過實測,10G 網路卡使用 Linux 網路協議棧只能跑到 2G 多,而 DPDK 分分鐘跑滿。

使用者態網路棧

上篇文章我們已經說到,Unix 程序在網路資料包過來的時候,要進行一次上下文切換,需要分別讀寫一次記憶體,當系統網路棧處理完資料把資料交給使用者態的程序如 Nginx 去處理還會出現一次上下文切換,還要分別讀寫一次記憶體。夭壽啦,一共 1200 個 CPU 週期呀,太浪費了。

而使用者態協議棧的意思就是把這塊網路卡完全交給一個位於使用者態的程序去處理,CPU 看待這個網路卡就像一個假肢一樣,這個網路卡資料包過來的時候也不會引發系統中斷了,不會有上下文切換,一切都如絲般順滑。當然,實現起來難度不小,因為 Linux 還是分時系統,一不小心就把 CPU 時間佔完了,所以需要小心地處理阻塞和快取問題。

NUMA

NUMA 來源於 AMD Opteron 微架構,其特點是將 CPU 直接和某幾根記憶體使用匯流排電路連線在一起,這樣 CPU 在讀取自己擁有的記憶體的時候就會很快,代價就是讀取別 U 的記憶體的時候就會比較慢。這個技術伴隨著伺服器 CPU 核心數越來越多,記憶體總量越來越大的趨勢下誕生的,因為傳統的模型中不僅頻寬不足,而且極易被搶佔,效率下降的厲害。

效能之殤:從馮·諾依曼瓶頸談起

NUMA 利用的就是電子計算機(圖靈機 + 馮·諾依曼架構)天生就帶的渦輪:區域性性。渦輪:汽車發動機加上渦輪,可以讓動力大增油耗降低

細說大頁記憶體

記憶體分頁

為了實現虛擬記憶體管理機制,前人們發明了記憶體分頁機制。這個技術誕生的時候,記憶體分頁的預設大小是 4KB,而到了今天,絕大多數作業系統還是用的這個數字,但是記憶體的容量已經增長了不知道多少倍了。

TLB miss

TLB(Translation Lookaside Buffers)轉換檢測緩衝區,是記憶體控制器中為增虛擬地址到實體地址的翻譯速度而設立的一組電子元件,最近十幾年已經隨著記憶體控制器被整合到了 CPU 內部,每顆 CPU 的 TLB 都有固定的長度。

如果快取未命中(TLB miss),則要付出 20-30 個 CPU 週期的帶價。假設應用程式需要 2MB 的記憶體,如果作業系統以 4KB 作為分頁的單位,則需要 512 個頁面,進而在 TLB 中需要 512 個表項,同時也需要 512 個頁表項,作業系統需要經歷至少 512 次 TLB Miss 和 512 次缺頁中斷才能將 2MB 應用程式空間全部對映到實體記憶體;然而,當作業系統採用 2MB 作為分頁的基本單位時,只需要一次 TLB Miss 和一次缺頁中斷,就可以為 2MB 的應用程式空間建立虛實對映,並在執行過程中無需再經歷 TLB Miss 和缺頁中斷。

大頁記憶體

大頁記憶體 HugePage 是一種非常有效的減少 TLB miss 的方式,讓我們來進行一個簡單的計算。

2013 年釋出的 Intel Haswell i7-4770 是當年的民用旗艦 CPU,其在使用 64 位 Windows 系統時,可以提供 1024 長度的 TLB,如果記憶體頁的大小是 4KB,那麼總快取記憶體容量為 4MB,如果記憶體頁的大小是 2MB,那麼總快取記憶體容量為 2GB。顯然後者的 TLB miss 機率會低得多。

DPDK 支援 1G 的記憶體分頁配置,這種模式下,一次性快取的記憶體容量高達 1TB,絕對夠用了。

不過大頁記憶體的效果沒有理論上那麼驚人,DPDK 實測有 10%~15% 的效能提升,原因依舊是那個天生就帶的渦輪:區域性性。

(六)現代計算機最親密的夥伴:區域性性與樂觀

馮·諾依曼架構中,指令和資料均儲存在記憶體中,徹底開啟了計算機「通用」的大門。這個結構中,「線性陣列」記憶體天生攜帶了一個渦輪:區域性性。

區域性性分類

空間區域性性

空間區域性性是最容易理解的區域性性:如果一段記憶體被使用,那麼之後,離他最近的記憶體也最容易被使用,無論是資料還是指令都是這樣。舉一個淺顯易懂的例子:

迴圈處理一個 Array,當處理完了 [2] 之後,下一個訪問的就是 [3],他們在記憶體裡是相鄰的。

時間區域性性

如果一個變數所在的記憶體被訪問過,那麼接下來這一段記憶體很可能被再次訪問,例子也非常簡單:

$a = [];
if ( !$b ) {
    $a[] = $b;
}

在一個 function 內,一個記憶體地址很可能被訪問、修改多次。

樂觀

「樂觀」作為一種思考問題的方式廣泛存在於計算機中,從硬體設計、記憶體管理、應用軟體到資料庫均廣泛運用了這種思考方式,並給我們帶來了十分可觀的效能收益。

樂觀的 CPU

第一篇文章中的 L1 L2 L3 三級快取和第二篇文章中的分支預測與流水線,均是樂觀思想的代表。

樂觀的虛擬記憶體

虛擬記憶體依據計算機記憶體的區域性性,將磁碟作為記憶體的本體,將記憶體作為磁碟的快取,用很小的效能代價帶來了數十倍併發程序數,是樂觀思想的集大成者。

樂觀的快取

Java 經典面試題 LRU 快取實現,也是樂觀思想的一種表達。

同樣,鳥哥的 yac 也是這一思想的強烈體現。

設計 Yac 的經驗假設

  1. 對於一個應用來說, 同名的 Cache 鍵, 對應的 Value, 大小几乎相當.

  2. 不同的鍵名的個數是有限的.

  3. Cache 的讀的次數, 遠遠大於寫的次數.

  4. Cache 不是資料庫, 即使 Cache 失效也不會帶來致命錯誤.

Yac 的限制

  1. key 的長度最大不能超過 48 個字元. (我想這個應該是能滿足大家的需求的, 如果你非要用長 Key, 可以 MD5 以後再存)

  2. Value 的最大長度不能超過 64M, 壓縮後的長度不能超過 1M.

  3. 當記憶體不夠的時候, Yac 會有比較明顯的踢出率, 所以如果要使用 Yac, 那麼儘量多給點記憶體吧.

樂觀鎖

樂觀鎖在併發控制和資料庫設計裡都擁有重要地位,其本質就是在特定的需求下,假定不會衝突,衝突之後再浪費較長時間處理,比直接每次請求都浪費較短時間檢測,總體的效能高。樂觀鎖在演算法領域有著非常豐富而成熟的應用。

樂觀的分散式計算

分散式計算的核心思想就是樂觀,由 95% 可靠的 PC 機組成的分散式系統,起可靠性也不會達到 99.99%,但是絕大多數場景下,99% 的可靠性就夠了,畢竟拿 PC 機做分散式比小型機便宜得多嘛。下一篇文章我會詳細介紹分散式計算的效能之殤,此處不再贅述。

樂觀的代價

出來混,早晚是要還的。

樂觀給了我們很多的好處,總結起來就是一句話:以微小的效能損失換來大幅的效能提升。但是,人在河邊走,哪有不溼鞋。每一個 2015 年 6 月入 A 股的散戶,都覺得大盤還能再翻一番,豈不知一週之後,就是股災了。

樂觀的代價來自於「微小的效能損失」,就跟房貸市場中「微小的風險」一樣,當大環境小幅波動的時候,他確實能承擔壓力,穩住系統,但是怕就怕突然雪崩:

  • 虛擬記憶體中的記憶體的區域性性突然大幅失效,磁碟讀寫速度成了記憶體讀寫速度,系統卡死

  • 分散式資料庫的六臺機器中的 master 掛了,系統在一秒內選舉出了新的 master,你以為系統會穩定執行?master 掛掉的原因就是壓力過大,這樣就會導致新的 master 瞬間又被打掛,然後一臺一臺地繼續,服務徹底失效。例如:「故障說明」對六月六日 LeanCloud 多項服務發生中斷的說明

(七)分散式計算、超級計算機與神經網路共同的瓶頸

分散式計算是這些年的熱門話題,各種大資料框架層出不窮,容器技術也奮起直追,各類資料庫(Redis、ELasticsearch、MongoDB)也大搞分散式,可以說是好不熱鬧。分散式計算在大熱的同時,也存在著兩臺機器也要硬上 Hadoop 的「面向簡歷程式設計」,接下來我就剖析一下分散式計算的本質,以及我的理解和體會。

分散式計算的本質

分散式計算來源於人們日益增長的效能需求與落後的 x86 基礎架構之間的矛盾。恰似設計模式是物件導向對現實問題的一種妥協。

x86 伺服器

x86 伺服器,俗稱 PC 伺服器、微機伺服器,近二十年以迅雷不及掩耳盜鈴之勢全面搶佔了絕大部分的伺服器市場,它和小型機比只有一個優勢,其他的全是缺點,效能、可靠性、可擴充套件性、佔地面積都不如小型機,但是一個優勢就決定了每年 2000 多億美元的 IDC 市場被 x86 伺服器佔領了 90%,這個優勢就是價格。畢竟有錢能使磨推鬼嘛。

現有的分散式計算,無論是 Hadoop 之類的大資料平臺,還是 HBase 這樣的分散式資料庫,無論是 Docker 這種容器排布,還是 Redis 這種樸素分散式資料庫,其本質都是因為 x86 的擴充套件性不夠好,導致大家只能自己想辦法利用網路來自己構建一個宏觀上更強效能更高負載能力的計算機。

x86 分散式計算,是一種新的計算機結構。

基於網路的 x86 伺服器分散式計算,其本質是把網路當做匯流排,設計了一套新的計算機體系結構:

  • 每一臺機器就等於一個運算器加一個儲存器

  • master 節點就是控制器加輸入裝置、輸出裝置

x86 分散式計算的弱點

上古時代,小型機的擴充套件能力是非常變態的,到今天,基於小型機的 Oracle 資料庫系統依舊能做到驚人的效能和可靠性。實際上單顆 x86 CPU 的效能已經遠超 IBM 小型機用的 PowerPC,但是當數量來到幾百顆,x86 伺服器叢集就敗下陣來,原因也非常簡單:

  • 小型機是專門設計的硬體和專門設計的軟體,只面向這種規模(例如幾百顆 CPU)的計算

  • 小型機是完全閉源的,不需要考慮擴充套件性,特定的幾種硬體在穩定性上前進了一大步

  • x86 的 IO 效能被架構鎖死了,各種匯流排、PCI、PCIe、USB、SATA、乙太網,為了個人計算機的便利性,犧牲了很多的效能和可靠性

  • 小型機使用匯流排通訊,可以實現極高的資訊傳遞效率,極其有效的監控以及極高的故障隔離速度

  • x86 伺服器基於網路的分散式具有天然的缺陷:

  • 作業系統決定了網路效能不足

  • 網路需要使用事件驅動處理,比匯流排電路的延遲高几個數量級

  • PC 機的硬體不夠可靠,故障率高

  • 很難有效監控,隔離故障速度慢

x86 分散式計算的基本套路

Google 系大資料處理框架

2003 年到 2004 年間,Google 發表了 MapReduce、GFS(Google File System)和 BigTable 三篇技術論文,提出了一套全新的分散式計算理論。MapReduce分散式計算框架,GFS(Google File System)是分散式檔案系統,BigTable 是基於 Google File System 的資料儲存系統,這三大元件組成了 Google 的分散式計算模型。

Hadoop、Spark、Storm 是目前最重要的三大分散式計算系統,他們都是承襲 Google 的思路實現並且一步一步發展到今天的。

MapReduce 的基本原理也十分簡單:將可以並行執行的任務切分開來,分配到不同的機器上去處理,最終再彙總結果。而 GFS 是基於 Master-Slave 架構的分散式檔案系統,其 master 只扮演控制者的角色,操控著所有的 slave 幹活。

Redis、MongoDB 的分散式

Redis 有兩個不同的分散式方案。Redis Cluster 是官方提供的工具,它透過特殊的協議,實現了每臺機器都擁有資料儲存和分散式調節功能,效能沒有損失。缺點就是缺乏統一管理,運維不友好。Codis 是一個非常火的 Redis 叢集搭建方案,其基本原理可以簡單地描述如下:透過一個 proxy 層,完全隔離掉了分散式調節功能,底層的多臺機器可以任意水平擴充套件,運維十分友好。

MongoDB 官方提供了一套完整的分散式部署的方案,提供了 mongos 控制中心,config server 配置儲存,以及眾多的 shard(其底層一般依然有兩臺互為主從強資料一致性的 mongod)。這三個元件可以任意部署在任意的機器上,MongoDB 提供了 master 選舉功能,在檢測到 master 異常後會自動選舉出新的 master 節點。

問題和瓶頸

人們費這麼大的勁研究基於網路的 x86 伺服器分散式計算,目的是什麼?還不是為了省錢,想用一大票便宜的 PC 機替換掉昂貴的小型機、大型機。雖然人們已經想盡了辦法,但還是有一些頑固問題無法徹底解決。

master 失效問題

無論怎樣設計,master 失效必然會導致服務異常,因為網路本身不夠可靠,所以監控系統的容錯要做的比較高,所以基於網路的分散式系統的故障恢復時間一般在秒級。而小型機的單 CPU 故障對外是完全無感的。

現行的選舉機制主要以節點上的資料以及節點資料之間的關係為依據,透過一頓猛如虎的數學操作,選舉出一個新的 master。邏輯上,選舉沒有任何問題,如果 master 因為硬體故障而失效,新的 master 會自動頂替上,並在短時間內恢復工作。

而自然界總是狠狠地打人類的臉:

  1. 硬體故障機率極低,大部分 master 失效都不是因為硬體故障

  2. 如果是流量過大導致的 master 失效,那麼選舉出新的 master 也無濟於事:提升叢集規模才是解決之道

  3. 即使能夠及時地在一分鐘之內頂替上 master 的工作,那這一分鐘的異常也可能導致雪崩式的 cache miss,從磁碟快取到虛擬記憶體,從 TLB 到三級快取,再到二級快取和一級快取,全部失效。如果每一層的失效會讓系統響應時間增加五倍的話,那最終的總響應時長將是驚人的。

系統規模問題

無論是 Master-Slave 模式還是 Proxy 模式,整個系統的流量最終還是要落到一個特定的資源上。當然這個資源可能是多臺機器,但是依舊無法解決一個嚴重的問題:系統規模越大,其本底效能損失就越大。

這其實是我們所在的這個宇宙空間的一個基本規律。我一直認為,這個宇宙裡只有一個自然規律:熵增。既然我們這個宇宙是一個熵增宇宙,那麼這個問題就無法解決。

超級計算機

超級計算機可以看成一個規模特別巨大的分散式計算系統,他的效能瓶頸從目前的眼光來看,是超多計算核心(數百萬)的調節效率問題。其本質是通訊速率不夠快,資訊傳遞的太慢,讓數百萬核心一起工作,傳遞命令和資料的工作佔據了絕大多數的執行時間。

神經網路

深度學習這幾年大火,其原因就是卷積神經網路(CNN)造就的 AlphaGo 打敗了人類,計算機在這個無法窮舉的遊戲裡徹底贏了。伴隨著 Google 帝國的強大推力,深度學習機器學習,乃至人工智慧,這幾個詞在過去的兩年大火,特別是在中美兩國。現在拿手機拍張照背後都有機器學習你敢信?

機器學習的瓶頸,本質也是資料交換:機器學習需要極多的計算,而計算速度的瓶頸現在就在運算器和儲存器的通訊上,這也是顯示卡搞深度學習比 CPU 快數十倍的原因:視訊記憶體和 GPU 資訊交換的速度極快。

九九歸一

分散式系統的效能問題,表現為多個方面,但是歸根到底,其原因只是一個非常單純的矛盾:人們日益增長的效能需求和資料一致性之間的矛盾。一旦需要強資料一致性,那就必然存在一個限制效能的瓶頸,這個瓶頸就是資訊傳遞的速度。

同樣,超級計算機和神經網路的瓶頸也都是資訊傳遞的速度。

那麼,資訊傳遞速度的瓶頸在哪裡呢?

我個人認為,資訊傳遞的瓶頸最表層是人類的硬體製造水平決定的,再往底層去是馮·諾依曼架構決定的,再往底層去是圖靈機邏輯模型決定的。可是圖靈機是計算機可行的理論基礎呀,所以,還是怪這個熵增宇宙吧,為什麼規模越大維護成本越高呢,你也是個成熟的宇宙了,該學會自己把自己變成熵減宇宙了。

效能之殤:從馮·諾依曼瓶頸談起


原文連結:https://lvwenhan.com/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/492.html

相關文章