存儲介質的性能
話不多說,先看一張圖,下圖左邊是磁盤到內存的不同介質,右邊形象地描述了每種介質的讀寫速率。一句話總結就是越靠近cpu,讀寫性能越快。了解了不同硬件介質的讀寫速率后,你會發現零拷貝技術是多么的香,對于追求極致性能的讀寫系統而言,掌握這個技術是多么的優秀~
上圖是當前主流存儲介質的讀寫性能,從磁盤到內存、內存到緩存、緩存到寄存器,每上一個臺階,性能就提升10倍。如果我們打開一個文件去讀里面的內容,你會發現時間讀取的時間是遠大于磁盤提供的這個時延的,這是為什么呢?問題就在內核態和用戶態這2個概念后面深藏的I/O邏輯作怪。
內核態和用戶態
內核態:也稱為內核空間。cpu可以訪問內存的所有數據,還控制著外圍設備的訪問,例如硬盤、網卡、鼠標、鍵盤等。cpu也可以將自己從一個程序切換到另一個程序。
用戶態:也稱為用戶空間。只能受限的訪問內存地址,cpu資源可以被其他程序獲取。
計算機資源的管控范圍
坦白地說內核態就是一個高級管理員,它可以控制整個資源的權限,用戶態就是一個業務,每個人都可以使用它。那計算機為啥要這么分呢?且看下文......
由于需要限制不同的程序之間的訪問能力, 防止他們獲取別的程序的內存數據, 或者獲取外圍設備的數據, 并發送到網絡。CPU劃分出兩個權限等級:用戶態和內核態。
32 位操作系統和 64 位操作系統的虛擬地址空間大小是不同的,在 Linux 操作系統中,虛擬地址空間的內部又被分為內核空間和用戶空間兩部分,如下所示:
通過這里可以看出:
32 位系統的內核空間占用 1G,位于最高處,剩下的 3G 是用戶空間;
64 位系統的內核空間和用戶空間都是 128T,分別占據整個內存空間的最高和最低處,剩下的中間部分是未定義的。
內核態控制的是內核空間的資源管理,用戶態訪問的是用戶空間內的資源。
從用戶態到內核態切換可以通過三種方式:
系統調用,其實系統調用本身就是中斷,但是軟件中斷,跟硬中斷不同。
異常:如果當前進程運行在用戶態,如果這個時候發生了異常事件,就會觸發切換。例如:缺頁異常。
外設中斷:當外設完成用戶的請求時,會向CPU發送中斷信號。
內核態和用戶態是怎么控制數據傳輸的
舉個例子:當計算機 A 上 a 進程要把一個文件傳送到計算機 B 上的 b 進程空間里面去,它是怎么做的呢?在當前的計算機系統架構下,它的 I/O 路徑如下圖所示:
計算機A的進程a先要通過系統調用Read(內核態)打開一個磁盤上的文件,這個時候就要把數據copy一次到內核態的PageCache中,進入了內核態;
進程a負責將數據從內核空間的 Page Cache 搬運到用戶空間的緩沖區,進入用戶態;
進程a負責將數據從用戶空間的緩沖區搬運到內核空間的?Socket(資源由內核管控)?緩沖區中,進入內核態。
進程a負責將數據從內核空間的 Socket 緩沖區搬運到的網絡中,進入用戶態;
從以上4個步驟我們可以發現,正是因為用戶態沒法控制磁盤和網絡資源,所以需要來回的在內核態切換。這樣一個發送文件的過程就產生了4 次上下文切換:
read 系統調用讀磁盤上的文件時:用戶態切換到內核態;
read 系統調用完畢:內核態切換回用戶態;
write 系統調用寫到socket時:用戶態切換到內核態;
write 系統調用完畢:內核態切換回用戶態。
如此笨拙的設計,我們覺得計算機是不是太幼稚了,為啥要來回切換不能直接在用戶態做數據傳輸嗎? CPU 全程負責內存內的數據拷貝,參考磁盤介質的讀寫性能,這個操作是可以接受的,但是如果要讓內存的數據和磁盤來回拷貝,這個時間消耗就非常的難看,因為磁盤、網卡的速度遠小于內存,內存又遠遠小于 CPU; 4 次 copy + 4 次上下文切換,代價太高。 所以計算機體系結構的大佬們就想到了能不能單獨地做一個模塊來專職負責這個數據的傳輸,不因為占用cpu而降低系統的吞吐呢?方案就是引入了DMA(Direct memory access)
什么是 DMA ? 沒有 DMA ,計算機程序訪問磁盤上的數據I/O 的過程是這樣的:
CPU 先發出讀指令給磁盤控制器(發出一個系統調用),然后返回;
磁盤控制器接受到指令,開始準備數據,把數據拷貝到磁盤控制器的內部緩沖區中,然后產生一個中斷;
CPU 收到中斷信號后,讓出CPU資源,把磁盤控制器的緩沖區的數據一次一個字節地拷貝進自己的寄存器,然后再把寄存器里的數據拷貝到內存,而在數據傳輸的期間 CPU 是無法執行其他任務的。
可以看到,整個數據的傳輸有幾個問題:一是數據在不同的介質之間被拷貝了多次;二是每個過程都要需要 CPU 親自參與(搬運數據的過程),在這個過程,在數據拷貝沒有完成前,CPU 是不能做額外事情的,被IO獨占。 如果I/O操作能比較快的完成,比如簡單的字符數據,那沒問題。如果我們用萬兆網卡或者硬盤傳輸大量數據,CPU就會一直被占用,其他服務無法使用,對單核系統是致命的。 為了解決上面的CPU被持續占用的問題,大佬們就提出了 DMA 技術,即直接內存訪問(Direct Memory Access)?技術。 那到底什么是 DMA 技術 所謂的 DMA(Direct Memory Access,即直接存儲器訪問)其實是一個硬件技術,其主要目的是減少大數據量傳輸時的 CPU 消耗,從而提高 CPU 利用效率。其本質上是一個主板和 IO 設備上的 DMAC 芯片。CPU 通過調度 DMAC 可以不參與磁盤緩沖區到內核緩沖區的數據傳輸消耗,從而提高效率。 那有了DMA,數據讀取過程是怎么樣的呢?下面我們來具體看看。
詳細過程:
用戶進程a調用系統調用read 方法,向OS內核(資源總管)發出 I/O 請求,請求讀取數據到自己的內存緩沖區中,進程進入阻塞狀態;
OS內核收到請求后,進一步將 I/O 請求發送 DMA,然后讓 CPU 執行其他任務;
DMA 再將 I/O 請求發送給磁盤控制器;
磁盤控制器收到 DMA 的 I/O 請求,把數據從磁盤拷貝到磁盤控制器的緩沖區中,當磁盤控制器的緩沖區被寫滿后,它向 DMA 發起中斷信號,告知自己緩沖區已滿;
DMA 收到磁盤的中斷信號后,將磁盤控制器緩沖區中的數據拷貝到內核緩沖區中,此時不占用 CPU,CPU 可以執行其他任務;
當 DMA 讀取了一個固定buffer的數據,就會發送中斷信號給 CPU;
CPU 收到 DMA 的信號,知道數據已經Ready,于是將數據從內核拷貝到用戶空間,結束系統調用;
DMA技術就是釋放了CPU的占用時間,它只做事件通知,數據拷貝完全由DMA完成。雖然DMA優化了CPU的利用率,但是并沒有提高數據讀取的性能。為了減少數據在2種狀態之間的切換次數,因為狀態切換是一個非常、非常、非常繁重的工作。為此,大佬們就提了零拷貝技術。
零拷貝技術實現的方式
常見的有2種,而今引入持久化內存后,還有APP直接訪問內存數據的方式,這里先不展開。下面介紹常用的2種方案,它們的目的減少“上下文切換”和“數據拷貝”的次數。
mmap + write(系統調用)
sendfile
mmap + write
主要目的,減少數據的拷貝 read()?系統調用:把內核緩沖區的數據拷貝到用戶的緩沖區里,用?mmap()?替換?read()?,mmap()?直接把內核緩沖區里的數據映射到用戶空間,減少這一次拷貝。 ?
具體過程如下:
應用進程調用了?mmap()?后,DMA 會把磁盤的數據拷貝到內核的緩沖區里。因為建立了這個內存的mapping,所以用戶態的數據可以直接訪問了;
應用進程再調用?write(),CPU將內核緩沖區的數據拷貝到 socket 緩沖區中,這一切都發生在內核態
DMA把內核的 socket 緩沖區里的數據,拷貝到網卡的緩沖區里
由上可知,系統調用mmap()?來代替?read(), 可以減少一次數據拷貝。那我們是否還有優化的空間呢?畢竟用戶態和內核態仍然需要 4 次上下文切換,系統調用還是 2 次。 那繼續研究下是否還能繼續減少切換和數據拷貝呢?答案是確定的:可以
sendfile
Linux 內核版本 2.1 提供了一個專門發送文件的系統調用函數?sendfile()。 參數說明:
前2個參數分別是目的端和源端的文件描述符,
后2個參數是源端的偏移量和復制數據的長度,返回值是實際復制數據的長度。
首先,使用sendfile()可以替代前面的?read()?和?write()?這兩個系統調用,減少一次系統調用和 2 次上下文切換。 其次,sendfile可以直接把內核緩沖區里的數據拷貝到 socket 緩沖區里,不再拷貝到用戶態,優化后只有 2 次上下文切換,和 3 次數據拷貝。如下圖:
盡管如此,我們還是又數據拷貝,這不符合我們的標題目標。如果網卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技術,我們就可以進一步減少通過 CPU 把內核緩沖區里的數據拷貝到 socket 緩沖區的過程。 我們可以在 Linux 系統下通過下面的命令,查看網卡是否支持 scatter-gather 特性: 于是,從 Linux 內核?2.4?版本開始起,對于支持網卡支持 SG-DMA 技術的情況下,?sendfile()?系統調用的過程發生了點變化,具體過程如下:
通過 DMA 將磁盤上的數據拷貝到內核緩沖區里;
緩沖區描述符和數據長度傳到 socket 緩沖區,這樣網卡的 SG-DMA 控制器就可以直接將內核緩存中的數據拷貝到網卡的緩沖區里;
在這個過程之中,實際上只進行了 2 次數據拷貝,如下圖:
這就是零拷貝(Zero-copy)技術,因為我們沒有在內存層面去拷貝數據,也就是說全程沒有通過 CPU 來搬運數據,所有的數據都是通過 DMA 來進行傳輸的。 零拷貝技術的文件傳輸方式相比傳統文件傳輸的方式,只需要 2 次上下文切換和數據拷貝次數,就可以完成文件的傳輸,而且 2 次的數據拷貝過程,都不需要通過 CPU,2 次都是由 DMA 來搬運。 所以,零拷貝技術可以把文件傳輸的性能提高至少一倍。
為啥要聊PageCache
回顧第一節的存儲介質的性能,如果我們總是在磁盤和內存間傳輸數據,一個大文件的跨機器傳輸肯定會讓你抓狂。那有什么方法加速呢?直觀的想法就是建立一個離CPU近的一個臨時通道,這樣就可以加速文件的傳輸。這個通道就是我們前文提到的「內核緩沖區」,這個「內核緩沖區」實際上是磁盤高速緩存(PageCache)。 零拷貝就是使用了DMA + PageCache 技術提升了性能,我們來看看 PageCache 是如何做到的。 從開篇的介質性能看,磁盤相比內存讀寫的速度要慢很多,所以優化的思路就是盡量的把「讀寫磁盤」替換成「讀寫內存」。因此通過 DMA 把磁盤里的數據搬運到內存里,轉為直接讀內存,這樣就快多了。但是內存的空間是有限的,成本也比磁盤貴,它只能拷貝磁盤里的一小部分數據。 那就不可避免的產生一個問題,到底選擇哪些磁盤數據拷貝到內存呢? 從業務的視角來看,業務的數據有冷熱之分,我們通過一些的淘汰算法可以知道哪些是熱數據,因為數據訪問的時序性,被訪問過的數據可能被再次訪問的概率很高,于是我們可以用?PageCache 來緩存最近被訪問的數據,當空間不足時淘汰最久未被訪問的數據。 讀 Cache 當內核發起一個讀請求時(例如進程發起read()請求),首先會檢查請求的數據是否緩存到了Page Cache中。如果有,那么直接從內存中讀取,不需要訪問磁盤,這被稱為cache命中(cache hit);如果cache中沒有請求的數據,即 cache 未命中(cache miss),就必須從磁盤中讀取數據。然后內核將讀取的數據緩存到 cache 中,這樣后續的讀請求就可以命中 cache 了。 page 可以只緩存一個文件部分的內容,不需要把整個文件都緩存進來。 寫 Cache 當內核發起一個寫請求時(例如進程發起write()請求),同樣是直接往cache中寫入,后備存儲中的內容不會直接更新(當服務器出現斷電關機時,存在數據丟失風險)。 內核會將被寫入的page標記為dirty,并將其加入dirty list中。內核會周期性地將dirty list中的page寫回到磁盤上,從而使磁盤上的數據和內存中緩存的數據一致。 當滿足以下兩個條件之一將觸發臟數據刷新到磁盤操作:
數據存在的時間超過了 dirty_expire_centisecs (默認300厘秒,即30秒) 時間;
臟數據所占內存 > dirty_background_ratio,也就是說當臟數據所占用的內存占總內存的比例超過dirty_background_ratio(默認10,即系統內存的10%)的時候會觸發pdflush刷新臟數據。
還有一點,現在的磁盤是擦除式讀寫,每次需要讀一個固定的大小,隨機讀取帶來的磁頭尋址會增加時延,為了降低它的影響,PageCache 使用了「預讀功能」。 在某些應用場景下,比如我們每次打開文件只需要讀取或者寫入幾個字節的情況,會比Direct I/O多一些磁盤的讀取于寫入。 舉個例子,假設每次我們要讀?32 KB?的字節,read填充到用戶buffer的大小是0~32KB,但內核會把其后面的 32~64 KB 也讀取到 PageCache,這樣后面讀取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出 PageCache 前,進程需要讀這些數據,對比分塊讀取的方式,這個策略收益就非常大。
Page Cache 的優勢與劣勢
優勢
加快對數據的訪問
減少磁盤I/O的訪問次數,提高系統磁盤壽命
減少對磁盤I/O的訪問,提高系統磁盤I/O吞吐量(Page Cache的預讀機制)
劣勢
使用額外的物理內存空間,當物理內存比較緊俏的時候,可能會導致頻繁的swap操作,最終會導致系統的磁盤I/O負載上升。
Page Cache沒有給應用層提供一個很好的API。導致應用層想要優化Page Cache的使用策略很難。因此一些應用實現了自己的Page管理,比如MySQL的InnoDB存儲引擎以16KB的頁進行管理。
另外,由于文件太大,可能某些部分的文件數據已經被淘汰出去了,這樣就會帶來 2 個問題:
PageCache 由于長時間被大文件的部分塊占據,而導致一些「熱點」的小文件可能就無法常駐 PageCache,導致頻繁讀寫磁盤而引起性能下降;
PageCache 中的大文件數據,由于沒有全部常駐內存,只有部分無法享受到緩存帶來的好處,同時過多的DMA 拷貝動作,增加了時延;
因此針對大文件的傳輸,不應該使用 PageCache。 Page Cache緩存查看工具:cachestat PageCache 的參數調優 備注:不同硬件配置的服務器可能效果不同,所以,具體的參數值設置需要考慮自己集群硬件配置。 考慮的因素主要包括:CPU核數、內存大小、硬盤類型、網絡帶寬等。 查看Page Cache參數:sysctl -a|grep dirty 調整內核參數來優化IO性能?
vm.dirty_background_ratio 參數優化:當 cached 中緩存當數據占總內存的比例達到這個參數設定的值時將觸發刷磁盤操作。把這個參數適當調小,這樣可以把原來一個大的 IO 刷盤操作變為多個小的 IO 刷盤操作,從而把 IO 寫峰值削平。對于內存很大和磁盤性能比較差的服務器,應該把這個值設置的小一點。
vm.dirty_ratio 參數優化:對于寫壓力特別大的,建議把這個參數適當調大;對于寫壓力小的可以適當調小;如果cached的數據所占比例(這里是占總內存的比例)超過這個設置,系統會停止所有的應用層的IO寫操作,等待刷完數據后恢復IO。所以萬一觸發了系統的這個操作,對于用戶來說影響非常大的。
vm.dirty_expire_centisecs參數優化:這個參數會和參數vm.dirty_background_ratio 一起來作用,一個表示大小比例,一個表示時間;即滿足其中任何一個的條件都達到刷盤的條件。
vm.dirty_writeback_centisecs 參數優化:理論上調小這個參數,可以提高刷磁盤的頻率,從而盡快把臟數據刷新到磁盤上。但一定要保證間隔時間內一定可以讓數據刷盤完成。
vm.swappiness 參數優化:禁用 swap 空間,設置 vm.swappiness=0
大文件傳輸怎么做
我們先來回顧下前文的讀流程,當調用 read 方法讀取文件時,如果數據沒有準備好,進程會阻塞在 read 方法調用,要等待磁盤數據的返回,如下圖:
具體過程:
當調用 read 方法時,切到內核態訪問磁盤資源。此時內核會向磁盤發起 I/O 請求,磁盤收到請求后,準備數據。數據讀取到控制器緩沖區完成后,就會向內核發起 I/O 中斷,通知內核磁盤數據已經準備好;
內核收到 I/O 中斷后,將數據從磁盤控制器緩沖區拷貝到 PageCache 里;
內核把 PageCache 中的數據拷貝到用戶緩沖區,read 調用返回成功。
對于大塊數傳輸導致的阻塞,可以用異步 I/O 來解決,如下圖:
分為兩步執行:
內核向磁盤發起讀請求,因為是異步請求可以不等待數據就位就可以返回,于是CPU釋放出來可以處理其他任務;
當內核將磁盤中的數據拷貝到進程緩沖區后,進程將接收到內核的通知,再去處理數據;
從上面流程來看,異步 I/O 并沒有讀寫 PageCache,繞開 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 則叫緩存 I/O。通常,對于磁盤異步 I/O 只支持直接 I/O。 因此,在高并發的場景下,針對大文件的傳輸的方式,應該使用「異步 I/O + 直接 I/O」來替代零拷貝技術。 直接 I/O 的兩種場景:
應用程序已經實現了磁盤數據的緩存大文件傳輸??
編輯:黃飛
?
評論
查看更多