存储进阶:怎么才能保证 IO 数据的安全?
來源 |?奇伢云存儲
頭圖?| 下載于視覺中國
寫成功了數據就安全了嗎?
思考一個問題:寫數據做到什么程度才叫安全了?
就是:用戶發過來一個寫 IO 請求,只要你給他回復了 “寫成功了”,那么無論機器發生掉電,還是重啟等等之類的,數據都還能讀出來。
所以,在我們不考慮數據靜默錯誤的前提下,數據安全的最本質要求是什么?
劃重點:那就是數據一定要在非易失性的存儲介質里,你才能給用戶回復“寫成功”。請一定要記住這句話,做存儲開發的人員,80% 的時間都在思考這句話。
那么常見的易失性介質和非易失性介質有哪些呢?
易失性介質:寄存器,內存 等;非易失性介質:磁盤,固態硬盤 等;
從上到下,速度遞減,容量遞增,價格遞減。
Linux IO 簡述
我們前面提到一個文件的讀寫方式,標準庫的方式和系統調用的方式。無論是哪一種,本質上都是基于文件的一種形式,下面承接了一層文件系統,主要層次:系統調用 -> vfs -> 文件系統 -> 塊設備 -> 硬件驅動 。
我們 open 了這個文件,然后 write 數據進去。好,現在思考一個問題,當 write 返回成功之后,數據到磁盤了嗎?
答案是:不確定。
因為有文件系統的 cache ,默認是 write back 的模式,數據寫到內存就返回成功了,然后內核根據實際情況(比如定期或者臟數據達到某個閾值),異步刷盤。
這樣的好處是保證了寫的性能,貌似寫的性能非常好(可不好嘛,數據寫內存的速度),壞處是存在數據風險。因為用戶收到成功的時候,數據可能還在內存,這個時候整機掉電,由于內存是易失性介質,數據就丟了。丟數據 是存儲最不能接受的事情,相當于丟失了存儲的生命線。
動畫演示:
那么,怎么保證數據的可靠?
劃重點:還是那句話,一定要確保數據落盤之后,才向用戶返回成功。
那么怎么才能保證這一點?有以下 3 種方法。
open 文件的時候,用 O_DIRECT 模式打開,這樣 write/read 的時候,文件系統的 IO 會繞過 cache,直接跟磁盤 IO;
open 文件的時候,使用 O_SYNC 模式,確保每一筆 IO 都是同步落盤的。或者 write 之后,主動調用一把 fsync ,強制數據落盤;
讀寫文件的另一種方式是通過 mmap 函數把文件映射到進程的地址空間,讀寫進程內存的地址的數據其實是轉發到磁盤上去讀寫,write 之后主動調用一把 msync 強制刷盤;
三種安全的 IO 姿勢
?1???O_DIRECT 模式
DIRECT IO 模式能夠保證每次 IO 都直接訪問磁盤數據,而不是數據寫到內存就向用戶返回成功的結果,這樣才能確保數據安全。因為內存是易失性的,掉電就丟了,數據只有寫到持久化的介質才能安心。
動畫演示:
讀的時候也是直接讀磁盤,而不會緩存到內存中,從而也能節省整機內存的使用。
缺點也同樣明顯,由于每次 IO 都要落盤,那么性能肯定看起來差(但你要明白,其實這才是真實的磁盤性能)。
劃重點:使用了 O_DIRECT 模式之后,必須要用戶自己保證對齊規則,否則 IO 會報錯,有 3 個需要對齊的規則:
磁盤 IO 的大小必須扇區大小(512字節)對齊
磁盤 IO 偏移按照扇區大小對齊;
內存 buffer 的地址也必須是扇區對齊;
c 語言示例:
#include?<stdio.h> #include?<stdlib.h> #include?<assert.h> #include?<fcntl.h> #include?<errno.h> #include?<string.h> #include?<stdint.h>extern?int?errno; #define?align_ptr(p,?a)?\(u_char?*)(((uintptr_t)(p)?+?((uintptr_t)a?-?1))?&?~((uintptr_t)a?-?1)) int?main(int?argc,?char?**argv) {char?timestamp[8192]?=?{0,};char?*timestamp_buf?=?NULL;int?timestamp_len?=?0;ssize_t?n?=?0;int?fd?=?-1;fd?=?open("./test_directio.txt",?O_CREAT?|?O_RDWR?|?O_DIRECT,?0644);assert(fd?>=?0);//?對齊內存地址timestamp_buf?=?(char?*)(align_ptr(timestamp,?512));timestamp_len?=?512;n?=?pwrite(fd,?timestamp_buf,?timestamp_len,?0);printf("ret?(%ld)?errno?(%s)\n",?n,?strerror(errno));return?0; }編譯命令:
gcc?-ggdb3?-O0?test.c?-D_GNU_SOURCE生成二進制文件,執行下就知道了,這個是成功的。
sh-4.4#?./a.out ret?(512)?errno?(Success)如果為了驗證對齊導致的錯誤,讀者朋友可以故意讓 io 的偏移或者大小,或者內存 buffer 地址不按照 512 對齊(比如故意讓 timestamp_buf 對齊之后的地址減 1,再試下運行),會得到如下:
sh-4.4#?./a.out ret?(-1)?errno?(Invalid?argument)思考問題:有些童鞋可能會好奇問了?IO 大小和偏移按照 512 對齊我會,但是怎么才能保證 malloc 的地址是 512 對齊的呢?
是啊,我們無法用 malloc 來控制生成的地址。這對這個需求,我們有兩個解決辦法:
方法一:分配大一點的內存,然后在這個大塊內存里找到對齊的地址,只需要確保 IO 大小不會超過最后的邊界即可;
我上面的 demo 例子就是如此,分配了 8192 的內存塊,然后從里面找到 512 對齊的地址。從這個地址開始往后 512 個字節是絕對到不了這個大內存塊的邊界的。對齊的目的安全達成。
這種方式實現簡單且通用,但是比較浪費內存。
方法二:使用 posix 標準封裝的接口 posix_memalign 來分配內存,這個接口分配的內存能保證對齊;
如下,分配 1 KiB 的內存 buffer,內存地址按照 512 字節對齊。
ret?=?posix_memalign?(&buf,?512,?1024); if?(ret)?{return?-1; }思考一個問題:O_DIRECT 模式 的 IO 一般是哪些應用場景?
最常見的是數據庫系統,數據庫有自己的緩存體系和 IO 優化,無需內核消耗內存再去完成相同的事情,并且可能好心辦壞事;
不格式化文件系統,而是直接管理塊設備的場景;
?2???標準 IO + sync
sync 功能:強制刷新內核緩沖區到輸出磁盤。
在 Linux 的緩存 I/O 機制中,用戶和磁盤之間有一層易失性的介質——內核空間的 buffer cache;
讀的時候會 cache 一份到內存中以便提高后續的讀性能;
寫的時候用戶數據寫到內存 cache 就向用戶返回成功,然后異步刷盤,從而提高用戶的寫性能。
讀操作描述如下:
操作系統先看內核的 buffer cache 有緩存不?有,那么就直接從緩存中返回;
否則從磁盤中讀取,然后緩存在操作系統的緩存中;
寫操作描述如下:
將數據從用戶空間復制到內核的內存 cache 中,這時就向用戶返回成功,對用戶來說寫操作就已經完成;
至于內存的數據什么時候才真正寫到磁盤由操作系統策略決定(如果此時機器掉電,那么就會丟失用戶數據);
所以,如果你要保證落盤,必須顯式調用了 sync 命令,顯式把數據刷到磁盤(只有刷到磁盤,機器掉電才不會導致丟數據);
劃重點:sync 機制能保證當前時間點之前的數據全部刷到磁盤。。而關于 sync 的方式大概有兩種:
open 的使用使用 O_SYNC 標識;
顯式調用 fsync 之類的系統調用;
方法一:open 使用 O_SYNC 標識;
c 語言示例:
#include?<stdio.h> #include?<stdlib.h> #include?<assert.h> #include?<fcntl.h> #include?<errno.h> #include?<string.h> #include?<stdint.h>extern?int?errno;int?main(int?argc,?char?**argv) {char?buffer[512]?=?{0,};ssize_t?n?=?0;int?fd?=?-1;fd?=?open("./test_sync.txt",?O_CREAT?|?O_RDWR?|?O_SYNC,?0644);assert(fd?>=?0);n?=?pwrite(fd,?buffer,?512,?0);printf("ret?(%ld)?errno?(%s)\n",?n,?strerror(errno));return?0; }這種方式能保證每一筆 IO 都是同步 IO,一定是刷到磁盤才返回,但是這種使用姿勢一般少見,因為這個性能會很差,并且不利于批量優化。
動畫演示:
方法二:單獨調用函數 fsync
這個則是在 write 之后 fsync 一把數據到磁盤,這種方式實際生產環境用的多些,因為方便業務優化。這種方式對程序員提出了更高的要求,要求必須自己掌握好 fsync 的時機,達到既保證安全又保證性能的目的,這里通常是個權衡點。
比如,你可以 write 10 次之后,最后才調用一把?fsync,這樣既能保證刷盤,又達成了批量 IO 的優化目的。
關于這種使用姿勢,有幾個類似函數,其中有些差異,各自體會下:
//?文件數據和元數據部分都刷盤 int?fsync(int?fildes); //?文件數據部分都刷盤 int?fdatasync(int?fildes); //?整個內存?cache?都刷磁盤 void?sync(void);動畫演示:
?3???mmap + msync
這是一個非常有趣的 IO 模式,通過 mmap 函數將硬盤上文件與進程地址空間大小相同的區域映射起來,之后當要訪問這段內存中一段數據時,內核會轉換為訪問該文件的對應位置的數據。從使用姿勢上,就跟操作內存一樣,但從結果上來看,本質上是文件 IO。
void?* mmap(void?*addr,?size_t?len,?int?prot,?int?flags,?int?fd,?off_t?offset)int munmap(void?*addr,?size_t?len);
mmap 這種方式可以減少數據在用戶空間和內核空間之間的拷貝操作,當數據大的時候,采用內存映射方式去訪問文件會獲得比較好的效率(因為可以減少內存拷貝量,并且聚合 IO,數據批量下盤,有效的減少 IO 次數)。
當然,你 write 數據也還是異步落盤的,并沒有實時落盤,如果要保證落盤,那么必須要調用 msync ,調用成功,才算持久化落盤。
mmap 的優點:
減少系統調用的次數。只需要 mmap 一次的系統調用,后續的操作都是內存拷貝操作姿勢,而不是 write/read 的系統調用;
減少數據拷貝次數;
c 語言示例:
#include?<stdio.h> #include?<stdlib.h> #include?<sys/mman.h> #include?<sys/types.h> #include?<unistd.h> #include?<sys/stat.h> #include?<assert.h> #include?<fcntl.h> #include?<string.h>int?main() {int?ret?=?-1;int?fd?=?-1;fd?=?open("test_mmap.txt",?O_CREAT?|?O_RDWR,?0644);assert(fd?>=?0);ret?=?ftruncate(fd,?512);assert(ret?>=?0);char?*const?address?=?(char?*)mmap(NULL,?512,?PROT_READ?|?PROT_WRITE,?MAP_SHARED,?fd,?0);assert(address?!=?MAP_FAILED);//?神奇在這里(看起來是內存拷貝,其實是文件?IO)strcpy(address,?"hallo,?world");ret?=?close(fd);assert(ret?>=?0);//?落盤確保ret?=?msync(address,?512,?MS_SYNC);assert(ret?>=?0);ret?=?munmap(address,?512);assert(ret?>=?0);return?0; }編譯運行看看吧。
gcc?-ggdb3?-O0?test_mmap.c?-D_GNU_SOURCE是不是生成了一個 test_mmap.txt 文件,里面有一句 “hello,world”。
動畫演示:
硬件緩存
以上方式保證了文件系統那一層的落盤,但是磁盤硬件其實本身也有緩存,這個屬于硬件緩存,這層緩存也是易失的。所以最后一點是,為了保證數據的落盤,硬盤緩存也要關掉。
按照內核保證數據落盤,硬件保證關閉緩存,綜合以上的 IO 姿勢,當你寫一筆 IO 落盤之后,才能說數據寫到磁盤了,才能保證數據是掉電非易失的。
總結
數據一定要寫在非易失性的存儲介質里,你才能給用戶回復“寫成功”。其他的取巧的方式都是耍流氓、走鋼絲;
本文總結 3 種最根本的 IO 安全的方式,分別是 O_DIRECT 寫,標準 IO + Sync 方式,mmap 寫 + msync 方式。要么每次都是同步寫盤,要么就是每次寫完,再調用 sync 主動刷,才能保證數據安全;
O_DIRECT 對使用者提出了苛刻的要求,必須要滿足 IO 的 offset,length 扇區對齊,內存 buffer 地址也要扇區對齊;
注意硬盤也有緩存,這個也是易失性的,必須要考慮在內,可以通過 hdparm 命令開關;
終于,你可以安心了,數據經過層層險阻來到磁盤了。嘿嘿,你以為數據就安全了嗎?里面的名堂還多著呢,磁盤靜默錯誤壞了怎么辦?數據還能搶救一下嗎?怎么保證網絡傳輸過程不出問題?怎么保證內存拷貝過程不出問題?以后慢慢跟你說。
總結
以上是生活随笔為你收集整理的存储进阶:怎么才能保证 IO 数据的安全?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 干货!一文搞懂无状态服务
- 下一篇: 亚信安全信舱(DS)取得联通天玑安全平台