磁盤又雙叒叕滿了,怎么辦?
背景
大家好,我是石頭哥。
題目你可能讀不全(沒事,俺也一樣),特此補充拼音!
[yòu shuāng ruò zhuó]
又雙叒叕
最近有讀者遇到本文一個神奇的問題,來詢問我為什么。我看了下,確實是非常經典的坑,我在剛畢業那會也遇到過,之前文章也分享過,這次重新整理編輯下,再分享給大家。
可以評論區說說,你有遇到過這個坑嗎?
difference-between-du-and-ls
知道為什么會有上面的結果嗎?什么又是稀疏文件?這篇文章將為你揭秘。
磁盤滿告警
某天收到的自動告警短信或者郵件告訴我某機器上的磁盤滿了,趕緊登錄機器查看。
其實,這都應該定時巡檢自動化處理的。
第一次出現該問題時, 我的處理方式是: 先刪了 /tmp/ 目錄, 空閑出部分空間, 然后檢查下幾個常用的用戶目錄。
最終發現某服務A的日志文件(contentutil.log)占用了好幾個十個大G,詢問相關開發人員后確定該日志文件不需要壓縮備份, 可直接刪除,于是 rm contentutil.log 之后就天真地認為萬事大吉了...
rm 文件后,磁盤空間就釋放了嗎?
磁盤滿告警,又來了
然而,大約xx天后,發現該機器磁盤又滿了,驚呼奇怪咋這么快又滿了。
最終發現是上次 rm contentutil.log 后, 占用好幾十大G的 contentutil.log 一直被服務A的進程打開了,rm 后空間并沒有釋放。
rm 其實是刪除該文件名到文件真正保存到磁盤位置的鏈接,此時該文件句柄還被服務A打開,因此對應的磁盤空間并沒有被系統回收。
其實可以理解為 GC 里面的引用計數, rm 只是減少了引用計數,并沒有真正的進行釋放,當引用計數為0的時候,OS 內核才會釋放空間,供其他進程使用。
所以當A進程停止(文件句柄的引用計數會變為0)或者重啟后,占用的存儲空間才被釋放(從某種程度上講說明該服務一直很穩定, 可以連續跑很久不出故障~ 微笑臉)。
tip: 如果不知道具體進程或文件名的話:lsof | grep deleted,這樣會查找所有被刪除的但是文件句柄沒有釋放的文件和相應的進程,然后再kill掉進程或者重啟進程即可。
后來,白老板告知可以用修改文件內容的方式在不用重啟進程的情況下釋放空間。
下面這個技巧,劃重點:
- echo "" > filename.log
du vs ls
前兩天該問題又出現了,該服務A的日志文件(contentutil.log)占用了約7.6G。
應該對服務日志做 log rotate。
這一次學聰明了,直接用echo 'hello' > contentutil.log, 然后 df 確認磁盤空間確實已經釋放,心想著這次可以 Happy 了,突然手賤執行了下 ls 和 du, 有了以下結果:
- [root@xxx shangtongdai-content-util]# ls -lah contentutil.log
- -rw-r--r--. 1 root root 7.6G Nov 7 19:36 contentutil.log
- [root@xxx shangtongdai-content-util]# du -h contentutil.log
- 2.3M contentutil.log
反正我看到這樣的結果是百思不得其解, 如果你已經明確為什么會產生這樣的結果呢?
可以明確的是, 這里的 ls 和 du 結果肯定代表不同的含義,在查閱相關資料和咨詢強大的票圈后了解到, 這大概與文件空洞和稀疏文件(holes in 'sparse' files)相關.
ls 的結果是 apparent sizes, 我的理解是文件長度,就類似文件系統中 file 這個數據結構中的定義文件長度的這個字段;
du 的結果 disk usage,即真正占用存儲空間的大小,且默認度量單位是 block。
apparent sizes 和 disk usage 說法摘自 man du 中的 --apparent-size 部分。
給出一個具體的示例:
- // Mac OS 10.11.6 (15G1004)
- ➜ _drafts git:(source) ✗ echo -n a >1B.log
- ➜ _drafts git:(source) ✗ ls -las 1B.log
- 8 -rw-r--r-- 1 tanglei staff 1 11 9 00:06 1B.log
- ➜ _drafts git:(source) ✗ du 1B.log
- 8 1B.log
- ➜ _drafts git:(source) ✗ du -h 1B.log
- 4.0K 1B.log
上面示例中, 文件 1B.log 內容僅僅包含一個字母"a", 文件長度為1個字節, 前面的 8 為占用的存儲空間 8 個 block, (ls -s 的結果跟 du 的結果等價, 都是實際占用磁盤的空間)。
為什么1個字節的文件需要占用8個 block 呢, 可以這樣理解, block 為磁盤存儲的基本的單位,方便磁盤尋址等(這里說的基本單位應該是磁盤物理結構單位例如一個扇區/柱面等,對應一個物理單位)。
而此處的block可以理解為一個邏輯單位, 且一個文件除了包括數據外, 還需要存儲描述此文件的其他信息, 因此包含1個字節的文件實際在磁盤中占用的存儲空間不止1個字節。
這里借用最近超火的一篇文章的圖示來解釋:
不得不說,這篇 “0.2 秒居然復制了100G文件?” 文章這動圖畫得真好,火是有原因的。不過,遺憾(諷刺)的是最開始的原文竟然找不到了(后補充:源自奇伢云存儲,鏈接見評論),各個文章轉載的時候,都沒注原文。
磁盤文件管理基本單位-block
然后讀寫的時候,都用另外一個結構來存儲對應的 block 信息。
文件系統inode 和 block 區
默認情況下, Mac中1個邏輯 block 中是 512 字節, 因此 du -h 結果是 8 * 512 = 4096 = 4.0K.
If the environment variable BLOCKSIZE is set, and the -k option is not specified, the block counts will be displayed in units of that size block. If BLOCKSIZE is not set, and the -k option is not specified, the block counts will be displayed in 512-byte blocks. (man du)
因此, 通常情況下, ls 的結果應該比 du 的結果更小(都指用默認的參數執行, 調整參數可使其表達含義相同), 然而上面跑服務 A 的機器上 contentutil.log 的對比結果是 7.6G vs. 2.3M, 仍然無法理解了。
稀疏文件
沿著 man du 可以看到:
although the apparent size is usually smaller, it may be larger due to holes in ('sparse') files, internal fragmentation, indirect blocks, and the like
即因contentutil.log是一個稀疏文件, 雖然其文件長度很大, 到7.6G了, 然而其中包含大量的holes并不占用實際的存儲空間。
下面用一個具體的例子來復現以上遇到的問題。注意以下例子為 Linux version 2.6.32 (Red Hat 4.4.7)中運行結果, 且在 Mac 中并不能復現(后文有指出為什么我的Mac不能復現)。
- // 從標準輸入中讀取 count=0 個block, 輸出到 sparse-file 中,
- // 一個 block 的大小為1k(bs=1k), 輸出時先將寫指針移動到 seek 位置的地方
- [root@localhost ~]# dd of=sparse-file bs=1k seek=5120 count=0
- 0+0 records in
- 0+0 records out
- 0 bytes (0 B) copied, 1.6329e-05 s, 0.0 kB/s
- // 所以此時的文件長度為: 5M = 5120*1k(1024) = 5242880
- [root@localhost ~]# ls -l sparse-file
- -rw-r--r--. 1 root root 5242880 Nov 8 11:32 sparse-file
- [root@localhost ~]# ls -ls sparse-file
- 0 -rw-r--r--. 1 root root 5242880 Nov 8 11:32 sparse-file
- // 而 sparse-file 占用的存儲空間為 0 個 block
- [root@localhost ~]# du sparse-file
- 0 sparse-file
- [root@localhost ~]# du -h sparse-file
- 0 sparse-file
此時若用 vim 打開該文件, 用二進制形式查看 (tip :%!xxd 可以更改當前文件顯示為2進制形式), 能看到里面的內容全是0. 或者直接用od命令查看2進制.
- // vim 二進制查看
- 0000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
- 0000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
- ....
- //od -b sparse-file
- 0000000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000
- *
- 24000000
實際上, Sparse 文件是并不占用磁盤存儲空間的, 那為什么能看到文件里面包含很多0? 因為當在讀取稀疏文件的時候, 文件系統根據文件的 metadata(就是前面所指描述文件的這個數據結構)自動用0填充[ref Wiki];
Wiki上還說,現代的不少文件系統都支持 Sparse 文件, 包括 Unix 及其變種和 NTFS, 然而Apple File System(APFS)不支持, 因此我在我的 Mac 上用 du 查看占用空間與 ls 的結果一致。
- // In Mac
- ➜ ~ dd of=sparse-file bs=1k seek=5120 count=0
- 0+0 records in
- 0+0 records out
- 0 bytes transferred in 0.000024 secs (0 bytes/sec)
- ➜ ~ ls -ls sparse-file
- 10240 -rw-r--r-- 1 tanglei staff 5242880 11 9 09:44 sparse-file
- ➜ ~ du sparse-file
- 10240 sparse-file
以上是用 dd 等命令創建稀疏文件, 也有同學用 c 代碼實現了相同的功能。
其實就是寫文件的時候, 改變下當前文件寫指針,前面遇到的問題就應該類似。
- #include <stdio.h>
- #include <fcntl.h>
- #include <string.h>
- int main() {
- int fd, result;
- char wbuf[] = "hello";
- if ((fd = open("./filetest.log", O_RDWR|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR))
- ) {
- perror("open");
- return -1;
- }
- if ((result = write(fd, wbuf, strlen(wbuf)+1)) < 0) {
- perror("write");
- return -1;
- }
- if ((result = lseek(fd, 1024*1024*10, SEEK_END)) < 0) {
- perror("lseek");
- return -1;
- }
- if ((result = write(fd, wbuf, strlen(wbuf)+1)) < 0) {
- perror("write");
- return -1;
- }
- close(fd);
- return 0;
- }
以上先將"hello"寫入 filetest.log, 然后改變文件指針到1024*1024*10(相當于文件長度這個字段變大了), gcc 編譯后運行結果文件詳情如下:
- [root@localhost ~]# ls -ls filetest.log
- 8 -rw-------. 1 root root 10485772 Nov 9 17:45 filetest.log
- [root@localhost ~]# du filetest.log
- 8 filetest.log
- [root@localhost ~]# du -h filetest.log
- 8.0K filetest.log
- [root@localhost ~]# ls -lh filetest.log
- -rw-------. 1 root root 11M Nov 9 17:45 filetest.log
- [root@localhost ~]# od -c filetest.log
- 0000000 h e l l o \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
- 0000020 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
- *
- 50000000 \0 \0 \0 \0 \0 \0 h e l l o \0
- 50000014
解釋下結果: 文件長度應該是 "hello" 加上 "\n" 共6個字節*2 = 12, 再加上1024*1024*10個字節, 即為ls產生的結果10485772個字節約11M。
而du的結果為8個block也為8k(這臺機器上的block大小與前面的Mac不一樣, 這里是1024)。
Display values are in units of the first available SIZE from --block-size, and the DU_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set. (du --help)
總結
總結一下: 出現以上問題說明自己對一些基礎掌握得尚不牢固, 比如
rm 某文件后, 文件占用的磁盤空間并不是立即釋放,而是其句柄沒有被任意一個進程引用時才回收;
ls/du 命令結果的具體含義;
稀疏文件。