Go存儲(chǔ)基礎(chǔ)使用direct io方法實(shí)例
Go 存儲(chǔ)編程怎么使用 O_DIRECT 模式?
今天分享一個(gè)存儲(chǔ)細(xì)節(jié),Go 存儲(chǔ)編程怎么使用 O_DIRECT 模式?
之前提過(guò)很多次,操作系統(tǒng)的 IO 過(guò)文件系統(tǒng)的時(shí)候,默認(rèn)是會(huì)使用到 page cache,并且采用的是 write back 的方式,系統(tǒng)異步刷盤的。由于是異步的,如果在數(shù)據(jù)還未刷盤之前,掉電的話就會(huì)導(dǎo)致數(shù)據(jù)丟失。
如果想要明確數(shù)據(jù)寫到磁盤有兩種方式:要么就每次寫完主動(dòng) sync 一把,要么就使用 direct io 的方式,指明每一筆 io 數(shù)據(jù)都要寫到磁盤才返回。
那么在 Go 里面怎么使用 direct io 呢?
有同學(xué)可能會(huì)說(shuō),那還不簡(jiǎn)單,open 文件的時(shí)候 flag 用 O_DIRECT 嘛,然后。。。
是嗎?有這么簡(jiǎn)單嗎?
提兩個(gè)問(wèn)題,童鞋們可以先思考下:
- O_DIRECT 這個(gè)定義在 Go 標(biāo)準(zhǔn)庫(kù)的哪個(gè)文件?
- direct io 需要 io 大小和偏移扇區(qū)對(duì)齊,且還要滿足內(nèi)存 buffer 地址的對(duì)齊,這個(gè)怎么做到?
O_DIRECT 的知識(shí)點(diǎn)
在此之前,先回顧 O_DIRECT 相關(guān)的知識(shí)。direct io 也就是常說(shuō)的 DIO,是在 Open 的時(shí)候通過(guò) flag 來(lái)指定 O_DIRECT 參數(shù),之后的數(shù)據(jù)的 write/read 都是繞過(guò) page cache,直接和磁盤操作,從而避免了掉電丟數(shù)據(jù)的尷尬局面,同時(shí)也讓應(yīng)用層可以自己決定內(nèi)存的使用(避免不必要的 cache 消耗)。
direct io 一般解決兩個(gè)問(wèn)題:
- 數(shù)據(jù)落盤,確保掉電不丟失;
- 減少內(nèi)核 page cache 的內(nèi)存使用,業(yè)務(wù)層自己控制內(nèi)存,更加靈活;
direct io 模式需要用戶保證對(duì)齊規(guī)則,否則 IO 會(huì)報(bào)錯(cuò),有 3 個(gè)需要對(duì)齊的規(guī)則:
- IO 的大小必須扇區(qū)大?。?12字節(jié))對(duì)齊
- IO 偏移按照扇區(qū)大小對(duì)齊;
- 內(nèi)存 buffer 的地址也必須是扇區(qū)對(duì)齊;
思考標(biāo)題
為什么 Go 的 O_DIRECT 知識(shí)點(diǎn)值得一提?
以下按照兩層意思分析思考。
第一層意思:O_DIRECT 平臺(tái)不兼容
劃重點(diǎn):Go 標(biāo)準(zhǔn)庫(kù) os 中的是沒(méi)有 O_DIRECT 這個(gè)參數(shù)的。
為什么呢?
Go os 庫(kù)實(shí)現(xiàn)的是各個(gè)操作系統(tǒng)兼容的實(shí)現(xiàn),direct io 這個(gè)在不同的操作系統(tǒng)下實(shí)現(xiàn)形態(tài)不一樣。其實(shí) O_DIRECT 這個(gè) Open flag 參數(shù)本就是只存在于 linux 系統(tǒng)。
以下才是各個(gè)平臺(tái)兼容的 Open 參數(shù) ( os/file.go )。
const ( // Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified. O_RDONLY int = syscall.O_RDONLY // open the file read-only. O_WRONLY int = syscall.O_WRONLY // open the file write-only. O_RDWR int = syscall.O_RDWR // open the file read-write. // The remaining values may be or'ed in to control behavior. O_APPEND int = syscall.O_APPEND // append data to the file when writing. O_CREATE int = syscall.O_CREAT // create a new file if none exists. O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist. O_SYNC int = syscall.O_SYNC // open for synchronous I/O. O_TRUNC int = syscall.O_TRUNC // truncate regular writable file when opened. )
發(fā)現(xiàn)了嗎?O_DIRECT 根本不在其中。O_DIRECT 其實(shí)是和系統(tǒng)平臺(tái)強(qiáng)相關(guān)的一個(gè)參數(shù)。
問(wèn)題來(lái)了,那么 O_DIRECT 定義在那里?
跟操作系統(tǒng)強(qiáng)相關(guān)的自然是定義在 syscall 庫(kù)中:
// syscall/zerrors_linux_amd64.go
const (
// ...
O_DIRECT = 0x4000
)
怎么打開(kāi)文件呢?
// +build linux // 指明在 linux 平臺(tái)系統(tǒng)編譯 fp := os.OpenFile(name, syscall.O_DIRECT|flag, perm)
第二層意思:Go 無(wú)法精確控制內(nèi)存分配地址
標(biāo)準(zhǔn)庫(kù)或者內(nèi)置函數(shù)沒(méi)有提供讓你分配對(duì)齊內(nèi)存的函數(shù)。
direct io 必須要滿足 3 種對(duì)齊規(guī)則:io 偏移扇區(qū)對(duì)齊,長(zhǎng)度扇區(qū)對(duì)齊,內(nèi)存 buffer 地址扇區(qū)對(duì)齊。前兩個(gè)還比較好滿足,但是分配的內(nèi)存地址作為一個(gè)小程序員無(wú)法精確控制。
先對(duì)比回憶下 c 語(yǔ)言,libc 庫(kù)是調(diào)用 posix_memalign 直接分配出符合要求的內(nèi)存塊。go 里面怎么做?
先問(wèn)個(gè)問(wèn)題:Go 里面怎么分配 buffer 內(nèi)存?
io 的 buffer 其實(shí)就是字節(jié)數(shù)組嘛,很好回答,最常見(jiàn)自然是用 make 來(lái)分配,如下:
buffer := make([]byte, 4096)
那這個(gè)地址是對(duì)齊的嗎?
答案是:不確定。
那怎么才能獲取到對(duì)齊的地址呢?
劃重點(diǎn):方法很簡(jiǎn)單,就是先分配一個(gè)比預(yù)期要大的內(nèi)存塊,然后在這個(gè)內(nèi)存塊里找對(duì)齊位置。 這是一個(gè)任何語(yǔ)言皆通用的方法,在 Go 里也是可用的。
什么意思?
比如,我現(xiàn)在需要一個(gè) 4096 大小的內(nèi)存塊,要求地址按照 512 對(duì)齊,可以這樣做:
- 先分配要給 4096 + 512 大小的內(nèi)存塊,假設(shè)得到的地址是 p1 ;
- 然后在 [ p1, p1+512 ] 這個(gè)地址范圍找,一定能找到 512 對(duì)齊的地址(這個(gè)能理解嗎?),假設(shè)這個(gè)地址是 p2 ;
- 返回 p2 這個(gè)地址給用戶使用,用戶能正常使用 [ p2, p2 + 4096 ] 這個(gè)范圍的內(nèi)存塊而不越界;
以上就是基本原理了,童鞋理解了不?下面看下代碼怎么寫。
const (
AlignSize = 512
)
// 在 block 這個(gè)字節(jié)數(shù)組首地址,往后找,找到符合 AlignSize 對(duì)齊的地址,并返回
// 這里用到位操作,速度很快;
func alignment(block []byte, AlignSize int) int {
return int(uintptr(unsafe.Pointer(&block[0])) & uintptr(AlignSize-1))
}
// 分配 BlockSize 大小的內(nèi)存塊
// 地址按照 512 對(duì)齊
func AlignedBlock(BlockSize int) []byte {
// 分配一個(gè),分配大小比實(shí)際需要的稍大
block := make([]byte, BlockSize+AlignSize)
// 計(jì)算這個(gè) block 內(nèi)存塊往后多少偏移,地址才能對(duì)齊到 512
a := alignment(block, AlignSize)
offset := 0
if a != 0 {
offset = AlignSize - a
}
// 偏移指定位置,生成一個(gè)新的 block,這個(gè) block 將滿足地址對(duì)齊 512;
block = block[offset : offset+BlockSize]
if BlockSize != 0 {
// 最后做一次校驗(yàn)
a = alignment(block, AlignSize)
if a != 0 {
log.Fatal("Failed to align block")
}
}
return block
}所以,通過(guò)以上 AlignedBlock 函數(shù)分配出來(lái)的內(nèi)存一定是 512 地址對(duì)齊的。
有啥缺點(diǎn)嗎?
浪費(fèi)空間嘛。 命名需要 4k 內(nèi)存,實(shí)際分配了 4k+512 。
我太懶了,一行代碼都不愿多寫,有開(kāi)源的庫(kù)嗎?
還真有,推薦個(gè):https://github.com/ncw/directio ,內(nèi)部實(shí)現(xiàn)極其簡(jiǎn)單,就是上面的一樣。
使用姿勢(shì)很簡(jiǎn)單:
步驟一:O_DIRECT 模式打開(kāi)文件:
// 創(chuàng)建句柄 fp, err := directio.OpenFile(file, os.O_RDONLY, 0666)
封裝關(guān)鍵在于:O_DIRECT 是從 syscall 庫(kù)獲取的。
步驟二:讀數(shù)據(jù)
// 創(chuàng)建地址按照 4k 對(duì)齊的內(nèi)存塊 buffer := directio.AlignedBlock(directio.BlockSize) // 把文件數(shù)據(jù)讀到內(nèi)存塊中 _, err := io.ReadFull(fp, buffer)
關(guān)鍵在于:buffer 必須是特制的 [ ]byte 數(shù)組,而不能僅僅根據(jù) make([ ]byte, 512 ) 這樣去創(chuàng)建,因?yàn)閮H僅是 make 無(wú)法保證地址對(duì)齊。
總結(jié)
- direct io 必須滿足 io 大小,偏移,內(nèi)存 buffer 地址三者都扇區(qū)對(duì)齊;
- O_DIRECT 不在 os 庫(kù),而在于操作系統(tǒng)相關(guān)的 syscall 庫(kù);
- Go 中無(wú)法直接使用 make 來(lái)分配對(duì)齊內(nèi)存,一般的做法是分配一塊大一點(diǎn)的內(nèi)存,然后在里面找到對(duì)齊的地址即可;
以上就是Go存儲(chǔ)基礎(chǔ)使用direct io方法實(shí)例的詳細(xì)內(nèi)容,更多關(guān)于Go存儲(chǔ)direct io 的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang控制結(jié)構(gòu)select機(jī)制及使用示例詳解
這篇文章主要介紹了golang控制結(jié)構(gòu)select機(jī)制及使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
Go語(yǔ)言面向?qū)ο笾械亩鄳B(tài)你學(xué)會(huì)了嗎
面向?qū)ο笾械亩鄳B(tài)(Polymorphism)是指一個(gè)對(duì)象可以具有多種不同的形態(tài)或表現(xiàn)方式,本文將通過(guò)一些簡(jiǎn)單的示例為大家講解一下多態(tài)的實(shí)現(xiàn),需要的可以參考下2023-07-07
基于Go語(yǔ)言實(shí)現(xiàn)高性能文件上傳下載系統(tǒng)
在Web應(yīng)用開(kāi)發(fā)中,文件上傳下載是一個(gè)非常常見(jiàn)的需求,本文將介紹如何使用Go語(yǔ)言實(shí)現(xiàn)一個(gè)安全、高效的本地文件存儲(chǔ)系統(tǒng),感興趣的小伙伴可以了解下2025-03-03
Go 通過(guò)結(jié)構(gòu)struct實(shí)現(xiàn)接口interface的問(wèn)題
這篇文章主要介紹了Go 通過(guò)結(jié)構(gòu)struct實(shí)現(xiàn)接口interface的問(wèn)題,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-10-10
golang原生http包實(shí)現(xiàn)各種情況的get請(qǐng)求方式
這篇文章主要介紹了golang原生http包實(shí)現(xiàn)各種情況的get請(qǐng)求方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08

