本號並不這麼認為。我們先舉一個活生生的例子,比如我們現在有個Web服務應用,崩潰重啟後在繫結套接字的時候出現報錯(socket_bind(): unable to bind address [98]: Address already in use。),導致服務端無法工作。問題比較明確,是地址(埠)被佔用了。你這時候可能會猜測那個程式佔了埠呢?大家都清楚,伺服器埠的使用都是嚴格受限的,肯定是這個程式。但是可能會疑惑:“這個程式不是剛剛起來嗎?!”如果你只是使用API,不懂得底層的原理,別說解決問題,可能都不知道如何下手。這個問題我們先放到這裡,後面再具體解釋,這裡只是想說明一下底層技術的重要性。
上層應用開發的多了之後,對底層技術的接觸就越來越少了。以至於很多人有了“底層技術無用論”的觀點。很多人認為學習框架多好啊,大家都在用,跳槽的時候也能用的上。學習那些底層技術幹啥,平時都用不到。
本號並不這麼認為。我們先舉一個活生生的例子,比如我們現在有個Web服務應用,崩潰重啟後在繫結套接字的時候出現報錯(socket_bind(): unable to bind address [98]: Address already in use。),導致服務端無法工作。問題比較明確,是地址(埠)被佔用了。你這時候可能會猜測那個程式佔了埠呢?大家都清楚,伺服器埠的使用都是嚴格受限的,肯定是這個程式。但是可能會疑惑:“這個程式不是剛剛起來嗎?!”如果你只是使用API,不懂得底層的原理,別說解決問題,可能都不知道如何下手。這個問題我們先放到這裡,後面再具體解釋,這裡只是想說明一下底層技術的重要性。
另外一個比較典型的例子是關於前端開發的。很多人熱衷於學習各種框架。框架雖然能幫助我們解決一些問題,節省開發成本並降低開發週期。但是,學習框架並不能掌握技術的根本,從而導致自己能力沒有本質的提升。我們以前端框架為例,在過去的幾年當中,JQuery、Bootstrap、Angular和Vue等等等等,輪番上陣。這個框架你還沒用熟悉呢,結果又來了一個新的框架,讓你應接不暇。而這些框架最本質的東西其實就是JS、CSS和HTML等內容,只有學會這些基礎技術,才能遊刃有餘。如果這些基礎技術不熟悉,而投入大量精力學習框架,這就好像還沒學會走,就想著跑,最後自己可能摔得滿頭是包。
可能扯的有點遠,前面的例子只是想告訴大家底層技術的重要性。對於我們搞軟體開發的人來說,底層技術其實相當於大廈的地基,地基不穩,大廈是很危險的。當然,計算機技術的細分領域很多,每個領域又有自己的底層技術,因此我們不可能都有涉及。今天我們介紹的底層技術則是最為通用的技術,也就是計算、儲存、網路和資料結構與演算法。
關於計算相關的內容
計算機技術自然核心是計算了。毫不誇張的說,所有應用都要依賴於計算,小到單機小遊戲,大到電商或者雲計算平臺。因此,計算問題自然是我們最為關心的問題了。說到計算,最主要的自然是程式的效能了,如果我們開發的程式的效能提升一倍,就相當於硬體成本降低了50%。對於網際網路這種需要大量計算資源的應用,其價值可見一斑。
我們先看一個具體的例子。下面是一段C語言的程式碼,程式碼很簡單,就是將二維陣列中的內容做加一操作。但是如果你測試一下兩段程式碼的耗時的話,就會發現兩者有四倍的效能差異。大家可以觀察一下圖中兩端程式碼的差異,並思考一下為什麼有如此之大的差異。
問題先放一下,我們回到我們今天的主角,CPU。CPU是計算依賴的硬體,大家都知道計算是在CPU內完成的。我們先看一下CPU長什麼樣。CPU是計算機的核心單元,它負責從儲存裝置讀取資料,經過計算後將生成的新資料再儲存起來。這就好像一個大型工廠的生產車間,將原材料加工成半成品或者成品(我們後面單獨用一個章節介紹CPU相關的內容)。
瞭解了CPU的基本功能,我們再解剖了看看它的五臟六腑長什麼樣。下圖是一個簡化的CPU內部結構圖,最為核心的元件就是計算單元(ALU)、暫存器(很多暫存器)和快取記憶體。另外就是透過匯流排介面與外部的記憶體進行連線。這裡面最核心的元件就是ALU了,其原理很簡單,就是完成加減乘除運算。
CPU要進行運算,就需要原料,而原料需要從記憶體搬運。有一個事實我們需要記住,就是訪問記憶體的代價(延時)是訪問暫存器的100倍左右。最早的CPU是直接訪問記憶體的,後來隨著ALU效能的提升,發現有問題,就在ALU和記憶體之間增加了快取。現代CPU快取通常為3級快取,分別是L1、L2和L3,其中L1和L2是CPU核獨有的,而L3是同一顆CPU的多核共享的。其基本的架構如下圖所示。
這裡面有個關鍵問題是快取的容量是遠遠小於主(內)存的容量的,因此,快取中的資料通常是主存資料的很小的一部分。由於應用訪問資料有區域區域性性的特點,因此快取中的資料通常是程式需要的資料,也就是ALU接下來要用的資料。另外一個需要注意的地方是從主存讀取資料到快取是有一定粒度(專業術語叫快取行)的,當前處理器通常是64位元組。如下圖所示,主存中的內容被讀取到快取中。
然後,我們回到一開始的關於上面兩段程式的效能問題來。上面程式碼中一個是逐行訪問二維陣列,另外一個是逐列訪問二維陣列。具體示意圖如下圖所示。
在逐行訪問時,訪問的地址是以4位元組為單位跳躍的,由於快取行大小是64位元組,因此很容易命中快取。而逐列訪問時,每次跳躍4096位元組,遠遠超越了快取行的大小,從而導致資料大部分是從記憶體讀取的。也正是因為這個,導致兩個程式有四倍的效能差異。
透過上面的介紹,我們應該記住兩個關鍵點,一個是訪問記憶體的代價比較高,因此在程式設計時儘量減少對記憶體的直接訪問;另外一個是充分利用快取的優勢。關於如何做到上面兩點,具體細節我們後續專門介紹。
關於儲存相關的內容
資料最終都要儲存在儲存裝置上,否則系統一斷電所有東西都丟了,這個道理大家都懂。這裡的儲存包括磁碟和SSD硬碟等內容。本文主要從儲存裝置及管理裝置的檔案系統分析儲存相關關鍵技術。儲存中最為重要的有兩個方面,一個是儲存資料的可靠性,另外一個是儲存資料的效能。
本文先從儲存的效能說起,可靠性我們後續專門介紹。在儲存領域使用最多的還是普通機械磁碟。機械磁碟的內部解剖圖如下圖所示,其資料的讀寫是透過一個機械臂完成的。機械臂擺來擺去,想想就知道不會太快。機械磁碟是IBM發明的,第一塊磁碟的尋道時間(機械臂定位到目的位置的時間)在600毫秒左右。而現代的機械磁碟尋道時間有了比較明顯的改善,但由於其機械特性的原因,其耗時還是比較長的,大概是4-8毫秒的樣子。
以下是付費內容
這個耗時是記憶體的近10萬倍,是暫存器耗時的千萬倍。因此機械磁碟的速度相對記憶體來說,無異於蝸牛對高鐵的速度。鑑於機械磁碟的上述缺陷,在軟體層面做了很多考量,從而保證效能最佳。
我們通常在使用硬碟的時候不會直接寫程式碼訪問(不排除個例),而是透過作業系統提供的介面訪問。這個作業系統的介面通常是檔案系統的介面。為了便於理解,我們先看一下對於Linux作業系統來說,磁碟系統的整個軟硬體棧,從上到下分別是:檔案系統、通用塊層、裝置驅動層和裝置層(具體的硬體裝置,可以理解為磁碟)。
在這裡有兩個層面的軟體對磁碟的訪問做了最佳化,一個是檔案系統,另外一個是通用塊層。其中檔案系統的核心功能是磁碟資料管理的功能,但考慮到磁碟的缺點,因此在讀寫資料方法做了一些效能方面的最佳化。而通用塊層則主要是針對磁碟的特性進行了各種最佳化。
檔案系統對磁碟訪問的效能最佳化是透過頁快取(頁快取其實就是記憶體)完成的,這個頁快取與CPU中的快取有異曲同工之妙。檔案系統透過頁快取在資料寫和讀兩方面分別作了最佳化。
寫方面的最佳化主要是延遲批次寫,也就是資料先寫到頁快取中,經過積累後再磁碟驅動提交。這種積累和延遲寫主要目的是為了增加資料的連續性,也就是為了規避磁碟機械臂的擺動,因為磁碟機械臂擺動是最耗時的。
讀方面的最佳化主要是預讀功能,預讀就是根據當前應用讀取資料的模式,提前將資料讀到記憶體當中。由於應用訪問資料的區域區域性性特點,這種預讀就可以避免應用直接從磁碟讀取資料的延時,從而提高讀效能。
通用塊層的主要作用是針對磁碟做IO排程,通俗的講就是決定哪個IO先發送到磁碟,哪個後傳送到磁碟。
針對機械磁碟來說,最為重要的就是通用塊層會進行IO的重排序(根據邏輯地址排序)。如上圖所示,假設上層應用按時間順序傳送1、2、3、4和5等5個請求的時候。此時,通用塊層並不會按照時間順序傳送給磁碟,而是按照圖中紅色虛線箭頭的順序(1、5、2、4、3)傳送給我。這樣,磁碟的機械臂就不用來回擺動,從而大大提升其效能。
其實說了半天,這裡有一點是需要我們注意的,那就是機械磁碟不善於處理IO地址差異比較大的請求(會導致機械臂頻繁擺動),這是我們在做架構設計的時候需要注意的。雖然作業系統和通用塊層為我們做了很多工作,但其能力畢竟有限,因此我們在設計的時候也必須考慮。後面我們會透過例項給大家介紹大牛公司在設計應用的時候是如何考慮的。