RocksDB 源碼分析 – I/O


轉載自:https://youjiali1995.github.io/rocksdb/io/ 

這篇文章介紹 RocksDB 中的文件 I/O

文件 I/O

image

page cache

操作系統(文件系統)為了提高文件 I/O 性能,會增加一層 page cache,用於緩存文件數據,大部分讀寫操作只需要訪問 page cache 即可,不需要發起真正的 I/Opage size 可用 sysconf(_SC_PAGESIZE) 獲取,一般為 4KB

第一次讀文件時,會發起真正的 I/O 操作,從 disk (指代各種存儲設備)讀取保存到 page cache 中,還有 read-ahead 機制, 會將后面的部分內容也一並讀取保存在 page cache 中,之后的順序讀只需要返回 cache 中數據即可。

寫操作復雜一些,因為 disk 的最小存儲單位是 sector,而這里以 page size 為單位,所以不滿足 page size boundaries 的寫入要從 disk 中讀出到 page cache 中 再修改,否則 write back 時會覆蓋原有數據:

  • 如果命中 page cache,直接修改 cache 中的數據,標記 pagedirty;否則
  • 如果寫入正好落在 page size boundaries,直接分配 page cache,標記為 dirty;否則
  • 讀入再修改,標記為 dirty

當內存緊缺時,會回收 page cacheclean page 可以直接回收,dirty pagewrite back 后變為 clean 再回收。

write back

write back 即將 dirty page 寫回到 diskwrite() 系統調用對於一般的文件而言(非 O_DIRECT|O_SYNC|O_DSYNC),只是修改 page cache,所以只能保證進程掛掉時數據是完好的。 fdatasync() 會將文件對應的 dirty page 寫回 disk,從而保證機器掛掉時數據是完好的,fsync() 除了 dirty page 外,還會把文件 metadata(inode) 寫回 disk

除了主動 sync 外,操作系統也會幫助 write backLinux 有如下幾個配置,見sysctl/vm.txt

  • dirty_background_ratio:當 dirty pagestotal available memory(free pages + reclaimable pages) 的比例超過該值時,會由后台 flusher 線程開始寫回,默認為 10total available memory 不是 total system memory,因為還有 unreclaimable pagesdirty_background_bytes 類似,以字節為單位,設置其中一個,另一個會置零。
  • dirty_writeback_centisecsflusher 線程定期寫回 dirty pages 的周期,單位為 1/100s,默認為 500
  • dirty_expire_centisecs:當 dirty pages 駐留時間超過該值時,會在下一次 flusher 線程工作時寫回,單位為 1/100s,默認為 3000
  • dirty_ratio:和 dirty_background_ratio 意義相同,但是當超過該值時,發起寫操作的進程要等待 flusher 線程寫回部分 dirty pages 保證 dirty_ratio 滿足要求,相當於變為了同步寫。 所以 dirty_ratio 也就是 dirty pages 的上限。 dirty_bytesdirty_background_bytes 類似。

block layer

block layer 會對 I/O 請求進行合並、排序等調度來提高性能,如電梯算法,然后通過隊列下發任務給各個 storage device, 每個 storage device 各有一個隊列來保存 I/O 任務,互不影響。

疑問:
storage device 一般不支持並行寫,是按順序執行任務,那么 flusher 線程、fsync()/fdatasync() 和其他 I/O 操作是如何協調的?雖說都是提交任務, 但應該有優先級或者 flusher 任務的粒度很細,否則其他操作的延遲可能會很大。至於 Redis 作者發現的 fsync() 阻塞 write() 可能就是順序執行任務導致的, 因為 write() 有可能要發起真正的 I/O

O_DIRECT

使用 O_DIRECTbypass page cache,直接提交到 block layer,節省了從用戶空間到內核空間的拷貝。當程序自己實現了 cache 或者不想污染 page cache 時可以使用 direct I/O

direct I/O 不能保證 synchronized I/O file/data integrity completion,即成功返回時不保證數據已寫到 disk,相當於只提交了任務。使用 fsync()/fdatasync()O_SYNC/O_DSYNC 可以保證 數據已寫到 disk(當然 disk 可能也有 disk cache,但這是操作系統能做到的最大保證了),且不會 bypass page cache, 見 stackoverflow

direct I/O 對文件 offsetbuffer 地址和大小有對齊要求,要按照 the logical block size of the underlying storage (typically 512 bytes) 對齊,可通過 ioctl(2) BLKSSZGET 獲取, 見 man open(2),很好理解,畢竟 disk 的最小存儲單位是 sector。所以 direct I/O 用起來比較麻煩,比如追加寫時數據大小如果不滿足對齊要求 就要 padding 到對齊要求再寫,后面再寫時要把之前那部分重新寫一遍,還要找到對應的 offset

mmap()

mmap() 的行為比較復雜,而且底層實現也在變化,這里主要討論 file-backedMAP_SHARED 這種。mmap() 讓程序以使用內存的形式讀取和修改文件,省去了 read()/write() 的調用,原理是建立了頁表到文件的映射, 且沒有立即讀取文件內容,而是訪問時觸發 page fault 將文件加載到用戶空間。

mmap() 不能映射超過文件大小的區域,所以當文件大小發生變化時,就需要重新映射,如果是追加寫,要先用 ftruncate()/fallocate()/lseek() 等增大文件大小,然后再映射寫。

可能在最初的實現中,mmap()page cache 是不相關的,當觸發 page fault 時直接從磁盤加載到用戶空間,也就可以減少內核到用戶空間的拷貝。 對文件映射的修改只有調用 msync()munmap() 或者內存不夠用發生 swap 時才會寫回文件,所以使用 mmap() 來寫文件時,如果文件大小超過內存可能因 swap 發生很多隨機 I/O, 性能就會很差,而且帶寬用的也不高,這時候需要搭配 msync()madvise() 使用:

  • msync():寫回指定部分的文件映射,MS_ASYNC 是異步寫回,即相當於提交了寫任務,MS_SYNC 是同步寫回,即寫到磁盤才返回。
  • madvise():告訴操作系統對映射操作的建議,MADV_DONTNEED 可以釋放資源,降低內存使用。

使用 mmap() 來讀文件,也需要 madvise(),否則性能也會差:

  • MADV_NORMAL:默認的行為,會發生少量 read-aheadread-behind
  • MADV_SEQUENTIALread-ahead 更多的 pages,而且訪問過的 page 會被很快釋放。
  • MADV_RANDOM:隨機訪問模式,不會進行 read-aheadread-behind

mmap() 也有可見性問題,比如使用 mmap() 修改了共享文件映射,那么使用 read() 能否讀到;使用 write() 修改了文件,mmap() 能否讀到:

  • 不同進程相同映射的修改是立即可見的,可用於 IPC
  • mmap() 修改后要調用 msync(MS_ASYNC||MS_SYNC)read() 才能讀到修改。
  • write() 修改后要調用 msync(MS_INVALIDATE)mmap() 才能讀到修改。

但是在 boltdb 中,是用共享文件 mmap() 作為 page pool,修改是調用 write(),也沒調用 msync(MS_INVALIDATE),這是怎么保證可見性的呢?原因是大多數操作系統(包括 Linux)都采用了 unified virtual memory,或者叫 unified buffer cache,也就是 mmap()page cache 會盡可能共享物理內存頁,當觸發 page fault 時,會先從 page cache 中查找對應的文件 pageread-ahead 等也是保存在 page cache(見 mmap, page cache 與 cgroup)。 所以 mmap()read()/write() 是保持一致的(direct-io 有影響嗎?),也就不再需要同步,msync() 的用途只用於寫回磁盤。這也影響了 mmap() 的寫回,之前只有主動 msync()munmap() 或者 swap 時寫回, 現在因為已經是 dirty page 了,會被操作系統異步寫回,fsync()/fdatasync() 也有效。

Writable File

RocksDB 只會用順序寫,支持 direct I/Ommap(),設置對應的 Options 即可。具體實現就不說了,說幾個細節:

  • mmap() 追加寫的時候,要擴大文件大小再重新映射,RocksDB 用的是 fallocate(),在 close 的時候要記得 ftruncate() 調整大小為真實大小。
  • RocksDB 是讀設備文件獲取 direct I/O 對齊要求的,見 io_posix.cc:GetLogicalBufferSize()
  • WritableFileWriterAlignedBuffer 緩沖寫數據,AlignedBuffer 的起始地址和大小都是對齊的,以便使用 direct I/O
  • 在一些場景下會先 fallocate() 預先分配空間然后再寫,好像可以提高性能並且減少空間浪費,有幾個配置也個這個功能有關,如 fallocate_with_keep_size 分配空間時不會改變文件大小,可用於提高順序寫性能(為啥):

    This allows for pre-allocation of space on devices where it can result in less file fragmentation and/or less waste from over-zealous filesystem pre-allocation.
    After a successful call, subsequent writes into the range specified by offset and len are guaranteed not to fail because of lack of disk space.
    If the FALLOC_FL_KEEP_SIZE flag is specified in mode, the behavior of the call is similar, but the file size will not be changed even if offset+len is greater than the file size. Preallocating zeroed blocks beyond the end of the file in this manner is useful for optimizing append workloads.

  • 會用 sync_file_range() 定期刷盤,因為如果只靠操作系統異步刷盤的話,在 I/O 很重的情況下可能導致阻塞,原因見上面的 write back。但是最新的 1MB 不會刷,為了避免后面再追加寫時重復寫入。

Readable File

RocksDB 中讀文件有順序讀和隨機讀,順序讀不會用 mmap(),具體實現也沒什么好說的。需要注意的是 readahead,操作系統默認會做 readahead,能夠提高順序讀的性能,但是對隨機讀毫無作用,可能還會降低性能, 因為可能會把其他有用的 pagepage cache 中擠出去,可以用 posix_fadvise(POSIX_FADV_RANDOM) 禁用 readaheadRocksDB 會對隨機訪問的文件自己做 readahead,見 ReadaheadRandomAccessFileFilePrefetchBufferposix_fadvise(POSIX_FADV_DONTNEED) 用於釋放 page cache,也可以避免有用的 page 被擠出去。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM