【原文】http://www.scylladb.com/2017/10/05/io-access-methods-scylla/
【譯文】
大多數(shù)服務(wù)應(yīng)用開(kāi)發(fā)者考慮IO時(shí)會(huì)重點(diǎn)考慮網(wǎng)絡(luò)IO,因?yàn)樗麄冊(cè)L問(wèn)的主要資源都是基于網(wǎng)絡(luò)的,如數(shù)據(jù)庫(kù)、對(duì)象存儲(chǔ)或其他微服務(wù)。而數(shù)據(jù)庫(kù)開(kāi)發(fā)者則必須考慮文件IO。本文描述了候選方式和如何權(quán)衡,以及為什么Scylla選擇異步direct IO(AIO/DIO)作為訪問(wèn)訪問(wèn)。
一、訪問(wèn)文件的候選方式
一般Linux服務(wù)器有四種訪問(wèn)文件的方式:read/write, mmap, Direct I/O (DIO) read/write, 和異步直接direct I/O (AIO/DIO).
1.1 傳統(tǒng)read/write
應(yīng)用已久的傳統(tǒng)方式是使用read和write兩個(gè)系統(tǒng)調(diào)用。在現(xiàn)代的實(shí)現(xiàn)中,系統(tǒng)調(diào)用read(或其變種pread,readv,preadv等)訪問(wèn)內(nèi)核,讀取一段文件,拷貝數(shù)據(jù)至調(diào)用的進(jìn)程地址空間。如果所有要訪問(wèn)的數(shù)據(jù)都在頁(yè)緩存中,內(nèi)核會(huì)直接拷貝,并立即返回;否則,它需要調(diào)度磁盤(pán)以讀取所需的數(shù)據(jù)到頁(yè)緩存中,并阻塞調(diào)用線程,當(dāng)數(shù)據(jù)可用時(shí),它會(huì)恢復(fù)線程,并拷貝數(shù)據(jù)。另一方面,系統(tǒng)調(diào)用write會(huì)拷貝數(shù)據(jù)到頁(yè)緩存中,內(nèi)核會(huì)在某個(gè)時(shí)間將頁(yè)緩存寫(xiě)回到磁盤(pán)。

1.2 Mmap
一種更現(xiàn)代的替代方案是,以內(nèi)存映射的方式,使用mmap系統(tǒng)調(diào)用,將文件映射到應(yīng)用程序地址空間中。該操作的效果是,一段地址空間直接對(duì)應(yīng)包含文件數(shù)據(jù)的頁(yè)緩存。這個(gè)準(zhǔn)備步驟完成后,應(yīng)用程序可以使用進(jìn)程內(nèi)存讀寫(xiě)指令來(lái)訪問(wèn)文件數(shù)據(jù)。如果請(qǐng)求的數(shù)據(jù)碰巧在緩存中,內(nèi)核完全被旁路,讀寫(xiě)以內(nèi)存級(jí)速度完成。如果緩存沒(méi)有需要的數(shù)據(jù),則會(huì)觸發(fā)頁(yè)錯(cuò)誤,內(nèi)核將活動(dòng)線程變成休眠狀態(tài),因?yàn)榇藭r(shí)該線程需要去讀取數(shù)據(jù)到內(nèi)存頁(yè)中。當(dāng)數(shù)據(jù)最終可用時(shí),內(nèi)存管理器受程序控制,最新讀取到數(shù)據(jù)可訪問(wèn)時(shí),相應(yīng)線程會(huì)被喚醒。

1.3 Direct IO(DIO)
傳統(tǒng)的read/write和mmap都需要內(nèi)核頁(yè)緩存和內(nèi)核調(diào)度IO。當(dāng)應(yīng)用程序希望自己調(diào)度IO時(shí)(原因稍后解釋),它可以使用direct IO。這需要使用標(biāo)志O_DIRECT來(lái)打開(kāi)文件;進(jìn)一步的工作是使用通用的讀寫(xiě)系統(tǒng)調(diào)用,但它們的行為現(xiàn)在會(huì)有變化;不訪問(wèn)內(nèi)存后,會(huì)該用直接訪問(wèn)磁盤(pán),這意味著調(diào)用線程會(huì)被無(wú)條件的置為休眠狀態(tài)。而且磁盤(pán)控制器會(huì)直接拷貝數(shù)據(jù)到用戶空間,即旁路內(nèi)核。

1.4 異步Direct IO(AIO/DIO)
異步Direct IO相對(duì)于Direct IO有改進(jìn),其行為相似,但不阻塞調(diào)用線程。應(yīng)用程序線程使用io_submit系統(tǒng)調(diào)用調(diào)度direct IO操作,但該線程并不會(huì)被阻塞;IO操作與線程執(zhí)行同時(shí)進(jìn)行。使用獨(dú)立的系統(tǒng)調(diào)用io_getevents來(lái)等待結(jié)果,并在IO操作完成后收集結(jié)果。像DIO一樣,內(nèi)核頁(yè)緩存也被旁路,磁盤(pán)控制器負(fù)責(zé)拷貝數(shù)據(jù)到用戶空間。

二、理解取舍平衡
不同訪問(wèn)方法擁有一些相同的特征,也有一些差異。表1總結(jié)來(lái)這些特征,具體見(jiàn)下表。
| Characteristic | R/W | mmap | DIO | AIO/DIO |
|---|---|---|---|---|
| Cache control | kernel | kernel | user | user |
| Copying | yes | no | no | no |
| MMU activity | low | high | none | none |
| I/O scheduling | kernel | kernel | mixed | user |
| Thread scheduling | kernel | kernel | kernel | user |
| I/O alignment | automatic | automatic | manual | manual |
| Application complexity | low | low | moderate | high |
2.1 緩存控制
對(duì)于read/write和mmap,緩存是內(nèi)核的職責(zé)。大部分系統(tǒng)內(nèi)存被交給頁(yè)緩存。內(nèi)核決定在內(nèi)存不足時(shí)哪個(gè)頁(yè)被淘汰,哪些頁(yè)需要回寫(xiě)至磁盤(pán),哪些需要預(yù)讀。應(yīng)用程序可以使用madvise和fadvise來(lái)為內(nèi)核提供一些指示。
由內(nèi)核控制緩存的最大優(yōu)勢(shì)在于,內(nèi)核開(kāi)發(fā)者們已經(jīng)投入幾十年和巨大精力以優(yōu)化緩存算法。這些算法已經(jīng)被成千上萬(wàn)的不同應(yīng)用程序使用,且整體而已都是很有效的。然而不足是,這些算法是面向通用目標(biāo),沒(méi)有針對(duì)具體應(yīng)用而優(yōu)化。內(nèi)核必須猜測(cè)應(yīng)用程序下一步的動(dòng)作,既是應(yīng)用程序知道完全不同,它也沒(méi)有辦法幫助內(nèi)核猜的更準(zhǔn)確。結(jié)果是頁(yè)被錯(cuò)誤的淘汰,IO以錯(cuò)誤的順序調(diào)度,或者預(yù)讀的數(shù)據(jù)在近期不會(huì)被訪問(wèn)。
2.2 拷貝和MMU活動(dòng)
mmap方式的一個(gè)好處是,如果數(shù)據(jù)在內(nèi)存中,內(nèi)核會(huì)被徹底跳過(guò)。內(nèi)核不需要從內(nèi)核空間拷貝數(shù)據(jù)到用戶空間或反向拷貝,這樣就能減少處理器周期的消耗。這還會(huì)改善負(fù)載,最大化利用緩存(例如,當(dāng)存儲(chǔ)大小比RAM大小接近1:1)。
當(dāng)數(shù)據(jù)不在緩存中,mmap會(huì)表現(xiàn)較差。當(dāng)存儲(chǔ)大小比RAM大小明顯大于1:1時(shí),這種現(xiàn)象尤會(huì)發(fā)生。每個(gè)載入緩存的頁(yè)都會(huì)引起另一頁(yè)的淘汰。這些頁(yè)必須插入頁(yè)表或從中移除,內(nèi)核必須掃描頁(yè)表來(lái)找到非活動(dòng)的頁(yè),并把它們作為待淘汰的候選。另外,mmap需要為頁(yè)表分配內(nèi)存。在x86處理器上,這會(huì)需要0.2%的映射文件大小的內(nèi)存。這看起來(lái)很小,但如果應(yīng)用程序使用的存儲(chǔ)與內(nèi)存的比達(dá)到100:1時(shí),結(jié)果是,20%的內(nèi)存被用來(lái)存儲(chǔ)頁(yè)表(0.2% * 100)。
2.3 IO調(diào)度
內(nèi)核控制緩存(mmap和read/write)的問(wèn)題之一是,應(yīng)用程序失去對(duì)IO調(diào)度的控制。內(nèi)核選擇它任何合適的數(shù)據(jù)塊,調(diào)度對(duì)其的讀寫(xiě)。這會(huì)導(dǎo)致以下的問(wèn)題:
- 寫(xiě)風(fēng)暴:當(dāng)內(nèi)核規(guī)劃大規(guī)模寫(xiě)時(shí),磁盤(pán)會(huì)忙一段時(shí)間,進(jìn)而導(dǎo)致讀延遲。
- 內(nèi)核不能區(qū)分重要和不重要的IO。后臺(tái)IO任務(wù)會(huì)擠垮前臺(tái)任務(wù),導(dǎo)致它們的延遲。
借助繞開(kāi)內(nèi)核頁(yè)緩存,應(yīng)用程序會(huì)承擔(dān)IO調(diào)度的壓力。這并不意味著問(wèn)題被解決,但意味著問(wèn)題可以被解決,只要投入足夠的重視和努力。
使用Direct IO時(shí),每個(gè)線程控制何時(shí)執(zhí)行IO。而內(nèi)核控制線程運(yùn)行,以便內(nèi)核和應(yīng)用程序共同承擔(dān)IO工作。使用AIO/DIO,應(yīng)用程序完成控制何時(shí)執(zhí)行IO。
2.4 線程調(diào)度
IO密集型應(yīng)用程序使用mmap或read/write時(shí)不能猜出其緩存的命中率。因此,必須運(yùn)行大量線程(顯著大于所運(yùn)行的機(jī)器的核數(shù))。使線程過(guò)少時(shí),它們可能都在等待磁盤(pán)運(yùn)動(dòng),處理器利用率會(huì)很低。由于每個(gè)線程都需要等待磁盤(pán)IO,運(yùn)行的線程數(shù)大致為存儲(chǔ)子系統(tǒng)并發(fā)數(shù)乘以一個(gè)小系數(shù),以保持磁盤(pán)能滿負(fù)荷運(yùn)轉(zhuǎn)。如果緩存命中率很高時(shí),這些大數(shù)量的線程彼此之間會(huì)競(jìng)爭(zhēng)有限的CPU核數(shù)。
使用direct IO,這個(gè)問(wèn)題會(huì)得到一定緩和,因?yàn)閼?yīng)用程序知道何時(shí)線程被IO阻塞、何時(shí)能運(yùn)行,所以應(yīng)用程序可以根據(jù)運(yùn)行環(huán)境,調(diào)整運(yùn)行的線程數(shù)。
使用AIO/DIO,應(yīng)用程序完全控制運(yùn)行的線程和等待的IO(二者完全隔離),所以它能輕松調(diào)整內(nèi)存、磁盤(pán)的使用。
2.5 IO對(duì)齊
存儲(chǔ)設(shè)備屬性之一是塊尺寸,所有IO必須以塊大小的整數(shù)倍運(yùn)行,通常是512或4096字節(jié)。使用read/write或mmap時(shí),內(nèi)核自動(dòng)對(duì)齊;小規(guī)模讀寫(xiě)會(huì)被內(nèi)核擴(kuò)展至整個(gè)塊。
使用DIO時(shí),由應(yīng)用程序來(lái)對(duì)齊塊。這帶來(lái)了一定的復(fù)雜度,但也提供了一個(gè)好處:當(dāng)512字節(jié)對(duì)齊就足夠時(shí),內(nèi)核通常需要4096字節(jié)對(duì)齊,但用戶應(yīng)用程序使用DIO就可以用512字節(jié)對(duì)齊的方式讀取,從而節(jié)省小對(duì)象的傳輸帶寬。
2.6 應(yīng)用程序復(fù)雜度
前面討論IO密集型應(yīng)用程序優(yōu)先選擇AIO/DIO,這個(gè)方式伴隨著一個(gè)顯著的成本:復(fù)雜度。為應(yīng)用程序設(shè)置緩存管理職責(zé),意味著它能比內(nèi)核更好的做出選擇,做出這些選擇需要更少的成本。然而,這些算法需要編寫(xiě)和測(cè)試。使用異步IO需要應(yīng)用程序支持回調(diào)方式、協(xié)程、或其他相似的方法,經(jīng)常需要降低很多可用庫(kù)的復(fù)用性。
三、Scylla和AIO/DIO
對(duì)于Scylla,我們選擇更高性能的選項(xiàng),AIO/DIO。為了隔離一些涉及的復(fù)雜度,我們寫(xiě)了Seastar,這是一個(gè)面向IO密集型應(yīng)用的高性能框架。Seastar抽象了執(zhí)行AIO的細(xì)節(jié),為網(wǎng)絡(luò)、磁盤(pán)、多核通訊提供了通用API。它也提供了回調(diào)、協(xié)程風(fēng)格的聲明管理,以適應(yīng)不同的使用用例。
不同領(lǐng)域的Scylla關(guān)注不同IO使用方式:
- 壓縮使用應(yīng)用級(jí)預(yù)讀和后寫(xiě)以提高吞吐量,但繞開(kāi)應(yīng)用級(jí)緩存是緣于設(shè)定其低命中率,同時(shí)避免冷數(shù)據(jù)的沖擊)。
- 查詢使用應(yīng)用控制預(yù)讀和應(yīng)用級(jí)緩存。應(yīng)用控預(yù)讀阻止提前預(yù)讀,是由于我們提前知道數(shù)據(jù)在磁盤(pán)上的邊界。應(yīng)用級(jí)緩存使我們不只可以緩存從磁盤(pán)讀取的數(shù)據(jù),也可以將多個(gè)文件的數(shù)據(jù)合并成一個(gè)緩存項(xiàng)。
- 小規(guī)模讀按512字節(jié)對(duì)齊,以減少總線數(shù)據(jù)傳輸和延遲。
- Seastar IO調(diào)度器允許我們動(dòng)態(tài)控制壓縮和查詢的IO率,以滿足用戶服務(wù)等級(jí)協(xié)議SLA。
- 獨(dú)立的IO調(diào)度類使commitlog獲得需要的帶寬,而不會(huì)被讀搶占。
如果應(yīng)用直接驅(qū)動(dòng)NVMe以繞過(guò)內(nèi)核,那么AIO/DIO將會(huì)是一個(gè)好的起點(diǎn)。這也是未來(lái)Seastar的特性。
四、結(jié)論
我們介紹了在Linux上四種不同類型的磁盤(pán)IO的方法,及之間不同的取舍與平衡。使用傳統(tǒng)read/write很容易上手,使用mmap會(huì)獲得內(nèi)存級(jí)性能,但為了獲取頂級(jí)性能和控制,我們?yōu)镾cylla選擇異步IO。