隨機讀寫之DirectIO

在上一節中講過MappedByteBuffer VS FileChannel它們稱得上零拷貝技術,但留下了順序讀比隨機讀快,順序寫比隨機寫快的問題,在我們的實際應用場景中爲了迴避隨機讀寫需求,通常的做法都是對其進行文件分片(又將隨機變成了有序),而本節藉助Direct IO來徹底解決該問題。

注:只有 Linux 系統才支持 Direct IO,還被 Linus 噴過,據說在 Jdk10 發佈之後將會得到原生支持

buffered io

普通文件操作,對性能、吞吐量沒有特殊要求,由kernel通過page cache統一管理緩存,page cache的創建和回收由kernel控制。

默認是異步寫,如果使用sync,則是同步寫,保證該文件所有的髒頁落盤後才返回(對於db transaction很重要,通過sync保證redo log落盤,從而保證一致性)

mmap 對文件操作的吞吐量、性能有一定要求,且對內存使用不敏感,不要求實時落盤的情況下使用。mmap對比buffered io,可以減少一次從page cache -> user space的內存拷貝,大文件場景下能大幅度提升性能。同時mmap將把文件操作轉換爲內存操作,避免lseek。通過msync回寫硬盤(此處只說IO相關的應用,拋開進程內存共享等應用)。
direct io

性能要求較高、內存使用率也要求較高的情況下使用。

適合DB場景,DB會將數據緩存在自己的cache中,換入、換出算法由DB控制,因爲DB比kernel更瞭解哪些數據應該換入換出,比如innodb的索引,要求常駐內存,對於redo log不需要重讀讀寫,更不要page cache,直接寫入磁盤就好。

順序讀寫 & 隨機讀寫

定義不難理解,其實就是要有序,不要隨機切換位置,比如:

thread1:write position[0~4096)
thread2:write position[4096~8194)
而非
thread1:write position[0~4096)
thread3:write position[8194~12288)
thread2:write position[4096~8194)

ExecutorService executor = Executors.newFixedThreadPool(64);
AtomicLong position = new AtomicLong(0);
//併發寫(達到隨機寫的描述)
for(int i=0;i<1024;i++){
    final int index = i;
    executor.execute(()->{
        fileChannel.write(ByteBuffer.wrap(new byte[4*1024]),position.getAndAdd(4*1024));
    })
}
//加鎖保證了順序寫
for(int i=0;i<1024;i++){
    final int index = i;
    executor.execute(()->{
        write(new byte[4*1024]);
    })
}
public synchronized void write(byte[] data){
    fileChannel.write(ByteBuffer.wrap(new byte[4*1024]),position.getAndAdd(4*1024));
}

思考:順序寫爲什麼比隨機寫要快?

這正是page cache在起作用!

disk cache分爲兩種:buffer cache以塊爲單位,緩存裸分區中的內容,(如super block、inode)。page cache是以頁爲單位緩存分區中文件的內容(通常爲4K) ,它是位於 application buffer(用戶內存)和 disk file(磁盤)之間的一層緩存。linux2.4中,兩者是並存的,但會造成mmap情況下,同一份數據會在兩個cache空間內存在,造成空間浪費,在linux2.6中,兩者合併,buffer cache也使用page cache的數據結構,只是在free統計內存時,會把page cache中緩存裸分區的部分統計爲buffer cache。

當用戶發起一個 fileChannel.read(4kb) 時:

  1. 操作系統從磁盤加載了磁盤塊(block) 16kb進入 PageCache,這被稱爲預讀(爲了減少實際磁盤IO)
  2. 應用程序(application buffer)操作(read)其實是從 PageCache 拷貝 4kb 進入用戶內存

關於扇區、磁盤塊、Page(PageCache)在N年前談過要有一定的瞭解!

常見的block大小爲512Bytes,1KB,4KB,查看本機 blockSize 大小的方式,通常爲 4kb

爲何加載block爲16kb,該值也很講究blockSize*4,難道也發生了4次IO?非也,這又涉及到IO合併(readahead算法)!另外該值並非固定值值,內核的預讀算法則會以它認爲更合適的大小進行預讀 I/O,比如16-128KB,當然可以手動進行調整。對這塊敢興趣的同學可以看文末的引用資料

當用戶繼續訪問接下來的[4kb,16kb]的磁盤內容時,便是直接從 PageCache 去訪問了!當我看到下圖也有些不適應,確實我們離開發操作系統的老外(老碼農)還遠着呢,只能大致領略的它味道,這裏致敬那些從事計算機基礎技術的前輩,更期待華爲的操作系統鴻蒙出世。

內存吃緊時PageCache 會受影響嗎?

肯定是會影響的,PageCache 是動態調整的,可以通過 linux 的系統參數進行調整,默認是佔據總內存的 20%。可以通過free命令進行監控(cache:page cache和slab所佔用的內存之和,buff/cache:buffers + cache),它一個內核配置接口 /proc/sys/vm/drop_caches 可以允許用戶手動清理cache來達到釋放內存的作用,這個文件有三個值:1、2、3

Writing to this will cause the kernel to drop clean caches, dentries and inodes from memory, causing that memory to become free.

- To free pagecache:

- * echo 1 > /proc/sys/vm/drop_caches

- To free dentries and inodes:

- * echo 2 > /proc/sys/vm/drop_caches

- To free pagecache, dentries and inodes:

- * echo 3 > /proc/sys/vm/drop_caches

- As this is a non-destructive operation, and dirty objects are notfreeable, the user should run "sync" first in order to make sure allcached objects are freed.

- This tunable was added in 2.6.16.

毛刺現象

現代操作系統會使用儘可能多的空閒內存來充當 PageCache,當操作系統回收 PageCache 內存的速度低於應用寫緩存的速度時,會影響磁盤寫入的速率,直接表現爲寫入 RT 增大,這被稱之爲“毛刺現象”

Direct IO

去掉PageCache的確可以實現高效的隨機讀,的確也有存在的價值!採用 Direct IO + 自定義內存管理機制會使得產品更加的可控,高性能。

如何使用呢?

使用 Direct IO 最終需要調用到 c 語言的 pwrite 接口,並設置 O_DIRECT flag,使用 O_DIRECT 存在不少限制:

  • 操作系統限制:Linux 操作系統在 2.4.10 及以後的版本中支持 O_DIRECT flag,老版本會忽略該 Flag;Mac OS 也有類似於 O_DIRECT 的機制
  • 用於傳遞數據的緩衝區,其內存邊界必須對齊爲 blockSize 的整數倍
  • 用於傳遞數據的緩衝區,其傳遞數據的大小必須是 blockSize 的整數倍
  • 數據傳輸的開始點,即文件和設備的偏移量,必須是 blockSize 的整數倍

 

目前Java 目前原生並不支持,但github已經有封裝好了 Java 的 JNA 庫(smacke/jaydio),實現了 Java 的 Direct IO。

它自己搞了一套 Buffer 接口跟 JDK 的類庫不兼容,且讀寫實現裏面加了一塊 Buffer 用於緩存內容至 Block 對齊有點破壞 Direct IO 的語義。

int bufferSize = 1<<23; // Use 8 MiB buffers
byte[] buf = new byte[bufferSize];

DirectRandomAccessFile fin = new DirectRandomAccessFile(new File("hello.txt"), "r", bufferSize);

DirectRandomAccessFile fout = new DirectRandomAccessFile(new File("world.txt"), "rw", bufferSize);

while (fin.getFilePointer() < fin.length()) {
    int remaining = (int)Math.min(bufferSize, fin.length()-fin.getFilePointer());
    fin.read(buf,0,remaining);
    fout.write(buf,0,remaining);
}

fin.close();
fout.close();

當然也有人對它進行了再度封裝,可參閱J-DirectIO

引用資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章