网站开发技术汇总,上海工程项目查询,苏中建设网站,创建论坛网站需要多少钱来源 | 奇伢云存储头图 | 下载于视觉中国写成功了数据就安全了吗#xff1f;思考一个问题#xff1a;写数据做到什么程度才叫安全了#xff1f;就是#xff1a;用户发过来一个写 IO 请求#xff0c;只要你给他回复了 “写成功了”#xff0c;那么无论机器发生掉电#x… 来源 | 奇伢云存储头图 | 下载于视觉中国写成功了数据就安全了吗思考一个问题写数据做到什么程度才叫安全了就是用户发过来一个写 IO 请求只要你给他回复了 “写成功了”那么无论机器发生掉电还是重启等等之类的数据都还能读出来。所以在我们不考虑数据静默错误的前提下数据安全的最本质要求是什么划重点那就是数据一定要在非易失性的存储介质里你才能给用户回复“写成功”。请一定要记住这句话做存储开发的人员80% 的时间都在思考这句话。那么常见的易失性介质和非易失性介质有哪些呢易失性介质寄存器内存 等非易失性介质磁盘固态硬盘 等从上到下速度递减容量递增价格递减。Linux IO 简述我们前面提到一个文件的读写方式标准库的方式和系统调用的方式。无论是哪一种本质上都是基于文件的一种形式下面承接了一层文件系统主要层次系统调用 - vfs - 文件系统 - 块设备 - 硬件驱动 。我们 open 了这个文件然后 write 数据进去。好现在思考一个问题当 write 返回成功之后数据到磁盘了吗答案是不确定。因为有文件系统的 cache 默认是 write back 的模式数据写到内存就返回成功了然后内核根据实际情况比如定期或者脏数据达到某个阈值异步刷盘。这样的好处是保证了写的性能貌似写的性能非常好可不好嘛数据写内存的速度坏处是存在数据风险。因为用户收到成功的时候数据可能还在内存这个时候整机掉电由于内存是易失性介质数据就丢了。丢数据 是存储最不能接受的事情相当于丢失了存储的生命线。动画演示那么怎么保证数据的可靠划重点还是那句话一定要确保数据落盘之后才向用户返回成功。那么怎么才能保证这一点有以下 3 种方法。open 文件的时候用 O_DIRECT 模式打开这样 write/read 的时候文件系统的 IO 会绕过 cache直接跟磁盘 IOopen 文件的时候使用 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.hextern 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 syncsync 功能强制刷新内核缓冲区到输出磁盘。在 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.hextern 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.hint 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);// 神奇在这里看起来是内存拷贝其实是文件 IOstrcpy(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 文件里面有一句 “helloworld”。动画演示硬件缓存以上方式保证了文件系统那一层的落盘但是磁盘硬件其实本身也有缓存这个属于硬件缓存这层缓存也是易失的。所以最后一点是为了保证数据的落盘硬盘缓存也要关掉。# 查看写缓存状态
hdparm -W /dev/sda
# 关闭 HDD Cache,保证数据强一致性避免断电时数据未落盘
hdparm -W 0 /dev/sda
# 打开 HDD Cache断电时可能导致丢数据
hdparm -W 1 /dev/sda按照内核保证数据落盘硬件保证关闭缓存综合以上的 IO 姿势当你写一笔 IO 落盘之后才能说数据写到磁盘了才能保证数据是掉电非易失的。总结数据一定要写在非易失性的存储介质里你才能给用户回复“写成功”。其他的取巧的方式都是耍流氓、走钢丝本文总结 3 种最根本的 IO 安全的方式分别是 O_DIRECT 写标准 IO Sync 方式mmap 写 msync 方式。要么每次都是同步写盘要么就是每次写完再调用 sync 主动刷才能保证数据安全O_DIRECT 对使用者提出了苛刻的要求必须要满足 IO 的 offsetlength 扇区对齐内存 buffer 地址也要扇区对齐注意硬盘也有缓存这个也是易失性的必须要考虑在内可以通过 hdparm 命令开关终于你可以安心了数据经过层层险阻来到磁盘了。嘿嘿你以为数据就安全了吗里面的名堂还多着呢磁盘静默错误坏了怎么办数据还能抢救一下吗怎么保证网络传输过程不出问题怎么保证内存拷贝过程不出问题以后慢慢跟你说。☞再见 Nacos我要玩 Service Mesh 了☞急CPU 被挖矿该怎么找进程☞从 0 到 1高德 Serverless 平台建设及实践☞听完姚期智的一句“嘟囔”他开始第二次创业点分享点收藏点点赞点在看