最近一段时间的项目进入阶段性收尾,项目中由于遇到了数据需要落地的场景,因此,便需要好好整理一下。这里从几个问题开始:
1、对于传统的机械硬盘,如何让写磁盘速度更快?如何了解自己是否已经达到最优?
2、多进程写文件的时候,如何保证文件有序?
3、只有一个磁盘,多进程写文件的时候,是同时写到一个文件更快,还是多个进程写到多个文件,总体IO效率更高?
4、在某些对数据严格要求安全的场景下,有什么常用方案保证数据落地的安全?
5、写一段磁盘内容,有没有其他方式可以加速?
问题1:对于传统的机械硬盘,如果写磁盘速度更快?如何了解自己是否已经达到最优?
对于这个问题,我先插一段自己对于技术价值的理解。对于一个有志成为业界一流水准的工程师而言,你必须有一个思维方式:做事的时候,知道事情的边界在哪里,那你做起事来,便知道何事可为,何事不可为之。不至于把时间花在一些不可能的事情上。例如:对于把一堆数字排序的问题而言,计算机理论上已经能够证明出来,O(nlogn)已经是理论极限,那你就不应该想着去发明一种比快排更快的排序方法。对于数据传输而言,香农定理已经给出了传输速率的极限值,那我们而言,只能往着这个方向去接近,而不要想着去超越。
回到这个问题,我们首先应该做的是,先了解磁盘的物理特征,就是他的物理极限值是多少,那我们就对于我们的应用程序能做到什么水平,心里大概有个底。而不至于应用程序写效率到达100M/s的时候,你无法直观的感觉到,这到底是快,还是慢?
机械硬盘的读写,物理上是通过一个磁头来寻找磁道,并把数据写到磁盘上。其中,最耗时的操作便是寻找扇区的时候,这时候,自己应该有一个经验值,那就是毫秒级别。具体可以看一个博客,里面有分析磁盘的一些物理特征。磁盘IO那些事儿
从磁盘的物理特征可以看出来,磁盘最擅长的操作是顺序读写操作,因为磁头可以连续读,而不需要进行来回切换寻找磁道。这是我们面对磁盘IO工程问题中,最核心的思路。这是关于问题1中,我的理解。
那如何简单快速的测试磁盘的写速度能达到多少呢?可以简单的使用一个dd命令。命令如下:time dd if=/dev/zero of=/test.dbf bs=1k count=3000000
如下图所示,我在一台服务器上测试的结果如下图所示:
因此,自己的应用程序,如果每次写缓冲大小为8K,那可以简单把这个数值当成一个磁盘写操作的极限值大概为1.5G/s。
对于读操作,也可以简单测一个经验值。命令如下:time dd if=/test.dbf of=/dev/null bs=8k
这是对磁盘IO的两个基本问题:一是认识到磁盘IO的一个物理特征和优化方向(顺序性)二是认识到磁盘读写的一个物理瓶颈值。
问题2、多进程写文件追回写的时候,如何保证文件有序?
对磁盘IO而言,每次读写,需要调整文件文件的写指针位置。而如果多进程写操作,每次操作都去移动指针到末尾,再执行写操作,则可能出现竞争问题,可能出现乱序问题。这里可以使用使用APPEND选项,每次写入的时候,内核会保证移动指针与写入的原子操作。
问题3、只有一个磁盘,多进程写文件的时候,是同时写到一个文件更快,还是多个进程写到多个文件,总体IO效率更高?
观点1:写多个会更快一些。因为写到一个文件的时候,需要内核对多个写操作做同步操作,而写多个不会有这个问题。
观战:多进程写一个文件会更快,因为写到一个文件的时候,全局总体来看是写到一个文件,是顺序写,而写多个文件,需要磁盘来回切。
多说无益,我们直接来做一下测试吧 。对于工程性的学科,我们即要有较高的工程理论素养,又要有比较强的动手能力,这才是一个实干型的解决问题的高手。
测试环境为CentOS Linux release 7.4.1708,测试过程每次写缓冲区大小为1KB,使用带IO缓冲的方式进行。
测试的代码如下:
#include <iostream>
#include <fcntl.h>
#include <string>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
using namespace std;
const int MAX_LOOP = 1024 * 1024 ;
int main(int argc, char** argv) {
string file_path = "./log/test_";
if (argc > 1) {
file_path += string(argv[1]);
}
else {
file_path += string("0");
}
file_path += ".dat";
int fd = open(file_path.c_str(), O_APPEND | O_RDWR | O_CREAT , 0666);
if (fd < 0) {
cerr << "fd = " << fd << endl;
cerr << "errmsg = " << strerror(errno) << endl;
return -1;
}
int buf_len = 1024;
char* ptr = (char*)malloc(buf_len);
printf("prt = %p\n", ptr);
int len = buf_len;
for (int i = 0; i < MAX_LOOP; ++i) {
ssize_t size = write(fd, ptr, len);
if (size != len) {
cerr << "len = " << size << " errno = " << errno << endl;
cerr << "len < 0 " << strerror(errno) << endl;
break;
}
}
close(fd);
return 0;
}
测试结论如下:
每个文件大小 | 写同一个文件耗时 | 写不同文件耗时 |
1G | 11.5s | 1.53s |
5G | 50.4s | 34.7s |
从测试的结论来看,写不同的文件的方式,在该种场景下的效率要高。根据结果来看,不同进程写缓冲的内核的时候,同步到缓冲区的时候消耗较大的时间,导致写一个文件的时候,总体效率较慢。
说明:这个结论,仅限于当前的所有条件下,并不能作为一个普遍性的结论,就认为写磁盘的时候,多进程写多文件,就一定比写单文件效率更高。后续将就这个问题,再做一次系统性的测试并分析结果。
问题4:在某些对数据严格要求安全的场景下,有什么常用方案保证数据落地的安全?
首先,可能会想到写到磁盘就已经是数据安全。没错,很多场景下,对于数据落地的话,例如写日志场景,这样已经够用了。但如果对于业务要求数据绝对不能丢失的情况,例如,用户的支付流水,用户的交易数据等,如果只是普通的调用库函数写文件,或者就是简单的open来写,可能存在机器掉电的情况下,导致数据还停留在系统的IO缓冲区中,没有写到磁盘而存在丢失的风险。那么如何解决呢?
方案一:保证写磁盘成功,再返回,再不是直接写到IO缓冲区中就认为是写成功。可以通过open打开文件的时候,通过O_SYNC和O_DIRECT标记。这个标记表示直接绕过系统IO缓冲把用户缓冲的数据直接写到磁盘之中。返回之后,即是可以认为磁盘已经接收到用户数据。那方案一对于简单的单机版本,可能存在什么问题呢?那就是存在磁盘损坏的可能?那怎么解决磁盘数据单一的问题?且看方案二
方案二:对于数据要求严格安全的场景下,只是简单的保证写到磁盘之中,并不能保证业务安全。例如说,磁盘损坏,数据无法恢复。简单的解决方案,可以对磁盘做raid冗余技术。raid n的技术方案和原理,可以参考这里。一言以蔽之,就是让硬件来做数据备份。raid技术简介,对磁盘做raid,就是对磁盘损失问题,成本最低的解决方案。那如果要求更高,机房存在地震损坏的风险,那可能方案二也达不到业务要求。
方案三:对于方案二都无法达到数据落地安全的情况下,更妥当的方案,就是对数据进行异地多机房备份。要实现这个方案,可以简单的使用一致性算法来做。在我们的项目中,使用了raft一致性算法,这个可以不用自己去实现,可以利用现有的开源组件来实现,我们的实践中,使用了百度开源的braft开源方案来写日志。可以选择同步,或者异步方式同步到另外两个节点,具体根本自己的业务场景来选择方案。
当然,并没有一个方案能最好的解决问题,所有的解决方案都对应的成本,只有针对特定的业务场景,选择一个最适合的解决方案,这才是一个一流工程师应该去训练的素养!
问题5:写一段磁盘内容,有没有其他方式可以加速?
方案一:对于问题1中的dd命令测试的过程中,可以再研究一番。我们使用的是命令:time dd if=/test.dbf of=/dev/null bs=8k,表示每次写8k的缓冲区。可以试一下1k,通过比较发现1k的时候,磁盘的写速度测出来比8k的时候小。这比较容易理解,缓冲大一些的时候,磁盘IO系统调用频率比较小,因此速度相对快一些。因此,对业务而言,对磁盘写操作,可以采用合并写的操作方式,加速磁盘IO速度。
方案二:先把要写的文件先创建好。我们知道,如果写文件的时候,文件大小如果改变之后,对于文件系统而言,还需要去修改这个文件对应的系统中的metadata,所以,正常情况下,写文件的时候,其实是需要写磁盘两次,一次是写文件对应的内容,二是修改文件对应的meta信息。因此,可以通过事先的情况下,把需要写的文件事先创建好,再通过往里面写数据,在文件中存储好偏移量,减少磁盘IO的次数去降低耗时。这种方案,在barkerly db写日志的方式中,就采用了。