總線
降低復雜性:總線的設計思路來源
計算機里其實有很多不同的硬件設備,除了 CPU 和內存之外,我們還有大量的輸入輸出設備。可以說,你計算機上的每一個接口,鍵盤、鼠標、顯示器、硬盤,乃至通過 USB 接口連接的各種外部設備,都對應了一個設備或者模塊。
如果各個設備間的通信,都是互相之間單獨進行的。如果我們有 N 個不同的設備,他們之間需要各自單獨連接,那么系統復雜度就會變成 N^2。每一個設備或者功能電路模塊,都要和其他 N−1 個設備去通信。為了簡化系統的復雜度,我們就引入了總線,把這個 N^2 的復雜度,變成一個 N 的復雜度。
那怎么降低復雜度呢?與其讓各個設備之間互相單獨通信,不如我們去設計一個公用的線路。CPU 想要和什么設備通信,通信的指令是什么,對應的數據是什么,都發送到這個線路上;設備要向 CPU 發送什么信息呢,也發送到這個線路上。這個線路就好像一個高速公路,各個設備和其他設備之間,不需要單獨建公路,只建一條小路通向這條高速公路就好了。
這個設計思路,就是我們今天要說的總線(Bus)。
總線,其實就是一組線路。我們的 CPU、內存以及輸入和輸出設備,都是通過這組線路,進行相互間通信的。總線的英文叫作 Bus,就是一輛公交車。這個名字很好地描述了總線的含義。我們的“公交車”的各個站點,就是各個接入設備。要想向一個設備傳輸數據,我們只要把數據放上公交車,在對應的車站下車就可以了。
其實,對應的設計思路,在軟件開發中也是非常常見的。我們在做大型系統開發的過程中,經常會用到一種叫作事件總線(Event Bus)的設計模式。
進行大規模應用系統開發的時候,系統中的各個組件之間也需要相互通信。模塊之間如果是兩兩之間單獨去定義協議,這個軟件系統一樣會遇到一個復雜度變成了 N^2 的問題。所以常見的一個解決方案,就是事件總線這個設計模式。
在事件總線這個設計模式里,各個模塊觸發對應的事件,並把事件對象發送到總線上。也就是說,每個模塊都是一個發布者(Publisher)。而各個模塊也會把自己注冊到總線上,去監聽總線上的事件,並根據事件的對象類型或者是對象內容,來決定自己是否要進行特定的處理或者響應。
這樣的設計下,注冊在總線上的各個模塊就是松耦合的。模塊互相之間並沒有依賴關系。無論代碼的維護,還是未來的擴展,都會很方便。
理解總線:三種線路和多總線架構
理解了總線的設計概念,我們來看看,總線在實際的計算機硬件里面,到底是什么樣。
現代的 Intel CPU 的體系結構里面,通常有好幾條總線。
首先,CPU 和內存以及高速緩存通信的總線,這里面通常有兩種總線。這種方式,我們稱之為雙獨立總線(Dual Independent Bus,縮寫為 DIB)。CPU 里,有一個快速的本地總線(Local Bus),以及一個速度相對較慢的前端總線(Front-side Bus)。
現代的 CPU 里,通常有專門的高速緩存芯片。這里的高速本地總線,就是用來和高速緩存通信的。而前端總線,則是用來和主內存以及輸入輸出設備通信的。有時候,我們會把本地總線也叫作后端總線(Back-side Bus),和前面的前端總線對應起來。而前端總線也有很多其他名字,比如處理器總線(Processor Bus)、內存總線(Memory Bus)。
除了前端總線呢,我們常常還會聽到 PCI 總線、I/O 總線或者系統總線(System Bus)。其實各種總線的命名一直都很混亂,我們不如直接來看一看CPU 的硬件架構圖。對照圖來看,一切問題就都清楚了。
CPU 里面的北橋芯片,把我們上面說的前端總線,一分為二,變成了三個總線。
我們的前端總線,其實就是系統總線。CPU 里面的內存接口,直接和系統總線通信,然后系統總線再接入一個 I/O 橋接器(I/O Bridge)。這個 I/O 橋接器,一邊接入了我們的內存總線,使得我們的 CPU 和內存通信;另一邊呢,又接入了一個 I/O 總線,用來連接 I/O 設備。
事實上,真實的計算機里,這個總線層面拆分得更細。根據不同的設備,還會分成獨立的 PCI 總線、ISA 總線等等。
在物理層面,其實我們完全可以把總線看作一組“電線”。不過呢,這些電線之間也是有分工的,我們通常有三類線路。
- 數據線(Data Bus),用來傳輸實際的數據信息,也就是實際上了公交車的“人”。
- 地址線(Address Bus),用來確定到底把數據傳輸到哪里去,是內存的某個位置,還是某一個 I/O 設備。這個其實就相當於拿了個紙條,寫下了上面的人要下車的站點。
- 控制線(Control Bus),用來控制對於總線的訪問。雖然我們把總線比喻成了一輛公交車。那么有人想要做公交車的時候,需要告訴公交車司機,這個就是我們的控制信號。
盡管總線減少了設備之間的耦合,也降低了系統設計的復雜度,但同時也帶來了一個新問題,那就是總線不能同時給多個設備提供通信功能。
我們的總線是很多個設備公用的,那多個設備都想要用總線,我們就需要有一個機制,去決定這種情況下,到底把總線給哪一個設備用。這個機制,就叫作總線裁決(Bus Arbitraction)。總線裁決的機制有很多種不同的實現,如果你對這個實現的細節感興趣,可以去看一看 Wiki 里面關於裁決器的對應條目。
總結
講解了計算機里各個不同的組件之間用來通信的渠道,也就是總線。總線的設計思路,核心是為了減少多個模塊之間交互的復雜性和耦合度。實際上,總線這個設計思路在我們的軟件開發過程中也經常會被用到。事件總線就是我們常見的一個設計模式,通常事件總線也會和訂閱者發布者模式結合起來,成為大型系統的各個松耦合的模塊之間交互的一種主要模式。
在實際的硬件層面,總線其實就是一組連接電路的線路。因為不同設備之間的速度有差異,所以一台計算機里面往往會有多個總線。常見的就有在 CPU 內部和高速緩存通信的本地總線,以及和外部 I/O 設備以及內存通信的前端總線。
前端總線通常也被叫作系統總線。它可以通過一個 I/O 橋接器,拆分成兩個總線,分別來和 I/O 設備以及內存通信。自然,這樣拆開的兩個總線,就叫作 I/O 總線和內存總線。
總線本身的電路功能,又可以拆分成用來傳輸數據的數據線、用來傳輸地址的地址線,以及用來傳輸控制信號的控制線。
總線是一個各個接入的設備公用的線路,所以自然會在各個設備之間爭奪總線所有權的情況。於是,我們需要一個機制來決定讓誰來使用總線,這個決策機制就是總線裁決。
輸入輸出設備
接口和設備:經典的適配器模式
輸入輸出設備,並不只是一個設備。大部分的輸入輸出設備,都有兩個組成部分。
第一個是它的接口(Interface),第二個才是實際的 I/O 設備(Actual I/O Device)。
我們的硬件設備並不是直接接入到總線上和 CPU 通信的,而是通過接口,用接口連接到總線上,再通過總線和 CPU 通信。
你平時聽說的並行接口(Parallel Interface)、串行接口(Serial Interface)、USB 接口,都是計算機主板上內置的各個接口。我們的實際硬件設備,比如,使用並口的打印機、使用串口的老式鼠標或者使用 USB 接口的 U 盤,都要插入到這些接口上,才能和 CPU 工作以及通信的。
接口本身就是一塊電路板。CPU 其實不是和實際的硬件設備打交道,而是和這個接口電路板打交道。我們平時說的,設備里面有三類寄存器,其實都在這個設備的接口電路上,而不在實際的設備上。
那這三類寄存器是哪三類寄存器呢?
它們分別是狀態寄存器(Status Register)、 命令寄存器(Command Register)以及數據寄存器(Data Register),
除了內置在主板上的接口之外,有些接口可以集成在設備上。你可能都沒有見過老一點兒的硬盤,簡單給你介紹一下。
上世紀 90 年代的時候,大家用的硬盤都叫作IDE 硬盤。這個 IDE 不是像 IntelliJ 或者 WebStorm 這樣的軟件開發集成環境(Integrated Development Environment)的 IDE,而是代表着集成設備電路(Integrated Device Electronics)。也就是說,設備的接口電路直接在設備上,而不在主板上。我們需要通過一個線纜,把集成了接口的設備連接到主板上去。
把接口和實際設備分離,這個做法實際上來自於計算機走向開放架構(Open Architecture)的時代。
當我們要對計算機升級,我們不會扔掉舊的計算機,直接買一台全新的計算機,而是可以單獨升級硬盤這樣的設備。我們把老硬盤從接口上拿下來,換一個新的上去就好了。各種輸入輸出設備的制造商,也可以根據接口的控制協議,來設計和制造硬盤、鼠標、鍵盤、打印機乃至其他種種外設。正是這樣的分工協作,帶來了 PC 時代的繁榮。
其實,在軟件的設計模式里也有這樣的思路。面向對象里的面向接口編程的接口,就是 Interface。如果你做 iOS 的開發,Objective-C 里面的 Protocol 其實也是這個意思。而 Adaptor 設計模式,更是一個常見的、用來解決不同外部應用和系統“適配”問題的方案。可以看到,計算機的軟件和硬件,在邏輯抽象上,其實是相通的。
如果你用的是 Windows 操作系統,你可以打開設備管理器,里面有各種各種的 Devices(設備)、Controllers(控制器)、Adaptors(適配器)。這些,其實都是對於輸入輸出設備不同角度的描述。被叫作 Devices,看重的是實際的 I/O 設備本身。被叫作 Controllers,看重的是輸入輸出設備接口里面的控制電路。而被叫作 Adaptors,則是看重接口作為一個適配器后面可以插上不同的實際設備。
CPU 是如何控制 I/O 設備的?
無論是內置在主板上的接口,還是集成在設備上的接口,除了三類寄存器之外,還有對應的控制電路。正是通過這個控制電路,CPU 才能通過向這個接口電路板傳輸信號,來控制實際的硬件。
我們先來看一看,硬件設備上的這些寄存器有什么用。這里,我拿我們平時用的打印機作為例子。
-
首先是數據寄存器(Data Register)。CPU 向 I/O 設備寫入需要傳輸的數據,比如要打印的內容是“GeekTime”,我們就要先發送一個“G”給到對應的 I/O 設備。
-
然后是命令寄存器(Command Register)。CPU 發送一個命令,告訴打印機,要進行打印工作。這個時候,打印機里面的控制電路會做兩個動作。
第一個,是去設置我們的狀態寄存器里面的狀態,把狀態設置成 not-ready。
第二個,就是實際操作打印機進行打印。
-
而狀態寄存器(Status Register),就是告訴了我們的 CPU,現在設備已經在工作了,所以這個時候,CPU 你再發送數據或者命令過來,都是沒有用的。直到前面的動作已經完成,狀態寄存器重新變成了 ready 狀態,我們的 CPU 才能發送下一個字符和命令。
當然,在實際情況中,打印機里通常不只有數據寄存器,還會有數據緩沖區。我們的 CPU 也不是真的一個字符一個字符這樣交給打印機去打印的,而是一次性把整個文檔傳輸到打印機的內存或者數據緩沖區里面一起打印的。不過,通過上面這個例子,相信你對 CPU 是怎么操作 I/O 設備的,應該有所了解了。
信號和地址:發揮總線的價值
搞清楚了實際的 I/O 設備和接口之間的關系,一個新的問題就來了。那就是,我們的 CPU 到底要往總線上發送一個什么樣的命令,才能和 I/O 接口上的設備通信呢?
CPU 和 I/O 設備的通信,一樣是通過 CPU 支持的機器指令來執行的。
看看MIPS 的機器指令的分類,你會發現,我們並沒有一種專門的和 I/O 設備通信的指令類型。那么,MIPS 的 CPU 到底是通過什么樣的指令來和 I/O 設備來通信呢?
答案就是,和訪問我們的主內存一樣,使用“內存地址”。為了讓已經足夠復雜的 CPU 盡可能簡單,計算機會把 I/O 設備的各個寄存器,以及 I/O 設備內部的內存地址,都映射到主內存地址空間里來。主內存的地址空間里,會給不同的 I/O 設備預留一段一段的內存地址。CPU 想要和這些 I/O 設備通信的時候呢,就往這些地址發送數據。這些地址信息,就是通過地址線來發送的,而對應的數據信息呢,自然就是通過數據線來發送的了。
而我們的 I/O 設備呢,就會監控地址線,並且在 CPU 往自己地址發送數據的時候,把對應的數據線里面傳輸過來的數據,接入到對應的設備里面的寄存器和內存里面來。CPU 無論是向 I/O 設備發送命令、查詢狀態還是傳輸數據,都可以通過這樣的方式。這種方式呢,叫作內存映射IO(Memory-Mapped I/O,簡稱 MMIO)。
MMIO 不是唯一一種 CPU 和設備通信的方式。精簡指令集 MIPS 的 CPU 特別簡單,所以這里只有 MMIO。而我們有 2000 多個指令的 Intel X86 架構的計算機,自然可以設計專門的和 I/O 設備通信的指令,也就是 in 和 out 指令。
Intel CPU 雖然也支持 MMIO,不過它還可以通過特定的指令,來支持端口映射 I/O(Port-Mapped I/O,簡稱 PMIO)或者也可以叫獨立輸入輸出(Isolated I/O)。
其實 PMIO 的通信方式和 MMIO 差不多,核心的區別在於,PMIO 里面訪問的設備地址,不再是在內存地址空間里面,而是一個專門的端口(Port)。這個端口並不是指一個硬件上的插口,而是和 CPU 通信的一個抽象概念。
無論是 PMIO 還是 MMIO,CPU 都會傳送一條二進制的數據,給到 I/O 設備的對應地址。設備自己本身的接口電路,再去解碼這個數據。解碼之后的數據呢,就會變成設備支持的一條指令,再去通過控制電路去操作實際的硬件設備。對於 CPU 來說,它並不需要關心設備本身能夠支持哪些操作。它要做的,只是在總線上傳輸一條條數據就好了。
這個,其實也有點像我們在設計模式里面的 Command 模式。我們在總線上傳輸的,是一個個數據對象,然后各個接受這些對象的設備,再去根據對象內容,進行實際的解碼和命令執行。
總結
CPU 並不是發送一個特定的操作指令來操作不同的 I/O 設備。因為如果是那樣的話,隨着新的 I/O 設備的發明,我們就要去擴展 CPU 的指令集了。
在計算機系統里面,CPU 和 I/O 設備之間的通信,是這么來解決的。
首先,在 I/O 設備這一側,我們把 I/O 設備拆分成,能和 CPU 通信的接口電路,以及實際的 I/O 設備本身。接口電路里面有對應的狀態寄存器、命令寄存器、數據寄存器、數據緩沖區和設備內存等等。接口電路通過總線和 CPU 通信,接收來自 CPU 的指令和數據。而接口電路中的控制電路,再解碼接收到的指令,實際去操作對應的硬件設備。
而在 CPU 這一側,對 CPU 來說,它看到的並不是一個個特定的設備,而是一個個內存地址或者端口地址。CPU 只是向這些地址傳輸數據或者讀取數據。所需要的指令和操作內存地址的指令其實沒有什么本質差別。通過軟件層面對於傳輸的命令數據的定義,而不是提供特殊的新的指令,來實際操作對應的 I/O 硬件。
IO_WAIT
IO 性能、順序訪問和隨機訪問
如果去看硬盤廠商的性能報告,通常你會看到兩個指標。一個是響應時間(Response Time),另一個叫作數據傳輸率(Data Transfer Rate)。
我們現在常用的硬盤有兩種。一種是 HDD 硬盤,也就是我們常說的機械硬盤。另一種是 SSD 硬盤,一般也被叫作固態硬盤。現在的 HDD 硬盤,用的是 SATA 3.0 的接口。而 SSD 硬盤呢,通常會用兩種接口,一部分用的也是 SATA 3.0 的接口;另一部分呢,用的是 PCI Express 的接口。
現在我們常用的 SATA 3.0 的接口,帶寬是 6Gb/s。這里的“b”是比特。這個帶寬相當於每秒可以傳輸 768MB 的數據。而我們日常用的 HDD 硬盤的數據傳輸率,差不多在 200MB/s 左右。
當我們換成 SSD 的硬盤,性能自然會好上不少。比如,Crucial MX500 的 SSD 硬盤。它的數據傳輸速率能到差不多 500MB/s,比 HDD 的硬盤快了一倍不止。不過 SATA 接口的硬盤,差不多到這個速度,性能也就到頂了。因為 SATA 接口的速度也就這么快。
不過,實際 SSD 硬盤能夠更快,所以我們可以換用 PCI Express 的接口。它的數據傳輸率,在讀取的時候就能做到 2GB/s 左右,差不多是 HDD 硬盤的 10 倍,而在寫入的時候也能有 1.2GB/s。
除了數據傳輸率這個吞吐率指標,另一個我們關心的指標響應時間,其實也可以在 AS SSD 的測試結果里面看到,就是這里面的 Acc.Time 指標。
這個指標,其實就是程序發起一個硬盤的寫入請求,直到這個請求返回的時間。可以看到,在上面的兩塊 SSD 硬盤上,大概時間都是在幾十微秒這個級別。如果你去測試一塊 HDD 的硬盤,通常會在幾毫秒到十幾毫秒這個級別。這個性能的差異,就不是 10 倍了,而是在幾十倍,乃至幾百倍。
光看響應時間和吞吐率這兩個指標,似乎我們的硬盤性能很不錯。即使是廉價的 HDD 硬盤,接收一個來自 CPU 的請求,也能夠在幾毫秒時間返回。一秒鍾能夠傳輸的數據,也有 200MB 左右。你想一想,我們平時往數據庫里寫入一條記錄,也就是 1KB 左右的大小。我們拿 200MB 去除以 1KB,那差不多每秒鍾可以插入 20 萬條數據呢。但是這個計算出來的數字,似乎和我們日常的經驗不符合啊?這又是為什么呢?
答案就來自於硬盤的讀寫。在順序讀寫和隨機讀寫的情況下,硬盤的性能是完全不同的。
我們回頭看一下上面的 AS SSD 的性能指標。你會看到,里面有一個“4K”的指標。這個指標是什么意思呢?它其實就是我們的程序,去隨機讀取磁盤上某一個 4KB 大小的數據,一秒之內可以讀取到多少數據。
你會發現,在這個指標上,我們使用 SATA 3.0 接口的硬盤和 PCI Express 接口的硬盤,性能差異變得很小。這是因為,在這個時候,接口本身的速度已經不是我們硬盤訪問速度的瓶頸了。更重要的是,你會發現,即使我們用 PCI Express 的接口,在隨機讀寫的時候,數據傳輸率也只能到 40MB/s 左右,是順序讀寫情況下的幾十分之一。
我們拿這個 40MB/s 和一次讀取 4KB 的數據算一下。
也就是說,一秒之內,這塊 SSD 硬盤可以隨機讀取 1 萬次的 4KB 的數據。如果是寫入的話呢,會更多一些,90MB /4KB 差不多是 2 萬多次。
這個每秒讀寫的次數,我們稱之為IOPS,也就是每秒輸入輸出操作的次數。事實上,比起響應時間,我們更關注 IOPS 這個性能指標。IOPS 和 DTR(Data Transfer Rate,數據傳輸率)才是輸入輸出性能的核心指標。
這是因為,我們在實際的應用開發當中,對於數據的訪問,更多的是隨機讀寫,而不是順序讀寫。我們平時所說的服務器承受的“並發”,其實是在說,會有很多個不同的進程和請求來訪問服務器。自然,它們在硬盤上訪問的數據,是很難順序放在一起的。這種情況下,隨機讀寫的 IOPS 才是服務器性能的核心指標。
好了,回到我們引出 IOPS 這個問題的 HDD 硬盤。我現在要問你了,那一塊 HDD 硬盤能夠承受的 IOPS 是多少呢?
HDD 硬盤的 IOPS 通常也就在 100 左右,而不是在 20 萬次。
如何定位 IO_WAIT?
我們看到,即使是用上了 PCI Express 接口的 SSD 硬盤,IOPS 也就是在 2 萬左右。而我們的 CPU 的主頻通常在 2GHz 以上,也就是每秒可以做 20 億次操作。
即使 CPU 向硬盤發起一條讀寫指令,需要很多個時鍾周期,一秒鍾 CPU 能夠執行的指令數,和我們硬盤能夠進行的操作數,也有好幾個數量級的差異。這也是為什么,我們在應用開發的時候往往會說“性能瓶頸在 I/O 上”。因為很多時候,CPU 指令發出去之后,不得不去“等”我們的 I/O 操作完成,才能進行下一步的操作。
那么,在實際遇到服務端程序的性能問題的時候,我們怎么知道這個問題是不是來自於 CPU 等 I/O 來完成操作呢?別着急,我們接下來,就通過 top
和 iostat
這些命令,一起來看看 CPU 到底有沒有在等待 io 操作。
# top
你一定在 Linux 下用過 top
命令。對於很多剛剛入門 Linux 的同學,會用 top 去看服務的負載,也就是 load average。不過,在 top 命令里面,我們一樣可以看到 CPU 是否在等待 IO 操作完成。
top - 06:26:30 up 4 days, 53 min, 1 user, load average: 0.79, 0.69, 0.65
Tasks: 204 total, 1 running, 203 sleeping, 0 stopped, 0 zombie
%Cpu(s): 20.0 us, 1.7 sy, 0.0 ni, 77.7 id, 0.0 wa, 0.0 hi, 0.7 si, 0.0 st
KiB Mem: 7679792 total, 6646248 used, 1033544 free, 251688 buffers
KiB Swap: 0 total, 0 used, 0 free. 4115536 cached Mem
top 命令的輸出結果
在 top
命令的輸出結果里面,有一行是以 %CPU 開頭的。這一行里,有一個叫作 wa
的指標,這個指標就代表着 iowait
,也就是 CPU 等待 IO 完成操作花費的時間占 CPU 的百分比。下一次,當你自己的服務器遇到性能瓶頸,load 很大的時候,你就可以通過 top 看一看這個指標。
知道了 iowait
很大,那么我們就要去看一看,實際的 I/O 操作情況是什么樣的。這個時候,你就可以去用 iostat
這個命令了。我們輸入“iostat
”,就能夠看到實際的硬盤讀寫情況。
$ iostat
avg-cpu: %user %nice %system %iowait %steal %idle
17.02 0.01 2.18 0.04 0.00 80.76
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 1.81 2.02 30.87 706768 10777408
你會看到,這個命令里,不僅有 iowait
這個 CPU 等待時間的百分比,還有一些更加具體的指標了,並且它還是按照你機器上安裝的多塊不同的硬盤划分的。
這里的 tps
指標,其實就對應着我們上面所說的硬盤的 IOPS 性能。而 kB_read/s 和 kB_wrtn/s 指標,就對應着我們的數據傳輸率的指標。
知道實際硬盤讀寫的 tps
、kB_read/s 和 kb_wrtn/s 的指標,我們基本上可以判斷出,機器的性能是不是卡在 I/O 上了。那么,接下來,我們就是要找出到底是哪一個進程是這些 I/O 讀寫的來源了。這個時候,你需要“iotop
”這個命令。
$ iotop
Total DISK READ : 0.00 B/s | Total DISK WRITE : 15.75 K/s
Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 35.44 K/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
104 be/3 root 0.00 B/s 7.88 K/s 0.00 % 0.18 % [jbd2/sda1-8]
383 be/4 root 0.00 B/s 3.94 K/s 0.00 % 0.00 % rsyslogd -n [rs:main Q:Reg]
1514 be/4 www-data 0.00 B/s 3.94 K/s 0.00 % 0.00 % nginx: worker process
通過 iotop
這個命令,你可以看到具體是哪一個進程實際占用了大量 I/O,那么你就可以有的放矢,去優化對應的程序了。
上面的這些示例里,不管是 wa
也好,tps
也好,它們都很小。那么,接下來,我就給你用 Linux 下,用 stress 命令,來模擬一個高 I/O 復雜的情況,來看看這個時候的 iowait
是怎么樣的。
在一台雲平台上的單個 CPU 核心的機器上輸入“stress -i 2
”,讓 stress 這個程序模擬兩個進程不停地從內存里往硬盤上寫數據。
$ stress -i 2
$ top
你會看到,在 top 的輸出里面,CPU 就有大量的 sy
和 wa
,也就是系統調用和 iowait。
top - 06:56:02 up 3 days, 19:34, 2 users, load average: 5.99, 1.82, 0.63
Tasks: 88 total, 3 running, 85 sleeping, 0 stopped, 0 zombie
%Cpu(s): 3.0 us, 29.9 sy, 0.0 ni, 0.0 id, 67.2 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 1741304 total, 1004404 free, 307152 used, 429748 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 1245700 avail Mem
$ iostat 2 5
如果我們通過 iostat
,查看硬盤的 I/O,你會看到,里面的 tps
很快就到了 4 萬左右,占滿了對應硬盤的 IOPS。
avg-cpu: %user %nice %system %iowait %steal %idle
5.03 0.00 67.92 27.04 0.00 0.00
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 39762.26 0.00 0.00 0 0
如果這個時候我們去看一看 iotop
,你就會發現,我們的 I/O 占用,都來自於 stress 產生的兩個進程了。
$ iotop
Total DISK READ : 0.00 B/s | Total DISK WRITE : 0.00 B/s
Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 0.00 B/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
29161 be/4 xuwenhao 0.00 B/s 0.00 B/s 0.00 % 56.71 % stress -i 2
29162 be/4 xuwenhao 0.00 B/s 0.00 B/s 0.00 % 46.89 % stress -i 2
1 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % init
相信到了這里,你也應該學會了怎么通過 top
、iostat
以及 iotop
,一步一步快速定位服務器端的 I/O 帶來的性能瓶頸了。你也可以自己通過 Linux 的 man 命令,看一看這些命令還有哪些參數,以及通過 stress 來模擬其他更多不同的性能壓力,看看我們的機器負載會發生什么變化。
總結
在順序讀取的情況下,無論是 HDD 硬盤還是 SSD 硬盤,性能看起來都是很不錯的。不過,等到進行隨機讀取測試的時候,硬盤的性能才能見了真章。因為在大部分的應用開發場景下,我們關心的並不是在順序讀寫下的數據量,而是每秒鍾能夠進行輸入輸出的操作次數,也就是 IOPS 這個核心性能指標。
你會發現,即使是使用 PCI Express 接口的 SSD 硬盤,IOPS 也就只是到了 2 萬左右。這個性能,和我們 CPU 的每秒 20 億次操作的能力比起來,可就差得遠了。所以很多時候,我們的程序對外響應慢,其實都是 CPU 在等待 I/O 操作完成。
在 Linux 下,我們可以通過 top
這樣的命令,來看整個服務器的整體負載。在應用響應慢的時候,我們可以先通過這個指令,來看 CPU 是否在等待 I/O 完成自己的操作。進一步地,我們可以通過 iostat
這個命令,來看到各個硬盤這個時候的讀寫情況。而 iotop
這個命令,能夠幫助我們定位到到底是哪一個進程在進行大量的 I/O 操作。
這些命令的組合,可以快速幫你定位到是不是我們的程序遇到了 I/O 的瓶頸,以及這些瓶頸來自於哪些程序,你就可以根據定位的結果來優化你自己的程序了。
機械硬盤
拆解機械硬盤
機械硬盤的 IOPS,大概只能做到每秒 100 次左右。那么,這個 100 次究竟是怎么來的呢?
我們把機械硬盤拆開來看一看,看看它的物理構造是怎么樣的,你就自然知道為什么它的 IOPS 是 100 左右了。
我們之前看過整個硬盤的構造,里面有接口,有對應的控制電路版,以及實際的 I/O 設備(也就是我們的機械硬盤)。這里,我們就拆開機械硬盤部分來看一看。
一塊機械硬盤是由盤面、磁頭和懸臂三個部件組成的。下面我們一一來看每一個部件。
首先,自然是盤面(Disk Platter)。盤面其實就是我們實際存儲數據的盤片。如果你剪開過軟盤的外殼,或者看過光盤 DVD,那你看到盤面應該很熟悉。盤面其實和它們長得差不多。
盤面本身通常是用的鋁、玻璃或者陶瓷這樣的材質做成的光滑盤片。然后,盤面上有一層磁性的塗層。我們的數據就存儲在這個磁性的塗層上。盤面中間有一個受電機控制的轉軸。這個轉軸會控制我們的盤面去旋轉。
我們平時買硬盤的時候經常會聽到一個指標,叫作這個硬盤的轉速。我們的硬盤有 5400 轉的、7200 轉的,乃至 10000 轉的。這個多少多少轉,指的就是盤面中間電機控制的轉軸的旋轉速度,英文單位叫RPM,也就是每分鍾的旋轉圈數(Rotations Per Minute)。所謂 7200 轉,其實更准確地說是 7200RPM,指的就是一旦電腦開機供電之后,我們的硬盤就可以一直做到每分鍾轉上 7200 圈。如果折算到每一秒鍾,就是 120 圈。
說完了盤面,我們來看磁頭(Drive Head)。我們的數據並不能直接從盤面傳輸到總線上,而是通過磁頭,從盤面上讀取到,然后再通過電路信號傳輸給控制電路、接口,再到總線上的。
通常,我們的一個盤面上會有兩個磁頭,分別在盤面的正反面。盤面在正反兩面都有對應的磁性塗層來存儲數據,而且一塊硬盤也不是只有一個盤面,而是上下堆疊了很多個盤面,各個盤面之間是平行的。每個盤面的正反兩面都有對應的磁頭。
最后我們來看懸臂(Actutor Arm)。懸臂鏈接在磁頭上,並且在一定范圍內會去把磁頭定位到盤面的某個特定的磁道(Track)上。這個磁道是怎么來呢?想要了解這個問題,我們要先看一看我們的數據是怎么存放在盤面上的。
一個盤面通常是圓形的,由很多個同心圓組成,就好像是一個個大小不一樣的“甜甜圈”嵌套在一起。每一個“甜甜圈”都是一個磁道。每個磁道都有自己的一個編號。懸臂其實只是控制,到底是讀最里面那個“甜甜圈”的數據,還是最外面“甜甜圈”的數據。
知道了我們硬盤的物理構成,現在我們就可以看一看,這樣的物理結構,到底是怎么來讀取數據的。
我們剛才說的一個磁道,會分成一個一個扇區(Sector)。上下平行的一個一個盤面的相同扇區呢,我們叫作一個柱面(Cylinder)。
讀取數據,其實就是兩個步驟。
一個步驟,就是把盤面旋轉到某一個位置。在這個位置上,我們的懸臂可以定位到整個盤面的某一個子區間。這個子區間的形狀有點兒像一塊披薩餅,我們一般把這個區間叫作幾何扇區(Geometrical Sector),意思是,在“幾何位置上”,所有這些扇區都可以被懸臂訪問到。
另一個步驟,就是把我們的懸臂移動到特定磁道的特定扇區,也就在這個“幾何扇區”里面,找到我們實際的扇區。找到之后,我們的磁頭會落下,就可以讀取到正對着扇區的數據。
所以,我們進行一次硬盤上的隨機訪問,需要的時間由兩個部分組成。
第一個部分,叫作平均延時(Average Latency)。這個時間,其實就是把我們的盤面旋轉,把幾何扇區對准懸臂位置的時間。這個時間很容易計算,它其實就和我們機械硬盤的轉速相關。隨機情況下,平均找到一個幾何扇區,我們需要旋轉半圈盤面。上面 7200 轉的硬盤,那么一秒里面,就可以旋轉 240 個半圈。那么,這個平均延時就是
第二個部分,叫作平均尋道時間(Average Seek Time),也就是在盤面選轉之后,我們的懸臂定位到扇區的的時間。我們現在用的 HDD 硬盤的平均尋道時間一般在 4-10ms。
這樣,我們就能夠算出來,如果隨機在整個硬盤上找一個數據,需要 8-14 ms。我們的硬盤是機械結構的,只有一個電機轉軸,也只有一個懸臂,所以我們沒有辦法並行地去定位或者讀取數據。那一塊 7200 轉的硬盤,我們一秒鍾隨機的 IO 訪問次數,也就是
如果我們不是去進行隨機的數據訪問,而是進行順序的數據讀寫,我們應該怎么最大化讀取效率呢?
我們可以選擇把順序存放的數據,盡可能地存放在同一個柱面上。這樣,我們只需要旋轉一次盤面,進行一次尋道,就可以去寫入或者讀取,同一個垂直空間上的多個盤面的數據。如果一個柱面上的數據不夠,我們也不要去動懸臂,而是通過電機轉動盤面,這樣就可以順序讀完一個磁道上的所有數據。所以,其實對於 HDD 硬盤的順序數據讀寫,吞吐率還是很不錯的,可以達到 200MB/s 左右。
Partial Stroking:根據場景提升性能
只有 100 的 IOPS,其實很難滿足現在互聯網海量高並發的請求。所以,今天的數據庫,都會把數據存儲在 SSD 硬盤上。不過,如果我們把時鍾倒播 20 年,那個時候,我們可沒有現在這么便宜的 SSD 硬盤。數據庫里面的數據,只能存放在 HDD 硬盤上。
今天,即便是數據中心用的 HDD 硬盤,一般也是 7200 轉的,因為如果要更快的隨機訪問速度,我們會選擇用 SSD 硬盤。但是在當時,SSD 硬盤價格非常昂貴,還沒有能夠商業化。硬盤廠商們在不斷地研發轉得更快的硬盤。在數據中心里,往往我們會用上 10000 轉,乃至 15000 轉的硬盤。甚至直到 2010 年,SSD 硬盤已經開始逐步進入市場了,西數還在嘗試研發 20000 轉的硬盤。轉速更高、尋道時間更短的機械硬盤,才能滿足實際的數據庫需求。
不過,10000 轉,乃至 15000 轉的硬盤也更昂貴。如果你想要節約成本,提高性價比,那就得想點別的辦法。你應該聽說過,Google 早年用家用 PC 乃至二手的硬件,通過軟件層面的設計來解決可靠性和性能的問題。那么,我們是不是也有什么辦法,能提高機械硬盤的 IOPS 呢?
還真的有。這個方法,就叫作Partial Stroking或者Short Stroking。沒有看到過有中文資料給這個方法命名。在這里,我就暫時把它翻譯成“縮短行程”技術。
其實這個方法的思路很容易理解。既然我們訪問一次數據的時間,是“平均延時 + 尋道時間”,那么只要能縮短這兩個之一,不就可以提升 IOPS 了嗎?
一般情況下,硬盤的尋道時間都比平均延時要長。那么我們自然就可以想一下,有什么辦法可以縮短平均的尋道時間。最極端的辦法就是我們不需要尋道,也就是說,我們把所有數據都放在一個磁道上。比如,我們始終把磁頭放在最外道的磁道上。這樣,我們的尋道時間就基本為 0,訪問時間就只有平均延時了。那樣,我們的 IOPS,就變成了
不過呢,只用一個磁道,我們能存的數據就比較有限了。這個時候,可能我們還不如把這些數據直接都放到內存里面呢。所以,實踐當中,我們可以只用 1/2 或者 1/4 的磁道,也就是最外面 1/4 或者 1/2 的磁道。這樣,我們硬盤可以使用的容量可能變成了 1/2 或者 1/4。但是呢,我們的尋道時間,也變成了 1/4 或者 1/2,因為懸臂需要移動的“行程”也變成了原來的 1/2 或者 1/4,我們的 IOPS 就能夠大幅度提升了。
比如說,我們一塊 7200 轉的硬盤,正常情況下,平均延時是 4.17ms,而尋道時間是 9ms。那么,它原本的 IOPS 就是
如果我們只用其中 1/4 的磁道,那么,它的 IOPS 就變成了
你看這個結果,IOPS 提升了一倍,和一塊 15000 轉的硬盤的性能差不多了。不過,這個情況下,我們的硬盤能用的空間也只有原來的 1/4 了。不過,要知道在當時,同樣容量的 15000 轉的硬盤的價格可不止是 7200 轉硬盤的 4 倍啊。所以,這樣通過軟件去格式化硬盤,只保留部分磁道讓系統可用的情況,可以大大提升硬件的性價比。
在 2000-2010 年這 10 年間,正是這些奇思妙想,讓海量數據下的互聯網蓬勃發展起來的。在沒有 SSD 的硬盤的時候,聰明的工程師們從硬件到軟件,設計了各種有意思的方案解決了我們遇到的各類性能問題。而對於計算機底層知識的深入了解,也是能夠找到這些解決辦法的核心因素。
總結
機械硬盤的硬件,主要由盤面、磁頭和懸臂三部分組成。我們的數據在盤面上的位置,可以通過磁道、扇區和柱面來定位。實際的一次對於硬盤的訪問,需要把盤面旋轉到某一個“幾何扇區”,對准懸臂的位置。然后,懸臂通過尋道,把磁頭放到我們實際要讀取的扇區上。
受制於機械硬盤的結構,我們對於隨機數據的訪問速度,就要包含旋轉盤面的平均延時和移動懸臂的尋道時間。通過這兩個時間,我們能計算出機械硬盤的 IOPS。
7200 轉機械硬盤的 IOPS,只能做到 100 左右。在互聯網時代的早期,我們也沒有 SSD 硬盤可以用,所以工程師們就想出了 Partial Stroking 這個浪費存儲空間,但是可以縮短尋道時間來提升硬盤的 IOPS 的解決方案。這個解決方案,也是一個典型的、在深入理解了硬件原理之后的軟件優化方案。