溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

文件IO操作的方法是什么

發(fā)布時間:2022-01-04 17:24:41 來源:億速云 閱讀:149 作者:iii 欄目:服務(wù)器

本篇內(nèi)容介紹了“文件IO操作的方法是什么”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!

01

/背景

已經(jīng)過去的中間件性能挑戰(zhàn)賽,和正在進行中的 第一屆 PolarDB 數(shù)據(jù)性能大賽 都涉及到了文件操作,合理地設(shè)計架構(gòu)以及正確地壓榨機器的讀寫性能成了比賽中獲取較好成績的關(guān)鍵。正在參賽的我收到了幾位公眾號讀者朋友的反饋,他們大多表達出了這樣的煩惱:“對比賽很感興趣,但不知道怎么入門”,“能跑出成績,但相比前排的選手,成績相差10倍有余”…為了能讓更多的讀者參與到之后相類似的比賽中來,我簡單整理一些文件IO操作的最佳實踐,而不涉及整體系統(tǒng)的架構(gòu)設(shè)計,希望通過這篇文章的介紹,讓你能夠歡快地參與到之后類似的性能挑戰(zhàn)賽之中來。

02

/知識點梳理

本文主要關(guān)注的 Java 相關(guān)的文件操作,理解它們需要一些前置條件,比如 PageCache,Mmap(內(nèi)存映射),DirectByteBuffer(堆外緩存),順序讀寫,隨機讀寫...不一定需要完全理解,但至少知道它們是個啥,因為本文將會主要圍繞這些知識點來展開描述。

03

/初識 FileChannel 和 MMAP

首先,文件IO類型的比賽最重要的一點,就是選擇好讀寫文件的方式,那 JAVA 中文件IO有多少種呢?原生的讀寫方式大概可以被分為三種:普通IO,F(xiàn)ileChannel(文件通道),MMAP(內(nèi)存映射)。區(qū)分他們也很簡單,例如 FileWriter,FileReader 存在于 java.io 包中,他們屬于普通IO;FileChannel 存在于 java.nio 包中,屬于 NIO 的一種,但是注意 NIO 并不一定意味著非阻塞,這里的 FileChannel 就是阻塞的;較為特殊的是后者 MMAP,它是由 FileChannel 調(diào)用 map 方法衍生出來的一種特殊讀寫文件的方式,被稱之為內(nèi)存映射。

使用 FIleChannel 的方式:

FileChannel fileChannel = new RandomAccessFile(new File("db.data"), "rw").getChannel();

獲取 MMAP 的方式:

MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, filechannel.size();

MappedByteBuffer 便是 JAVA 中 MMAP 的操作類。

面向于字節(jié)傳輸?shù)膫鹘y(tǒng) IO 方式遭到了我們的唾棄,我們重點探討 FileChannel 和 MMAP 這兩種讀寫方式的區(qū)別。

04

/FileChannel 讀寫

// 寫byte[] data = new byte[4096];long position = 1024L;//指定 position 寫入 4kb 的數(shù)據(jù)fileChannel.write(ByteBuffer.wrap(data), position);//從當(dāng)前文件指針的位置寫入 4kb 的數(shù)據(jù)fileChannel.write(ByteBuffer.wrap(data));// 讀ByteBuffer buffer = ByteBuffer.allocate(4096);long position = 1024L;//指定 position 讀取 4kb 的數(shù)據(jù)fileChannel.read(buffer,position);//從當(dāng)前文件指針的位置讀取 4kb 的數(shù)據(jù)fileChannel.read(buffer);

FileChannel 大多數(shù)時候是和 ByteBuffer 這個類打交道,你可以將它理解為一個 byte[] 的封裝類,提供了豐富的 API 去操作字節(jié),不了解的同學(xué)可以去熟悉下它的 API。值得一提的是,write 和 read 方法均是線程安全的,F(xiàn)ileChannel 內(nèi)部通過一把 privatefinalObjectpositionLock=newObject(); 鎖來控制并發(fā)。

FileChannel 為什么比普通 IO 要快呢?這么說可能不嚴謹,因為你要用對它,F(xiàn)ileChannel 只有在一次寫入 4kb 的整數(shù)倍時,才能發(fā)揮出實際的性能,這得益于 FileChannel 采用了 ByteBuffer 這樣的內(nèi)存緩沖區(qū),讓我們可以非常精準的控制寫盤的大小,這是普通 IO 無法實現(xiàn)的。4kb 一定快嗎?也不嚴謹,這主要取決你機器的磁盤結(jié)構(gòu),并且受到操作系統(tǒng),文件系統(tǒng),CPU 的影響,例如中間件性能挑戰(zhàn)賽時的那塊盤,一次至少寫入 64kb 才能發(fā)揮出最高的 IOPS。

文件IO操作的方法是什么

然而 PolarDB 這塊盤就完全不一樣了,可謂是異常彪悍,具體是如何的表現(xiàn)由于比賽仍在進行中,不予深究,但憑借著 benchmark everyting 的技巧,我們完全可以測出來。

另外一點,成就了 FileChannel 的高效,介紹這點之前,我想做一個提問:FileChannel 是直接把 ByteBuffer 中的數(shù)據(jù)寫入到磁盤嗎?思考幾秒…答案是:NO。ByteBuffer 中的數(shù)據(jù)和磁盤中的數(shù)據(jù)還隔了一層,這一層便是 PageCache,是用戶內(nèi)存和磁盤之間的一層緩存。我們都知道磁盤 IO 和內(nèi)存 IO 的速度可是相差了好幾個數(shù)量級。我們可以認為 filechannel.write 寫入 PageCache 便是完成了落盤操作,但實際上,操作系統(tǒng)最終幫我們完成了 PageCache 到磁盤的最終寫入,理解了這個概念,你就應(yīng)該能夠理解 FileChannel 為什么提供了一個 force() 方法,用于通知操作系統(tǒng)進行及時的刷盤。

同理,當(dāng)我們使用 FileChannel 進行讀操作時,同樣經(jīng)歷了:磁盤->PageCache->用戶內(nèi)存這三個階段,對于日常使用者而言,你可以忽略掉 PageCache,但作為挑戰(zhàn)者參賽,PageCache 在調(diào)優(yōu)過程中是萬萬不能忽視的,關(guān)于讀操作這里不做過多的介紹,我們在下面的小結(jié)中還會再次提及,這里當(dāng)做是引出 PageCache 的概念。

05

/MMAP 讀寫

// 寫byte[] data = new byte[4];int position = 8;//從當(dāng)前 mmap 指針的位置寫入 4b 的數(shù)據(jù)mappedByteBuffer.put(data);//指定 position 寫入 4b 的數(shù)據(jù)MappedByteBuffer subBuffer = mappedByteBuffer.slice();subBuffer.position(position);subBuffer.put(data);// 讀byte[] data = new byte[4];int position = 8;//從當(dāng)前 mmap 指針的位置讀取 4b 的數(shù)據(jù)mappedByteBuffer.get(data);//指定 position 讀取 4b 的數(shù)據(jù)MappedByteBuffer subBuffer = mappedByteBuffer.slice();subBuffer.position(position);subBuffer.get(data);

FileChannel 已經(jīng)足夠強大了,MappedByteBuffer 還能玩出什么花來呢?請容許我賣個關(guān)子先,先介紹一下 MappedByteBuffer 的使用注意點。

當(dāng)我們執(zhí)行 fileChannel.map(FileChannel.MapMode.READ_WRITE,0,1.5*1024*1024*1024); 之后,觀察一下磁盤上的變化,會立刻獲得一個 1.5G 的文件,但此時文件的內(nèi)容全部是 0(字節(jié) 0)。這符合 MMAP 的中文描述:內(nèi)存映射文件,我們之后對內(nèi)存中 MappedByteBuffer 做的任何操作,都會被最終映射到文件之中,

mmap 把文件映射到用戶空間里的虛擬內(nèi)存,省去了從內(nèi)核緩沖區(qū)復(fù)制到用戶空間的過程,文件中的位置在虛擬內(nèi)存中有了對應(yīng)的地址,可以像操作內(nèi)存一樣操作這個文件,相當(dāng)于已經(jīng)把整個文件放入內(nèi)存,但在真正使用到這些數(shù)據(jù)前卻不會消耗物理內(nèi)存,也不會有讀寫磁盤的操作,只有真正使用這些數(shù)據(jù)時,也就是圖像準備渲染在屏幕上時,虛擬內(nèi)存管理系統(tǒng) VMS 才根據(jù)缺頁加載的機制從磁盤加載對應(yīng)的數(shù)據(jù)塊到物理內(nèi)存進行渲染。這樣的文件讀寫文件方式少了數(shù)據(jù)從內(nèi)核緩存到用戶空間的拷貝,效率很高

看了稍微官方一點的描述,你可能對 MMAP 有了些許的好奇,有這么厲害的黑科技存在的話,還有 FileChannel 存在的意義嗎!并且網(wǎng)上很多文章都在說,MMAP 操作大文件性能比 FileChannel 搞出一個數(shù)量級!然而,通過我比賽的認識,MMAP 并非是文件 IO 的銀彈,它只有在一次寫入很小量數(shù)據(jù)的場景下才能表現(xiàn)出比 FileChannel 稍微優(yōu)異的性能。緊接著我還要告訴你一些令你沮喪的事,至少在 JAVA 中使用 MappedByteBuffer 是一件非常麻煩并且痛苦的事,主要表現(xiàn)為三點:

  1. MMAP 使用時必須實現(xiàn)指定好內(nèi)存映射的大小,并且一次 map 的大小限制在 1.5G 左右,重復(fù) map 又會帶來虛擬內(nèi)存的回收、重新分配的問題,對于文件不確定大小的情形實在是太不友好了。

  2. MMAP 使用的是虛擬內(nèi)存,和 PageCache 一樣是由操作系統(tǒng)來控制刷盤的,雖然可以通過 force() 來手動控制,但這個時間把握不好,在小內(nèi)存場景下會很令人頭疼。

  3. MMAP 的回收問題,當(dāng) MappedByteBuffer 不再需要時,可以手動釋放占用的虛擬內(nèi)存,但…方式非常的詭異。

public static void clean(MappedByteBuffer mappedByteBuffer) {    ByteBuffer buffer = mappedByteBuffer;    if (buffer == null || !buffer.isDirect() || buffer.capacity() == 0)        return;    invoke(invoke(viewed(buffer), "cleaner"), "clean");}private static Object invoke(final Object target, final String methodName, final Class<?>... args) {    return AccessController.doPrivileged(new PrivilegedAction<Object>() {        public Object run() {            try {                Method method = method(target, methodName, args);                method.setAccessible(true);                return method.invoke(target);            } catch (Exception e) {                throw new IllegalStateException(e);            }        }    });}private static Method method(Object target, String methodName, Class<?>[] args)        throws NoSuchMethodException {    try {        return target.getClass().getMethod(methodName, args);    } catch (NoSuchMethodException e) {        return target.getClass().getDeclaredMethod(methodName, args);    }}private static ByteBuffer viewed(ByteBuffer buffer) {    String methodName = "viewedBuffer";    Method[] methods = buffer.getClass().getMethods();    for (int i = 0; i < methods.length; i++) {        if (methods[i].getName().equals("attachment")) {            methodName = "attachment";            break;        }    }    ByteBuffer viewedBuffer = (ByteBuffer) invoke(buffer, methodName);    if (viewedBuffer == null)        return buffer;    else        return viewed(viewedBuffer);}

對的,你沒看錯,這么長的代碼僅僅是為了干回收 MappedByteBuffer 這一件事。

所以我建議,優(yōu)先使用 FileChannel 去完成初始代碼的提交,在必須使用小數(shù)據(jù)量(例如幾個字節(jié))刷盤的場景下,再換成 MMAP 的實現(xiàn),其他場景 FileChannel 完全可以 cover(前提是你理解怎么合理使用 FileChannel)。至于 MMAP 為什么在一次寫入少量數(shù)據(jù)的場景下表現(xiàn)的比 FileChannel 優(yōu)異,我還沒有查到理論根據(jù),如果你有相關(guān)的線索,歡迎留言。理論分析下,F(xiàn)ileChannel 同樣是寫入內(nèi)存,但比 MMAP 多了一次內(nèi)核緩沖區(qū)與用戶空間互相復(fù)制的過程,所以在極端場景下,MMAP 表現(xiàn)的更加優(yōu)秀。至于 MMAP 分配的虛擬內(nèi)存是否就是真正的 PageCache 這一點,我覺得可以近似理解成 PageCache。

06

/順序讀比隨機讀快,順序?qū)懕入S機寫快

無論你是機械硬盤還是 SSD,這個結(jié)論都是一定成立的,雖然背后的原因不太一樣,我們今天不討論機械硬盤這種古老的存儲介質(zhì),重點 foucs 在 SSD 上,來看看在它之上進行的隨機讀寫為什么比順序讀寫要慢。即使各個 SSD 和文件系統(tǒng)的構(gòu)成具有差異性,但我們今天的分析同樣具備參考價值。

首先,什么是順序讀,什么是隨機讀,什么是順序?qū)?,什么是隨機寫?可能我們剛接觸文件 IO 操作時并不會有這樣的疑惑,但寫著寫著,自己都開始懷疑自己的理解了,不知道你有沒有經(jīng)歷過這樣類似的階段,反正我有一段時間的確懷疑過。那么,先來看看兩段代碼:

寫入方式一:64個線程,用戶自己使用一個 atomic 變量記錄寫入指針的位置,并發(fā)寫入

ExecutorService executor = Executors.newFixedThreadPool(64);AtomicLong wrotePosition = new AtomicLong(0);for(int i=0;i<1024;i++){    final int index = i;    executor.execute(()->{        fileChannel.write(ByteBuffer.wrap(new byte[4*1024]),wrote.getAndAdd(4*1024));    })}

寫入方式二:給 write 加了鎖,保證了同步。

ExecutorService executor = Executors.newFixedThreadPool(64);AtomicLong wrotePosition = new AtomicLong(0);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]),wrote.getAndAdd(4*1024));}

答案是方式二才算順序?qū)?,順序讀也是同理。對于文件操作,加鎖并不是一件非常可怕的事,不敢同步 write/read 才可怕!有人會問:FileChannel 內(nèi)部不是已經(jīng)有 positionLock 保證寫入的線程安全了嗎,為什么還要自己加同步?為什么這樣會快?我用大白話來回答的話就是多線程并發(fā) write 并且不加同步,會導(dǎo)致文件空洞,它的執(zhí)行次序可能是

時序1:thread1 write position[0~4096)

時序2:thread3 write position[8194~12288)

時序2:thread2 write position[4096~8194)

所以并不是完全的“順序?qū)憽薄2贿^你也別擔(dān)心加鎖會導(dǎo)致性能下降,我們會在下面的小結(jié)介紹一個優(yōu)化:通過文件分片來減少多線程讀寫時鎖的沖突。

再來分析原理,順序讀為什么會比隨機讀要快?順序?qū)憺槭裁幢入S機寫要快?這兩個對比其實都是一個東西在起作用:PageCache,前面我們已經(jīng)提到了,它是位于 application buffer(用戶內(nèi)存)和 disk file(磁盤)之間的一層緩存。

文件IO操作的方法是什么

以順序讀為例,當(dāng)用戶發(fā)起一個 fileChannel.read(4kb) 之后,實際發(fā)生了兩件事

  1. 操作系統(tǒng)從磁盤加載了 16kb 進入 PageCache,這被稱為預(yù)讀

  2. 操作通從 PageCache 拷貝 4kb 進入用戶內(nèi)存

最終我們在用戶內(nèi)存訪問到了 4kb,為什么順序讀快?很容量想到,當(dāng)用戶繼續(xù)訪問接下來的[4kb,16kb]的磁盤內(nèi)容時,便是直接從 PageCache 去訪問了。試想一下,當(dāng)需要訪問 16kb 的磁盤內(nèi)容時,是發(fā)生4次磁盤 IO 快,還是發(fā)生1次磁盤 IO+4 次內(nèi)存 IO 快呢?答案是顯而易見的,這一切都是 PageCache 帶來的優(yōu)化。

深度思考:當(dāng)內(nèi)存吃緊時,PageCache 的分配會受影響嗎?PageCache 的大小如何確定,是固定的 16kb 嗎?我可以監(jiān)控 PageCache 的命中情況嗎? PageCache 會在哪些場景失效,如果失效了,我們又要哪些補救方式呢?

我進行簡單的自問自答,背后的邏輯還需要讀者去推敲:

  • 當(dāng)內(nèi)存吃緊時,PageCache 的預(yù)讀會受到影響,實測,并沒有搜到到文獻支持

  • PageCache 是動態(tài)調(diào)整的,可以通過 linux 的系統(tǒng)參數(shù)進行調(diào)整,默認是占據(jù)總內(nèi)存的 20%

  • https://github.com/brendangregg/perf-tools github 上一款工具可以監(jiān)控 PageCache

  • 這是很有意思的一個優(yōu)化點,如果用 PageCache 做緩存不可控,不妨自己做預(yù)讀如何呢?

順序?qū)懙脑砗晚樞蜃x一致,都是收到了 PageCache 的影響,留給讀者自己推敲一下。

07

/直接內(nèi)存 VS 堆內(nèi)內(nèi)存

前面 FileChannel 的示例代碼中已經(jīng)使用到了堆內(nèi)內(nèi)存: ByteBuffer.allocate(4*1024),ByteBuffer 提供了另外的方式讓我們可以分配堆外內(nèi)存 : ByteBuffer.allocateDirect(4*1024)。這就引來的一系列的問題,我什么時候應(yīng)該使用堆內(nèi)內(nèi)存,什么時候應(yīng)該使用直接內(nèi)存?

我不花太多筆墨去闡述了,直接上對比:

文件IO操作的方法是什么

關(guān)于堆內(nèi)內(nèi)存和堆外內(nèi)存的一些最佳實踐:

  1. 當(dāng)需要申請大塊的內(nèi)存時,堆內(nèi)內(nèi)存會受到限制,只能分配堆外內(nèi)存。

  2. 堆外內(nèi)存適用于生命周期中等或較長的對象。( 如果是生命周期較短的對象,在 YGC 的時候就被回收了,就不存在大內(nèi)存且生命周期較長的對象在 FGC 對應(yīng)用造成的性能影響 )。

  3. 直接的文件拷貝操作,或者 I/O 操作。直接使用堆外內(nèi)存就能少去內(nèi)存從用戶內(nèi)存拷貝到系統(tǒng)內(nèi)存的消耗

  4. 同時,還可以使用池+堆外內(nèi)存 的組合方式,來對生命周期較短,但涉及到 I/O 操作的對象進行堆外內(nèi)存的再使用( Netty中就使用了該方式 )。在比賽中,盡量不要出現(xiàn) 頻繁 newbyte[] ,創(chuàng)建內(nèi)存區(qū)域再回收也是一筆不小的開銷,使用 ThreadLocal<ByteBuffer> 和 ThreadLocal<byte[]> 往往會給你帶來意外的驚喜~

  5. 創(chuàng)建堆外內(nèi)存的消耗要大于創(chuàng)建堆內(nèi)內(nèi)存的消耗,所以當(dāng)分配了堆外內(nèi)存之后,盡可能復(fù)用它。


08

/黑魔法:UNSAFE

public class UnsafeUtil {    public static final Unsafe UNSAFE;    static {        try {            Field field = Unsafe.class.getDeclaredField("theUnsafe");            field.setAccessible(true);            UNSAFE = (Unsafe) field.get(null);        } catch (Exception e) {            throw new RuntimeException(e);        }    }}

我們可以使用 UNSAFE 這個黑魔法實現(xiàn)很多無法想象的事,我這里就稍微介紹一兩點吧。

實現(xiàn)直接內(nèi)存與內(nèi)存的拷貝:

ByteBuffer buffer = ByteBuffer.allocateDirect(4 * 1024 * 1024);long addresses = ((DirectBuffer) buffer).address();byte[] data = new byte[4 * 1024 * 1024];UNSAFE.copyMemory(data, 16, null, addresses, 4 * 1024 * 1024);

copyMemory 方法可以實現(xiàn)內(nèi)存之間的拷貝,無論是堆內(nèi)和堆外,1~2 個參數(shù)是 source 方,3~4 是 target 方,第 5 個參數(shù)是 copy 的大小。如果是堆內(nèi)的字節(jié)數(shù)組,則傳遞數(shù)組的首地址和 16 這個固定的 ARRAYBYTEBASE_OFFSET 偏移常量;如果是堆外內(nèi)存,則傳遞 null 和直接內(nèi)存的偏移量,可以通過 ((DirectBuffer) buffer).address() 拿到。為什么不直接拷貝,而要借助 UNSAFE?當(dāng)然是因為它快啊!少年!另外補充:MappedByteBuffer 也可以使用 UNSAFE 來 copy 從而達到寫盤/讀盤的效果哦。

至于 UNSAFE 還有那些黑科技,可以專門去了解下,我這里就不過多贅述了。

09

/文件分區(qū)

前面已經(jīng)提到了順序讀寫時我們需要對 write,read 加鎖,并且我一再強調(diào)的一點是:加鎖并不可怕,文件 IO 操作并沒有那么依賴多線程。但是加鎖之后的順序讀寫必然無法打滿磁盤 IO,如今系統(tǒng)強勁的 CPU 總不能不壓榨吧?我們可以采用文件分區(qū)的方式來達到一舉兩得的效果:既滿足了順序讀寫,又減少了鎖的沖突。

那么問題又來了,分多少合適呢?文件多了,鎖沖突變降低了;文件太多了,碎片化太過嚴重,單個文件的值太少,緩存也就不容易命中,這樣的 trade off 如何平衡?沒有理論答案,benchmark everything~

10

/Direct IO

文件IO操作的方法是什么

最后我們來探討一下之前從沒提到的一種 IO 方式,Direct IO,什么,Java 還有這東西?博主你騙我?之前怎么告訴我只有三種 IO 方式!別急著罵我,嚴謹來說,這并不是 JAVA 原生支持的方式,但可以通過 JNA/JNI 調(diào)用 native 方法做到。從上圖我們可以看到 :Direct IO 繞過了 PageCache,但我們前面說到過,PageCache 可是個好東西啊,干嘛不用他呢?再仔細推敲一下,還真有一些場景下,Direct IO 可以發(fā)揮作用,沒錯,那就是我們前面沒怎么提到的:隨機讀。當(dāng)使用 fileChannel.read() 這類會觸發(fā) PageCache 預(yù)讀的 IO 方式時,我們其實并不希望操作系統(tǒng)幫我們干太多事,除非真的踩了狗屎運,隨機讀都能命中 PageCache,但幾率可想而知。Direct IO 雖然被 Linus 無腦噴過,但在隨機讀的場景下,依舊存在其價值,減少了 Block IO Layed(近似理解為磁盤) 到 Page Cache 的 overhead。

話說回來,Java 怎么用 Direct IO 呢?有沒有什么限制呢?前面說過,Java 目前原生并不支持,但也有好心人封裝好了 Java 的 JNA 庫,實現(xiàn)了 Java 的 Direct IO

int bufferSize = 20 * 1024 * 1024;DirectRandomAccessFile directFile = new DirectRandomAccessFile(new File("dio.data"), "rw", bufferSize);for(int i= 0;i< bufferSize / 4096;i++){    byte[] buffer = new byte[4 * 1024];    directFile.read(buffer);    directFile.readFully(buffer);}directFile.close();


“文件IO操作的方法是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

io
AI