byte数组转file不写入磁盘_Linux 环境写文件如何稳定跑满磁盘 I/O 带宽?
準備
要求
機器配置
測試磁盤 IO 性能
實驗一: Buffer IO 寫入
實驗二: 4K 單次 Direct IO 寫入
實驗三: mmap 寫入
實驗四: 改進的 mmap 寫入
結論
準備
要求
在?限制內存?的情況下,假定我們每次寫入 4k 的數據,如何保證?kill -9?不丟數據的情況下,仍然穩定的跑滿磁盤的 IO?因為需要保證?kill -9?不丟數據,所以?fwrite()?就不在我們的考慮范圍之內了. 又因為限制內存,所以直觀的想法是直接 Direct IO, 但 Direct IO 能否跑滿磁盤 IO 呢?
機器配置
CPU: 64 核 Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz
磁盤?: Intel Optane SSD
測試磁盤 IO 性能
官方稱讀 / 寫帶寬是?2400/2000 MB/s, 我們利用?fio?來進行實測:
順序讀性能:
sudo fio --filename=test -iodepth=64 -ioengine=libaio --direct=1 --rw=read --bs=2m --size=2g --numjobs=4 --runtime=10 --group_reporting --name=test-read結果:
READ: bw=2566MiB/s (2691MB/s), 2566MiB/s-2566MiB/s (2691MB/s-2691MB/s), io=8192MiB (8590MB), run=3192-3192msec順序寫性能:
sudo fio --filename=test -iodepth=64 -ioengine=libaio -direct=1 -rw=write -bs=1m -size=2g -numjobs=4 -runtime=20 -group_reporting -name=test-write結果:
WRITE: bw=2181MiB/s (2287MB/s), 2181MiB/s-2181MiB/s (2287MB/s-2287MB/s), io=8192MiB (8590MB), run=3756-3756msec實測讀寫帶寬:?2566/2181 MB/s
實驗一: Buffer IO 寫入
因為是限制內存,所以 Buffer IO 不在我們的考慮范圍內,但是我們先來測試一下 Buffer IO 的具體性能到底如何? 我們使用最簡單的方法,因為我們的 CPU 核數是 64,所以直接 64 線程單次?4K?字節 Buffer IO 寫入, 即通過操作系統的 Page Cache 的策略來緩存,刷盤:
代碼片段?:?完整代碼
static char data[4096] attribute((aligned(4096))) = {'a'};void writer(int index) {
std::string fname = "data" + std::to_string(index);
int data_fd = ::open(fname.c_str(), O_RDWR | O_CREAT | O_APPEND, 0645);
for (int32_t i = 0; i < 1000000; i++) {
::write(data_fd, data, 4096);
}
close(data_fd);
}
int main() {
std::vectorstd::thread threads;
for(int i = 0; i < 64; i++) {
std::thread worker(writer, i);
threads.push_back(std::move(worker));
}
for (int i = 0; i < 64; i++) {
threads[i].join();
}
return 0;
}
我們通過?O_APPEND?單次 4k 追加寫入,之后通過?vmstat?來保留?120s?的寫入帶寬:
vmstat 1 120 > buffer_io經過最后的測試數據整理,我們發現 Buffer IO 的性能基本能穩定跑滿帶寬, 其中只有一次 I/O 抖動:
實驗二: 4K 單次 Direct IO 寫入
Buffer IO 利用 Page Cache 幫助我們緩存了大量的數據,其實必然提高了寫入帶寬,但假如在限制內存的情況下,Buffer IO 就不是正確的解決方案了,這次我們繞過 Page Cache, 直接 Direct IO 單次?4K?寫入:
代碼片段?:?完整代碼
唯一需要修改的地方就是在?open()?中加入?O_DIRECT?標志:
int data_fd = ::open(fname.c_str(), O_RDWR | O_CREAT | O_APPEND | O_DIRECT, 0645);通過?vmstat?獲取寫入帶寬數據, 整理如下:
通過數據我們發現,單次 4k 的 Direct IO 寫入無法跑滿磁盤的 I/O 帶寬,僅僅只有?800MB/S
實驗三: mmap 寫入
通過前面這兩個實驗我們發現,Buffer IO 是可以跑滿磁盤 I/O 的,那我們可以嘗試模擬 Buffer IO 的寫入方式,使用較少的內存來達到 Buffer IO 的寫入效果.
我們使用?mmap?來實現 Buffer IO 寫入,通過限定的 Buffer Block 來模擬 Page Cache 的聚合效果, 實驗中我們使用?memcpy?來完成數據拷貝,Buffer Block 我們設定為?4K * 4, 與 Direct IO 的不同,我們這次限定即?16KB?的單次寫入:
代碼片段:?完整代碼
main()?函數不變,修改線程的?writer()?函數:
static char data[4096] attribute((aligned(4096))) = {'a'};static int32_t map_size = 4096 * 4;
void MapRegion(int fd, uint64_t file_offset, char** base) {
void* ptr = mmap(nullptr, map_size, PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
file_offset);
if (unlikely(ptr == MAP_FAILED)) {
*base = nullptr;
return;
}
base = reinterpret_cast<char>(ptr);
}
void UnMapRegion(char* base) {
munmap(base, map_size);
}
void writer(int index) {
std::string fname = "data" + std::to_string(index);
char* base = nullptr;
char* cursor = nullptr;
uint64_t mmap_offset = 0, file_offset = 0;
int data_fd = ::open(fname.c_str(), O_RDWR | O_CREAT, 0645);
posix_fallocate(data_fd, 0, (4096UL * 1000000));
MapRegion(data_fd, 0, &base);
if (unlikely(base == nullptr)) {
return;
}
cursor = base;
file_offset += map_size;
for (int32_t i = 0; i < 1000000; i++) {
if (unlikely(mmap_offset >= map_size)) {
UnMapRegion(base);
MapRegion(data_fd, file_offset, &base);
if (unlikely(base == nullptr)) {
return;
}
cursor = base;
file_offset += map_size;
mmap_offset = 0;
}
memcpy(cursor, data, 4096);
cursor += 4096;
mmap_offset += 4096;
}
UnMapRegion(base);
close(data_fd);
}
我們通過?vmstat?來獲取寫入帶寬數據,我們發現?mmap?的?16K?寫入可以跑滿磁盤帶寬,但 I/O 抖動較大,無法類似于 Buffer IO 穩定的寫入.
我們通過?perf?生成火焰圖分析:
通過?pref?生成分析瓶頸時發現,寫入?writer()?時觸發了大量的?Page Fault, 即缺頁中斷,而?mmap()?本身的調用也有一定的消耗 (關于?mmap()?的源碼分析,我們在后面的文章會詳細分析 ),我們實驗三的思路是: 首先?fallocate?一個大文件,然后?mmap()?內存映射?16k?的 Block,?memcpy()?寫滿之后,游標右移重新?mmap(),以此循環.
實驗四: 改進的 mmap 寫入
為了避免?mmap()?的開銷,我們使用臨時文件在寫入之前?mmap()?映射,之后循環利用這?16K?的 Block, 避免?mmap()?的巨大開銷:
代碼片段:?完整代碼
void MapRegion(int fd, uint64_t file_offset, char** base) {void* ptr = mmap(nullptr, map_size, PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
file_offset);
if (unlikely(ptr == MAP_FAILED)) {
*base = nullptr;
return;
}
*base = reinterpret_cast<char*>(ptr);
}
void UnMapRegion(char* base) {
munmap(base, map_size);
}
void writer(int index) {
std::string fname = "data" + std::to_string(index);
std::string batch = "batch" + std::to_string(index);
char* base = nullptr;
char* cursor = nullptr;
uint64_t mmap_offset = 0, file_offset = 0;
int data_fd = ::open(fname.c_str(), O_RDWR | O_CREAT | O_DIRECT, 0645);
int batch_fd = ::open(batch.c_str(), O_RDWR | O_CREAT | O_DIRECT, 0645);
posix_fallocate(data_fd, 0, (4096UL * 1000000));
posix_fallocate(batch_fd, 0, map_size);
MapRegion(batch_fd, 0, &base);
if (unlikely(base == nullptr)) {
return;
}
cursor = base;
file_offset += map_size;
for (int32_t i = 0; i < 1000000; i++) {
if (unlikely(mmap_offset >= map_size)) {
pwrite64(data_fd, base, map_size, file_offset);
cursor = base;
file_offset += map_size;
mmap_offset = 0;
}
memcpy(cursor, data, 4096);
cursor += 4096;
mmap_offset += 4096;
}
UnMapRegion(base);
close(data_fd);
close(batch_fd);
}
使用?vmstat?來獲取寫入速度的數據, 整理如下:
這次避免了?mmap()?的開銷,寫入速度可以穩定保持在?2180 MB/S?左右,且沒有 I/O 抖動.
內存使用也僅僅只有?18000KB, 大約?18M:
結論
下面是四種方式的寫入速度對比:
在限制內存,且需要?kill -9?不丟數據的情況下,我們可以使用?mmap()?來模擬 Buffer IO,但為了避免頻繁?mmap()?的開銷,我們需要臨時文件來做我們的內存映射. 這種方法可以保證我們的寫入速度穩定且?kill -9?不至于丟失數據.
轉載自:http://www.leviathan.vip
-?END?-
「技術分享」某種程度上,是讓作者和讀者,不那么孤獨的東西。歡迎關注我的微信公眾號:「Kirito的技術分享」
總結
以上是生活随笔為你收集整理的byte数组转file不写入磁盘_Linux 环境写文件如何稳定跑满磁盘 I/O 带宽?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android 获取serialno_[
- 下一篇: android微信小程序自动填表_微信小