如何做到 0-2 秒複製 100G 文件?

cp 引發的思考

今天同事用 cp 命令,把他給驚到了!

背景是這樣的:他用 cp 拷貝了一個 100 G 的文件,竟然一秒不到就拷貝完成了!

用 ls 看一把文件,顯示文件確實是 100 G。

sh-4.4# ls -lh  
-rw-r--r-- 1 root root 100G Mar  6 12:22 test.txt

但是 copy 起來爲什麼會這麼快呢?

sh-4.4# time cp ./test.txt ./test.txt.cp  
  
real 0m0.107s  
user 0m0.008s  
sys 0m0.085s

一個 SATA 機械盤的寫能力能到 150 M/s (大部分的機械盤都是到不了這個值的)就算非常不錯了,正常情況下,copy 一個 100G 的文件至少要 682 秒(100 G/ 150 M/s),也就是 11 分鐘。

實際情況卻是 cp 一秒沒到就完成了工作,驚呆了,爲啥呢?

更詭異的是:他的文件系統只有 40 G,爲啥裏面會有一個 100 G 的文件呢?

同事把我找來,看看這個詭異的問題。

分析文件

我讓他先用 du 命令看一下,卻只有 2M ,根本不是 100G,這是怎麼回事?

sh-4.4# du -sh ./test.txt  
2.0M ./test.txt

再看 stat 命令顯示的信息:

sh-4.4# stat ./test.txt  
  File: ./test.txt  
  Size: 107374182400 Blocks: 4096       IO Block: 4096   regular file  
Device: 78h/120d Inode: 3148347     Links: 1  
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)  
Access: 2021-03-13 12:22:00.888871000 +0000  
Modify: 2021-03-13 12:22:46.562243000 +0000  
Change: 2021-03-13 12:22:46.562243000 +0000  
 Birth: -

stat 命令輸出解釋:

劃重點:

同事問道:“文件大小和實際物理佔用,這兩個竟然不是相同的概念 !爲什麼是這樣?”

“看來,我們必須得深入文件系統才能理解了,來,我給你好好講講。”

文件系統

文件系統聽起來很高大上,通俗話就用來存數據的一個容器而已,本質和你的行李箱、倉庫沒有啥區別,只不過文件系統存儲的是數字產品而已。

我有一個視頻文件,我把這個視頻放到這個文件系統裏,下次來拿,要能拿到我完整的視頻文件數據,這就是文件系統,對外提供的就是存取服務。

現實的存取場景

例如你到火車站使用寄存服務:

存行李的時候,是不是要登記一些個人信息?對吧,至少自己名字要寫上。可能還會給你一個牌子,讓你掛手上,這個東西就是爲了標示每一個唯一的行李。

取行李的時候,要報自己名字,有牌子的給他牌子,然後工作人員才能去特定的位置找到你的行李。

劃重點:存的時候必須記錄一些關鍵信息(記錄 ID、給身份牌),取的時候才能正確定位到。

文件系統

回到我們的文件系統,對比上面的行李存取行爲,可以做個簡單的類比。

上面的對應並不是非常嚴謹,僅僅是幫助大家理解文件系統而已,讓大家知道其實文件系統是非常樸實的一個東西,思想都來源於生活。

空間管理

現在思考文件系統是怎麼管理空間的?

如果,一個連續的大磁盤空間給你使用,你會怎麼使用這段空間呢?

直觀的一個想法,我把進來的數據就完整的放進去。

這種方式非常容易實現,屬於眼前最簡單,以後最麻煩的方式。因爲會造成很多空洞,明明還有很多空間位置,但是由於整個太大,形狀不合適(數據大小),哪裏都放不下。因爲你要放一個完整的空間。

怎麼改進?有人會想,既然整個放不進去,那就剁碎了唄。這裏塞一點,那裏塞一點,就塞進去了。

對,思路完全正確。改進的方式就是切分,把空間按照一定粒度切分。每個小粒度的物理塊命名爲 Block,每個 Block 一般是 4K 大小,用戶數據存到文件系統裏來自然也是要切分,存儲到磁盤上各個角落。

圖示標號表示這個完整對象的 Block 的序號,用來複原對象用的。

隨之而來又有一個問題:你光會切成塊還不行,取文件數據的時候,還得把它們給組合起來纔行。

所以,要有一個表記錄文件對應所有 Block 的位置,這個表被文件系統稱爲 inode。

寫文件的流程是這樣的:

讀文件流程則是:

inode/block 概念

好,我們現在來看看 inode,直觀地感受一下:

這個 inode 有文件元數據和 Block 數組(長度是 15),數組中前兩項指向 Block 3 和 Block 11,表示數據在這兩個塊中存着。

你肯定會意識到:Block 數組只有 15 個元素,每個 Block 是 4K, 難道一個文件最大隻能是 15 * 4K = 60 K?

這是絕對不行的!

最簡單的辦法就是:把這個 Block 數組長度給擴大!

比如我們想讓文件系統最大支持 100G 的文件,Block 數組需要這麼長:

(10010241024)/4 = 26214400

Block 數組中每一項是 4 個字節,那就需要 (26214400*4)/1024/1024 = 100M。

爲了支持 100G 的文件,我們的 Block 數組本身就得 100M !

並且對每個文件都是如此 !即使這個文件只有 1K! 這將是巨大浪費!

肯定不能這麼幹,解決方案就是間接索引,按照約定,把這 15 個槽位分作 4 個不同類別來用:

  1. 前 12 個槽位(也就是 0 - 11 )我們成爲直接索引;

  2. 第 13 個位置,我們稱爲 1 級索引;

  3. 第 14 個位置,我們稱爲 2 級索引;

  4. 第 15 個位置,我們稱爲 3 級索引。

直接索引:能存 12 個 block 編號,每個 block 4K,就是 48K,也就是說,48K 以內的文件,前 12 個槽位存儲編號就能完全 hold 住。

一級索引:也就是說這裏存儲的編號指向的 block 裏面存儲的也是 block 編號,裏面的編號指向用戶數據。一個 block 4K,每個元素 4 字節,也就是有 1024 個編號位置可以存儲。

所以,一級索引能尋址 4M(1024 * 4K)空間。

二級索引:二級索引是在一級索引的基礎上多了一級而已,換算下來,有了 4M 的空間用來存儲用戶數據的編號。所以二級索引能尋址 4G(4M/4 * 4K)的空間。

三級索引:三級索引是在二級索引的基礎上又多了一級,也就是說,有了 4G 的空間來存儲用戶數據的 block 編號。所以二級索引能尋址 4T(4G/4 * 4K)的空間。

所以,在這種文件系統(如 ext2)上,通過這種間接塊索引的方式,最大能支撐的文件大小 = 48K + 4M + 4G + 4T ,約等於 4 T。

這種多級索引尋址性能表現怎麼樣?

在不超過 12 個數據塊的小文件的尋址是最快的,訪問文件中的任意數據理論只需要兩次讀盤,一次讀 inode,一次讀數據塊。

訪問大文件中的數據則需要最多五次讀盤操作:inode、一級間接尋址塊、二級間接尋址塊、三級間接尋址塊、數據塊。

爲什麼 cp 那麼快?

接下來我們要寫入一個奇怪的文件,這個文件很大,但是真正的數據只有 8K:

在 [0 , 4K] 這位置有 4K 的數據。

在 [1T , 1T+4K] 處也有 4K 數據。

中間沒有數據,這樣的文件該如何寫入硬盤?

  1. 創建一個文件,這個時候分配一個 inode;

  2. 在 [0 , 4K] 的位置寫入 4K 數據,這個時候只需要 一個 block,把這個編號寫到 block[0] 這個位置保存起來;

  3. 在 [1T , 1T+4K] 的位置寫入 4K 數據,這個時候需要分配一個 block,因爲這個位置已經落到三級索引才能表現的空間了,所以需要還需要分配出 3 個索引塊;

  4. 寫入完成,close 文件。

實際存儲如圖:

這個時候,我們的文件看起來是超大文件,size 等於 1T + 4K ,但裏面實際的數據只有 8K,位置分別是 [0 , 4K] ,[1T , 1T+4K]。

由於沒寫數據的地方不用分配物理 block 塊,所以實際佔用的物理空間只有 8K。

重點:文件 size 只是 inode 裏面的一個屬性,實際物理空間佔用則是要看用戶數據放了多少個 block ,沒寫數據的地方不用分配物理 block 塊。

這樣的文件其實就是稀疏文件, 它的邏輯大小和實際物理空間是不相等的。

所以當我們用 cp 命令去複製一個這樣的文件時,那肯定迅速就完成了。

總結

好,我們再深入思考下,文件系統爲什麼能做到這一點?

這三點是層層遞進的。

後記

我把這點小知識給小夥伴講了一小時,看到他感動欲哭的表情,我覺得他學 fei 了,非常滿意。

是我想太多了嗎?中午喫飯都沒叫我。

作者:OSC 開源社區

來源:https://mp.weixin.qq.com/s/GqKHhAd93iQorDeGyHBMnA

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