Go 存儲基礎 - 怎麼使用 direct io ?

Go 存儲編程怎麼使用 O_DIRECT 模式?今天就分享這個存儲細節,

之前提過很多次,操作系統的 IO 過文件系統的時候,**默認是會使用到 page cache,並且採用的是 write back 的方式,系統異步刷盤的。**由於是異步的,如果在數據還未刷盤之前,掉電的話就會導致數據丟失。

如果想要明確數據寫到磁盤有兩種方式:要麼就每次寫完主動 sync 一把,要麼就使用 direct io 的方式,指明每一筆 io 數據都要寫到磁盤才返回。

那麼在 Go 裏面怎麼使用 direct io 呢?

有同學可能會說,那還不簡單,open 文件的時候 flag 用 O_DIRECT 嘛,然後。。。

**是嗎?有這麼簡單嗎?**提兩個問題,童鞋們可以先思考下:

  1. O_DIRECT 這個定義在 Go 標準庫的哪個文件?

  2. direct io 需要 io 大小和偏移扇區對齊,且還要滿足內存 buffer 地址的對齊,這個怎麼做到?

在此之前,先回顧 O_DIRECT 相關的知識。direct io 也就是常說的 DIO,是在 Open 的時候通過 flag 來指定 O_DIRECT 參數,之後的數據的 write/read 都是繞過 page cache,直接和磁盤操作,從而避免了掉電丟數據的尷尬局面,同時也讓應用層可以自己決定內存的使用(避免不必要的 cache 消耗)。

direct io 一般解決兩個問題:

  1. 數據落盤,確保掉電不丟失;

  2. 減少內核 page cache 的內存使用,業務層自己控制內存,更加靈活;

direct io 模式需要用戶保證對齊規則,否則 IO 會報錯,有 3 個需要對齊的規則:

  1. IO 的大小必須扇區大小(512 字節)對齊

  2. IO 偏移按照扇區大小對齊;

  3. 內存 buffer 的地址也必須是扇區對齊;

direct io 模式卻不對齊會怎樣?

讀寫報錯唄,會拋出 “無效參數” 的錯誤。

思考問題

爲什麼 Go 的 O_DIRECT 知識點值得一提?

以下按照兩層意思分析思考。

 1   第一層意思:O_DIRECT  平臺不兼容

劃重點:Go 標準庫 os 中的是沒有 O_DIRECT 這個參數的。

爲什麼呢?

Go os 庫實現的是各個操作系統兼容的實現,direct io 這個在不同的操作系統下實現形態不一樣。其實 O_DIRECT 這個 Open flag 參數本就是隻存在於 linux 系統。

以下才是各個平臺兼容的 Open 參數 (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.
)

發現了嗎?O_DIRECT 根本不在其中。O_DIRECT 其實是和系統平臺強相關的一個參數。

問題來了,那麼 O_DIRECT 定義在那裏?

跟操作系統強相關的自然是定義在 syscall 庫中:

// syscall/zerrors_linux_amd64.go
const (
    // ...
    O_DIRECT         = 0x4000
)

怎麼打開文件呢?

// +build linux
// 指明在 linux 平臺系統編譯
fp := os.OpenFile(name, syscall.O_DIRECT|flag, perm)

 2   第二層意思:Go 無法精確控制內存分配地址

標準庫或者內置函數沒有提供讓你分配對齊內存的函數。

direct io 必須要滿足 3 種對齊規則:io 偏移扇區對齊,長度扇區對齊,內存 buffer 地址扇區對齊。前兩個還比較好滿足,但是分配的內存地址作爲一個小程序員無法精確控制。

先對比回憶下 c 語言,libc 庫是調用 posix_memalign 直接分配出符合要求的內存塊。go 裏面怎麼做?

先問個問題:Go 裏面怎麼分配 buffer 內存?

io 的 buffer 其實就是字節數組嘛,很好回答,最常見自然是用 make 來分配,如下:

buffer := make([]byte, 4096)

那這個地址是對齊的嗎?

答案是:不確定。

那怎麼才能獲取到對齊的地址呢?

劃重點:方法很簡單,就是先分配一個比預期要大的內存塊,然後在這個內存塊裏找對齊位置。 這是一個任何語言皆通用的方法,在 Go 裏也是可用的。

什麼意思?

比如,我現在需要一個 4096 大小的內存塊,要求地址按照 512 對齊,可以這樣做:

  1. 先分配要給 4096 + 512 大小的內存塊,假設得到的地址是 p1 ;

  2. 然後在 [p1, p1+512] 這個地址範圍找,一定能找到 512 對齊的地址(這個能理解嗎?),假設這個地址是 p2 ;

  3. 返回 p2 這個地址給用戶使用,用戶能正常使用 [p2, p2 + 4096] 這個範圍的內存塊而不越界;

以上就是基本原理了,童鞋理解了不?下面看下代碼怎麼寫。

const (
    AlignSize = 512
)

// 在 block 這個字節數組首地址,往後找,找到符合 AlignSize 對齊的地址,並返回
// 這裏用到位操作,速度很快;
func alignment(block []byte, AlignSize int) int {
   return int(uintptr(unsafe.Pointer(&block[0])) & uintptr(AlignSize-1))
}

// 分配 BlockSize 大小的內存塊
// 地址按照 512 對齊
func AlignedBlock(BlockSize int) []byte {
   // 分配一個,分配大小比實際需要的稍大
   block := make([]byte, BlockSize+AlignSize)

   // 計算這個 block 內存塊往後多少偏移,地址才能對齊到 512 
   a := alignment(block, AlignSize)
   offset := 0
   if a != 0 {
      offset = AlignSize - a
   }

   // 偏移指定位置,生成一個新的 block,這個 block 將滿足地址對齊 512;
   block = block[offset : offset+BlockSize]
   if BlockSize != 0 {
      // 最後做一次校驗 
      a = alignment(block, AlignSize)
      if a != 0 {
         log.Fatal("Failed to align block")
      }
   }
   
   return block
}

所以,通過以上 AlignedBlock 函數分配出來的內存一定是 512 地址對齊的。

有啥缺點嗎?

浪費空間嘛。 命名需要 4k 內存,實際分配了 4k+512 。

 3   我太懶了,一行代碼都不願多寫,有開源的庫嗎?

還真有,推薦個:https://github.com/ncw/directio ,內部實現極其簡單,就是上面的一樣。

使用姿勢很簡單:

步驟一:O_DIRECT 模式打開文件:

// 創建句柄
fp, err := directio.OpenFile(file, os.O_RDONLY, 0666)

封裝關鍵在於:O_DIRECT 是從 syscall 庫獲取的。

步驟二:讀數據

// 創建地址按照 4k 對齊的內存塊
buffer := directio.AlignedBlock(directio.BlockSize)
// 把文件數據讀到內存塊中
_, err := io.ReadFull(fp, buffer)

關鍵在於:buffer 必須是特製的 []byte 數組,而不能僅僅根據 make([]byte, 512 ) 這樣去創建,因爲僅僅是 make 無法保證地址對齊。

總結

  1. direct io 必須滿足 io 大小,偏移,內存 buffer 地址三者都扇區對齊;

  2. O_DIRECT 不在 os 庫,而在於操作系統相關的 syscall 庫;

  3. Go 中無法直接使用 make 來分配對齊內存,一般的做法是分配一塊大一點的內存,然後在裏面找到對齊的地址即可;

堅持思考,方向比努力更重要。關注我:奇伢雲存儲

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/gW_3JD52rtRdEqXvyg-lJQ