关于 fallocate 文件系统预分配 的一些细粒度测试
文章目錄
- Rocksdb 中的預分配
- Fallocate in rocksdb 性能測試
- Fallocate 使用 以及 對應配置的行為
- API 使用
- 不同 Mode 的行為
- 分配磁盤空間
- 釋放磁盤空間
- 折疊/裁剪 文件內容
- 清零文件 + 擴容文件
Rocksdb 中的預分配
預分配文件存儲空間 在存儲引擎中用的還是比較頻繁的,尤其是 在Appen-only 形態下的文件寫入采用預分配還是對性能比較友好的。
主要優勢:
- 能夠有效減少每次 io 時 文件系統分配磁盤空間 并 更新對應inode 屬性時的開銷,讓文件系統一次性為當前文件分配合理的磁盤空間能夠減少這一部分的開銷。
- 對于可靠性要求比較高的 WAL/REDO log文件來說,開啟預分配能夠減少磁盤空間滿時沒有空間出現的 i/o 異常問題,因為已經提前分配好了大塊空間,坑位已經占下了,盡情寫入即可。
我們的os 文件系統提供了預分配機制,并為外部用戶提供了相關的系統調用來直接使用。
在 5 系版本的內核下,已經支持預分配機制的內核文件系統包括如下幾個。
對于Rocksdb 來說預分配能夠進一步提升寫 WAL 的性能,尤其是在wal 開sync 的情況下通過預分配好的磁盤空間,能夠減少每次請求重新分配磁盤塊的開銷。
Fallocate in rocksdb 性能測試
做一個簡單的測試,如下db_bench 配置,fallocate 會處理文件的預分配,如果要對比差異,也就是對比不用fallocate 時每一個請求需要做一次空間分配的情況,則建議打開 wal 的 sync操作。
./db_bench \-benchmarks="fillsync" \-print_malloc_stats=true \-threads=8 \-sync=true \-value_size=128 \-num=1000000 \-db=./db6 \-compression_type=none \-allow_fallocate=false #db_bench沒有這個配置,增加了一個
-
8 threads, with_fallocate=false
fillsync : 119.996 micros/op 66667 ops/sec; 9.2 MB/s (1000 ops) -
8 threads, with_fallocate=true
fillsync : 118.707 micros/op 67391 ops/sec; 9.3 MB/s (1000 ops)
可以看到在開啟了預分配之后,整體的性能還是有一部分的提升(10%的樣子)
Rocksdb 中對于預分配 接口使用的調用棧為:
WritableFileWriter::AppendFSWritableFile::PrepareWritePosixWritableFile::Allocatefallocate
通過 EnvOptions 的參數allow_fallocate 以及 fallocate_with_keep_size 來控制是否使用fallocate 以及 它的行為。
Fallocate 使用 以及 對應配置的行為
API 使用
接下來主要看一下預分配接口的一些基本使用:
使用的過程是非常簡單的,包含頭文件#include <linux/falloc.h> 即可,這樣fallocate 不同的 mode 名字可以直接使用。
int fallocate(int fd, int mode, off_t offset, off_t len);
- fd 就是通過 open 系統調用返回的文件描述符。
- mode 則是fallocate 支持的不同 配置,用來指定fallocate 的不同行為。
- offset 和 len 表示接下來要操作這個文件的 起始偏移位置 以及 操作的長度。
因為這個接口對于一些用戶來說不是很方便,因為它需要使用者清楚多個不同的mode 適用的場景,增加了接口使用的學習成本。很多人只想要使用fallocate 來直接預分配磁盤空間就可以了,其他的不想了解。
所以,glibc 提供了庫函數 posix_fallocate:
int posix_fallocate(int fd, off_t offset, off_t len);
用戶不需要再關心mode了, 它的底層mode 使用的直接是0,就是直接分配[offset, offset+len]大小的磁盤空間。
簡單代碼:
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <linux/falloc.h>using namespace std;
int main(int argc, char* argv[]) {char* filename="111.txt";int fd = open(filename, O_CREAT | O_RDWR, 0660);if (fd == -1) {fprintf(stdout, "open file failed : %d\n", errno);exit(-1);}// 這里也可以替換為 posix_fallocate(fd, 0, 65536);if (fallocate(fd, 0, 0, 65536)) {fprintf(stdout, "fallocate file failed\n");exit(-1);}close(fd);return 0;
}
最后可以看到預分配的文件,文件系統側看到的大小為64K,占用了128 個磁盤block塊(每個 block 為 512B)
$ stat 111.txtFile: ‘111.txt’Size: 65536 Blocks: 128 IO Block: 4096 regular file
Device: 10301h/66305d Inode: 4369192664 Links: 1
Access: (0660/-rw-rw----) Uid: ( 1001/test) Gid: ( 1001/test)
Access: 2021-11-29 00:55:03.484240060 +0800
Modify: 2021-11-29 00:56:42.629443806 +0800
Change: 2021-11-29 00:56:42.629443806 +0800
不同 Mode 的行為
分配磁盤空間
-
mode=0,也就是前面測試代碼的默認行為,會將inode 的文件大小 以及 需要占用的磁盤空間都直接進行預分配,這種情況下拿到的文件fd 在后續的持續寫入時不再需要預分配 以及 修改inode 中的文件大小屬性。分配的方式是 直接對設置的 offset 填充 len 個0,當然,如果offset 范圍內還有數據,則會跳過這一些數據,直接填充0。
這也是rocksdb 中 建議 寫WAL時 進行預分配時采用的mode。
-
mode=FALLOC_FL_KEEP_SIZE表示 在預分配的過程中 僅預分配磁盤空間,不會變更文件的inode 元信息。
也就是會出現ls -lh file和du -sh file不一致的情況。
測試代碼:
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <linux/falloc.h>using namespace std;bool Fallocate(int fd, int use_keep_size, off_t off, off_t len) {int mode = use_keep_size > 0 ? FALLOC_FL_KEEP_SIZE : 0;int ret = fallocate(fd, mode , off, len);if (ret != 0) {fprintf(stdout, "fallocate failed with %d\n", errno);return false;}return true;
}int main(int argc, char* argv[]) {int use_keep_size = 0; // 輸入為1 ,則開啟keep_size 配置char filename[1024]; // 文件名int use_close = 0; // 關閉文件if (argc > 1) {use_keep_size = atoi(argv[1]);memcpy(filename, argv[2], strlen(argv[2]));use_close = atoi(argv[3]);}int fd = open(filename, O_CREAT | O_RDWR, 0660);if (fd == -1) {fprintf(stdout, "open file failed\n");exit(-1);}size_t size = write(fd, std::string(8192,'a').c_str(), 8192);if (!Fallocate(fd, use_keep_size, 0, 65536)) {exit(-1);}if (use_close) close(fd);return 0;
}
先寫入了8192 字節的數據,再預分配 從 0-65536 的空間,看看 使用 mode 為 0 和 FALLOC_FL_KEEP_SIZE 的磁盤空間占用情況:
-
./test_fallocate 0 nihao1 1磁盤空間和 file_size 都直接變更為了預分配到的數據$ stat nihao1File: ‘nihao1’Size: 65536 Blocks: 128 IO Block: 4096 regular file Device: 10301h/66305d Inode: 4369192662 Links: 1 Access: (0660/-rw-rw----) Uid: ( 1001/test) Gid: ( 1001/test) Access: 2021-11-29 01:24:50.387540475 +0800 Modify: 2021-11-29 01:24:50.387540475 +0800 Change: 2021-11-29 01:24:50.387540475 +0800$ ls -lh nihao1 -rw-rw---- 1 test test 64K Nov 29 01:24 nihao1$ du -sh nihao1 64K nihao1 -
./test_fallocate 1 nihao1 1使用了keep_size 之后 file_size 顯示的是真實數據的大小,但是磁盤空間已經分配出來了$ stat nihao1File: ‘nihao1’Size: 8192 Blocks: 128 IO Block: 4096 regular file Device: 10301h/66305d Inode: 4369192662 Links: 1 Access: (0660/-rw-rw----) Uid: ( 1001/test) Gid: ( 1001/test) Access: 2021-11-29 01:25:16.988146849 +0800 Modify: 2021-11-29 01:25:16.988146849 +0800 Change: 2021-11-29 01:25:16.988146849 +0800$ ls -lh nihao1 # -rw-rw---- 1 test test 8.0K Nov 29 01:25 nihao1$ du -sh nihao1 64K nihao1
釋放磁盤空間
釋放磁盤空間這里主要關注的是一個配置 FALLOC_FL_PUNCH_HOLE,這個配置需要和 FALLOC_FL_KEEP_SIZE 一起使用。
fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, off, len);
用來標識 從 off 到 off+len 的 fd 文件內容全部寫入0,而且對應的文件管理的磁盤block 也會被刪除。
根據上面的代碼,修改bool Fallocate 函數:
bool Fallocate(int fd, int use_keep_size, off_t off, off_t len) {// use_keep_size :// 0 : 0 預分配磁盤空間 以及 變更文件大小// 1 : FALLOC_FL_KEEP_SIZE 預分配磁盤空間,保留文件大小問為當前數據大小// 2 : FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE 填充0int mode = use_keep_size > 0 ?(use_keep_size == 2 ? FALLOC_FL_KEEP_SIZE | FALLOC_FL_PUNCH_HOLE :FALLOC_FL_KEEP_SIZE) : 0;int ret = fallocate(fd, mode , off, len);if (ret != 0) {fprintf(stdout, "fallocate failed with %d\n", errno);return false;}return true;
}
我們先在磁盤上寫入 8k 的數據,再 對 [0,4k] 的內容打一個洞,預期的文件大小是 8k(因為需要和keep_size 一起使用,keep_size 會保留文件之前的file_size 元信息),但是blocks的個數會從 寫入8k 時候的 16個 變為 8個。
$ ./test_fallocate 2 nihao1 1
$ stat nihao1File: ‘nihao1’Size: 8192 Blocks: 8 IO Block: 4096 regular file
Device: 10301h/66305d Inode: 4369192662 Links: 1
Access: (0660/-rw-rw----) Uid: ( 1001/test) Gid: ( 1001/test)
Access: 2021-11-29 23:15:50.102077219 +0800
Modify: 2021-11-29 23:15:50.103077241 +0800
Change: 2021-11-29 23:15:50.103077241 +0800
查看文件內容,可以看到前4K 已經被寫入0, 后面的4K都是正常的數據。
需要注意,只有部分文件系統能夠支持 FALLOC_FL_PUNCH_HOLE:xfs, ext4, btrfs, tmpfs, gfs2
折疊/裁剪 文件內容
這里fallocate 通過 FALLOC_FL_COLLAPSE_RANGE 的行為和 ftruncate 系統調用的行為比較相似,就是把制定的 offset – offset+len 這段空間的內容直接折疊并且對應的block也會清理掉,相當于將offset+len 位置的內容覆蓋到 offset這個位置。
測試:寫入8k 數據,折疊從0 - 4k 位置的內容。
$ stat nihao1File: ‘nihao1’Size: 4096 Blocks: 8 IO Block: 4096 regular file
Device: 10301h/66305d Inode: 4369192662 Links: 1
Access: (0660/-rw-rw----) Uid: ( 1001/web_server) Gid: ( 1001/web_server)
Access: 2021-11-29 23:31:25.588654425 +0800
Modify: 2021-11-29 23:31:25.588654425 +0800
Change: 2021-11-29 23:31:25.588654425 +0800
這個配置目前只在 ext4 和 xfs 上有效。
清零文件 + 擴容文件
這里補充最后兩個參數:
-
FALLOC_FL_ZERO_RANGE(覆蓋原有內容)會將指定的 range 內部所有內容寫入0x0,行為有點像是打洞 FALLOC_FL_PUNCH_HOLE ,但是這個會占用磁盤空間以及更新文件大小的meta信息。相當于是覆蓋寫入了0,后面的讀取也會讀出0。打洞則不會占用磁盤block空間。這個配置對一個文件重寫0 的性能還是很給力的,fallocate 10G 的空間 write 寫0 10G的空間,性能差異巨大:
$ time ./test_fallocate 2 nihao1 1 # write 寫0 ,寫入10G real 0m7.679s user 0m0.608s sys 0m7.047s$ dd if=/dev/zero of=./hello bs=4M count=2560 # dd 寫入10G 2560+0 records in 2560+0 records out 10737418240 bytes (11 GB) copied, 4.32988 s, 2.5 GB/s$ time ./test_fallocate 2 nihao1 1 # fallocate 只需要一次系統調用的時間 real 0m0.002s user 0m0.002s sys 0m0.000s -
FALLOC_FL_INSERT_RANGE用于文件擴容,在指定的range 內填充0,不會覆蓋原有內容。比如,先寫入了8k,后面想要在0 位置插入16k 的空洞, 則8k 的原本內容會被移動到0到16K 后面。并且,這個配置會分配磁盤塊空間 以及 更新對應的文件元數據。感覺這個配置有點雞肋,還不如直接 ftruncate。而且,這個配置僅在 4.1 及 以后版本支持xfs,4.2 及以后版本支持 ext4。
總結
以上是生活随笔為你收集整理的关于 fallocate 文件系统预分配 的一些细粒度测试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 求一个很骚的个性签名
- 下一篇: Linux 下获取本机所有网卡 以及 网