1.大文件系统背景
对于需要存储大量数据的平台,数据不可能只保存在普通的单个文件或单台服务器中。
原因:
EMC高端存储一台上百万元,但是两台也才不到一百个T,07年以前高端小型存储,后去IOE化(以前系统里面使用了很多IBM的小型机,oracle的数据库),EMC的存储,成本太高,解决不了海量存储的问题。
针对于海量非结构化数据(图片等)的存储,淘宝设计出了一款分布式文件系统TFS,其构筑在普通的linux集群上,可以为外部提高高可靠和高并发的存储访问。
结构化数据一般是存储在数据库中,但是对于图片等非结构化数据一般存储在文件系统中。
2.什么是文件系统
文件系统是一种把数据组织成文件和目录的存储方式。
(1)文件系统接口
文件系统,提供了基于文件的存取接口,并通过文件权限控制访问。
文件系统由操作系统管控,与应用程序交互属于逻辑IO,与磁盘进行交互属于物理IO。
下图为文件系统的层次结构:
(2)文件系统存储单位
对于硬盘来说,扇区是其最小的存储单位。一般每个扇区存储512字节(相当于0.5KB)
磁盘的每一面被分为很多条磁道,一个“圈”就是一个磁道,最内侧磁道上的扇区面积最小,因此数据密度最大。
一个磁道又被划分成一个个的扇区,每一个扇区就是一个“磁盘块”。
对于文件来说,文件存取的最小单位是“块”,其大小最常见的是8*512B = 4KB,即8个连续的扇区组成一个块。
(3)文件系统的访问
操作系统格式化分区时自动将硬盘分成3个区域。
- 目录项区–存放目录下文件的列表信息
- 数据区–存放文件数据
- inode区–存放inode所包含的信息
为什么是这样分区?(扩展)
一个文件对应一个FCB文件控制块,FCB的有序集合称为“文件目录”,一个FCB就是一个文件目录项。 FCB包括其文件的文件名、文件存放的物理地址等。FCB实现了文件名和文件之间的映射。使得用户可以实现“按名存取”。
在没有索引节点时,查找各级目录找到对应匹配的文件名时,需要读出该文件的其他信息,这样会使得目录项区域大小过大。
如:
假设一个FCB大小是64B,磁盘块的大小为1KB,则每个盘块中只能存放16个FCB。若一个文件目录中共有640个目录项,则一共需要占用640/16=40个盘块。如果按照某文件名检索该目录,平均需要查询320个目录项,平均需要启动磁盘20次(每个磁盘I/O读入一块)。
但是如果使用索引节点机制,文件名占14B,索引结点指针占2B,则每个盘口可存放64个目录项,那么按文件名检索目录评价只需要读入320/64=5个盘块。显然,这样将大大提升文件检索速度。
当找到文件名对应的目录项时,才需要将索引结点调入内存,索引结点中记录了文件的各种信息,包括文件在外存中的存放位置,根据“存放位置”即可找到文件。
再补充一下索引节点inode:
索引节点,存储文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。每个inode都有一个号码,操作系统用inode号码来识别不同的文件。
使用ls -i即可查看inode号。
inode节点大小 - 一般是128字节或256字节。inode节点的总数,格式化时就给定了,一般是每1KB或2KB就设置一个inode。在一块1GB的硬盘中,每1KB就设置一个inode,每个inode大小为128B,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。
系统读取文件的方式
左边为没有使用索引节点机制时的系统读取文件,右边为使用了索引节点机制时系统读取文件。
现在一般的操作系统都是使用索引节点机制。
3.海量存储使用小文件存储的缺点
如果选用普通文件存储海量小数据,会引发以下问题:
-
大规模的小文件存取,磁头需要频繁的寻道和换道,因此在读取上容易带来较长的延时。
查找一次文件需要三次寻址换道:
那么如果是海量小文件,不停地寻址换道,会浪费发送几十上百MB数据的时间。 -
频繁的新增删除操作导致磁盘碎片,降低磁盘利用率和IO读写效率。
-
Inode占用大量的磁盘空间,降低了缓存的效果。
4.大文件存储引擎的设计思路
本引擎的核心技术:内存映射和哈希存储引擎。
实现思路:
- 以block“块”文件的形式存放数据文件(一般64M一个block),每一个块都有唯一的一个整数编号,块在使用之前所有用到的存储空间都会预先分配和初始化。
- 每一个块由一个索引文件、一个主块文件和若干个扩展块组成,“小文件”主要存放在主块中,扩展块主要用来存放溢出的数据。
- 每个索引文件存放对应的块信息和“小文件”索引信息,索引文件会在服务启动时映射(mmap)到内存,以便极大的提高文件检索速度。“小文件”索引信息采用在索引文件中的数据结构哈希链表来实现。
- 每个文件都有对应的文件编号,文件编号从1开始,依次递增,同时作为哈希查找算法的key来定位“小文件”在主块和扩展块中的偏移量。 文件编号+块编号按照某种算法可得到“小文件”对应的文件名。
原理图
文件映射
文件映射一般用于进程间共享信息、实现文件数据从磁盘到内存的映射,极大提升应用程序访问文件的速度。
具体可以看后续的另一篇文章:内存映射
1.哈希链表
系统根据索引文件中的文件编号快速定位到相应的主块中的小文件就是使用的哈希链表,每个链表节点中都保存自己本身的文件编号(key)和下一个节点的位置等信息。具体可以看后面索引节点MetaInfo的结构。
2.大文件存储结构图
上面已经说到每一个块都是由一个索引文件、一个主块文件和一个扩展块所构成,具体结构见下图。
在本文中,每次新建一个块,都会有其对应的索引文件类IndexHandle、主块文件类BlockInfo来进行相关的初始化或加载等处理。
3.文件哈希链表实现图(文件哈希索引块)
上面的左边索引IndexHandle图,可以看到前六个可以组成索引头部IndexHeader,后面的每一个文件哈希索引块MetaInfo就是记录对应的小文件在主块中的一些信息。
注意结合上面说到的哈希链表结构,这里前几个MetaInfo实际上是哈希桶,即保存本桶的首个MetaInfo的位置(偏移量)。也就是说如果只有一个小文件,那么哈希桶里面就是保存的这个小文件在索引文件中的偏移位置,如果有多个小文件,就要根据key值(文件编号),定位到对应的哈希桶,然后从哈希桶存的首节点进行查找,直到找到一样key值的MetaInfo则成功。
看看TFS中给的索引文件结构示意图:
关键数据结构与系统函数
1. 块结构:
解释:当前版本号是因为服务器有多台,各个版本可能不一样。
2. 索引节点(即就是日常的文件)结构:
3. 文件映射相关函数:
文件映射mmap函数:
内存磁盘同步msync函数:
重新映射mremap函数:
扩大(或缩小,一般扩大)现有的内存映射
5.大文件存储引擎的实现
内存映射MMapFile类
本类主要做的事情和流程:
可以指定一个文件名,用open系统函数打开拿到fd文件句柄,然后将文件句柄传给内存映射类构造函数进行该类的一些初始化,接着利用map_file进行文件的内存映射操作,并在该函数中利用mmap系统函数拿到映射成功的内存首地址,且注意在该函数中也会利用ensure_file_size进行磁盘的扩容。在映射成功后,还可以测试重新映射方法,同步内容方法和解除映射方法,最后关闭句柄。
mmap_file.h
三个内存初始化构造函数:(一般使用第三个)
- MMapFile();
- explicit MMapFile(const int fd);
- MMapFile(const MMapSizeOption& mmap_size_option,const int fd);
文件映射到内存相关函数:
- bool map_file(const bool write=false);//进行文件内存映射操作,同时设置映射区的保护方式
- void* get_data() const; //拿到映射成功的内存首地址
- int32_t get_size() const;//拿到映射数据的大小
- bool munmap_file(); //解除映射
- bool remap_file(); //重新映射(扩大/缩小现有映射的内存大小)
内存同步到文件磁盘相关函数:
- bool sync_file(); //同步
- bool ensure_file_size(const int32_t size);//磁盘扩容(private)
内存映射的属性
- int32_t size_;//映射数据的大小
- int fd_; //文件句柄
- void* data_;//映射的内存首地址
- struct MMapSizeOption mmapfile_size_option;
struct MMapSizeOption
{
int32_t max_mmap_size; //最大映射大小 8M
int32_t first_mmap_size; //第一次映射的大小 4K
int32_t per_mmap_size; //每次再映射增加的映射大小 4K
};
#ifndef MMAP_FILE_H
#define MMAP_FILE_H
#include<unistd.h>
#include"common.h"
namespace program
{
namespace largefile
{
//设定三个映射类型的大小
struct MMapSizeOption
{
int32_t max_mmap_size; //最大映射大小 8M
int32_t first_mmap_size; //第一次映射的大小 4K
int32_t per_mmap_size; //每次再映射增加的映射大小 4K
};
class MMapFile
{
public:
MMapFile();
explicit MMapFile(const int fd); //避免隐式构造(避免一个参数的,带来歧义)
MMapFile(const MMapSizeOption& mmap_size_option,const int fd);
~MMapFile();
//文件映射到内存-------------------------------------------------------------
//进行内存映射一定要fd
bool map_file(const bool write=false);//进行文件内存映射操作,同时设置访问权限
void* get_data() const;//拿到内存映射成功的这部分数据
int32_t get_size() const;//拿到映射数据的大小
bool munmap_file(); //解除映射
bool remap_file(); //重新映射
//内存映射到文件,即内存内容同步到磁盘-------------------------------------------
bool sync_file(); //同步文件
private:
bool ensure_file_size(const int32_t size);//磁盘扩容
//内存映射的属性
private:
int32_t size_;
int fd_;
void* data_;
struct MMapSizeOption mmapfile_size_option;
};
}
}
#endif
mmap_file.cpp
#include"mmap_file.h"
#include<stdio.h>
static int debug = 1; //如果日志信息太多会影响性能,所以设置开关
namespace program
{
namespace largefile
{
//初始化与析构----------------------------------------------------
MMapFile::MMapFile():
size_(0),fd_(-1),data_(NULL)
{
}
MMapFile::MMapFile(const int fd):
size_(0),fd_(fd),data_(NULL)
{
}
MMapFile::MMapFile(const MMapSizeOption& mmap_size_option,const int fd):
size_(0),fd_(fd),data_(NULL)
{
mmapfile_size_option.max_mmap_size = mmap_size_option.max_mmap_size;
mmapfile_size_option.first_mmap_size = mmap_size_option.first_mmap_size;
mmapfile_size_option.per_mmap_size = mmap_size_option.per_mmap_size;
}
MMapFile::~MMapFile()
{
//如果还有数据
if(data_)
{
if(debug) printf("mmap_file destruct,fd:%d,data:%p,maped_size:%d\n",fd_,data_,size_);
msync(data_,size_,MS_SYNC); //内存与磁盘同步
munmap(data_,size_); //解除映射
size_ = 0;
data_ = NULL;
fd_ = -1;
mmapfile_size_option.max_mmap_size = 0;
mmapfile_size_option.first_mmap_size = 0;
mmapfile_size_option.per_mmap_size = 0;
}
}
//内存映射到文件(异步)是否成功--------------------------------------------------
bool MMapFile::sync_file()
{
if(NULL!=data_&&size_>0)
{
return msync(data_,size_,MS_ASYNC)==0; //同步成功否
}
return true; //没有数据需要同步,也算同步成功
}
//文件映射到内存----------------------------------------------------------------
bool MMapFile::map_file(const bool write)//进行文件内存映射操作,同时设置访问权限
{
int flags = PROT_READ;
if(write)
{
flags |=PROT_WRITE;
}
if((fd_ < 0) && (0 == mmapfile_size_option.max_mmap_size))
{
return false;
}
//对于内存映射的大小设定,这里涉及到硬盘文件与内存大小的同步
if(size_ < mmapfile_size_option.max_mmap_size)
{
size_ = mmapfile_size_option.first_mmap_size;
}
else
{
size_ = mmapfile_size_option.max_mmap_size;
}
//磁盘扩容
if(ensure_file_size(size_) == 0)
{
fprintf(stderr,"ensure file size failed in map_file,size:%d",size_);
return false;
}
//如果成功映射,映射到的内存首地址返回给指针
data_ = mmap(0,size_,flags,MAP_SHARED,fd_,0);
if(MAP_FAILED == data_)
{
fprintf(stderr,"map file failed: %s\n",strerror(errno)); //失败会返回错误编号errno,通过stderror拿到错误编号
size_= 0;
fd_= -1;
data_ = NULL;
return false;
}
if(debug) printf("mmap file successed,fd:%d maped size: %d,data:%p\n",fd_,size_,data_);
return true;
}
//拿到内存映射成功的这部分数据
void* MMapFile::get_data() const
{
return data_;
}
//拿到映射数据的大小
int32_t MMapFile::get_size() const
{
return size_;
}
//解除映射
bool MMapFile::munmap_file()
{
if(munmap(data_,size_) == 0)
{
return true;
}
else
{
return false;
}
}
//重新映射(扩大/缩小现有内存映射)
bool MMapFile::remap_file()
{
//防御性编程
if(fd_ < 0 || data_ == NULL)
{
fprintf(stderr,"mremap not yet\n");
return false;
}
if(size_ == mmapfile_size_option.max_mmap_size)
{
fprintf(stderr,"already mapped max size:%d,now size:%d\n",size_,mmapfile_size_option.max_mmap_size);
return false;
}
int32_t newsize = size_ + mmapfile_size_option.per_mmap_size;
//如果内存空间达到最大值,那么就不能再大了,这里涉及到硬盘文件与内存大小的同步
if(size_ > mmapfile_size_option.max_mmap_size)
{
newsize = mmapfile_size_option.max_mmap_size;
}
//要记得给磁盘也扩容
if(ensure_file_size(newsize) == 0)
{
fprintf(stderr,"ensure file size failed in map_file,size:%d",size_);
return false;
}
if(debug) printf("mremap start fd:%d,now size: %d,new size:%d, old data:%p\n",fd_,size_,newsize,data_);
//重新映射,指向一个新的地址,进行扩容,看flags,反之就是在原来的地方扩容,阔不了就是失败
void* new_map_data = mremap(data_,size_,newsize,MREMAP_MAYMOVE);
//if(MAP_FAILED == mremap(data_,size_,newsize,MREMAP_MAYMOVE))
if(MAP_FAILED == new_map_data)
{
fprintf(stderr,"mremap failed,fd:%d,new size:%d,error desc: %s\n",fd_,newsize,strerror(errno));
return false;
}
else
{
if(debug)
printf("mremap success. fd:%d,now size: %d,new size:%d, old data:%p, new data:%p\n",fd_,size_,newsize,data_,new_map_data);
}
data_ = new_map_data;
size_ = newsize;
return true;
}
//磁盘扩容
bool MMapFile::ensure_file_size(const int32_t size)
{
struct stat s; //文件的状态
//拿到文件的状态
if(fstat(fd_,&s) < 0)
{
fprintf(stderr,"fstat error,error desc: %s\n",strerror(errno));
return false;
}
//获取文件大小,如果磁盘文件小于内存,则磁盘扩容
if(s.st_size < size)
{
//调整文件的大小
if(ftruncate(fd_,size) < 0)
{
fprintf(stderr,"fstat error,size:%ld,error desc: %s\n",s.st_size,strerror(errno));
return false;
}
}
return true;
}
}
}
main.cpp测试内存映射
#include"common.h"
#include"mmap_file.h"
using namespace std;
using namespace program;
static const mode_t OPEN_MODE = 0644; //无符号整数,打开权限参数
static const largefile::MMapSizeOption mmapfile_size_option = {
10240000,4096,4096};//内存映射大小参数 10M 4K 4K
int open_file(string file_name,int open_flags)
{
int fd = open(file_name.c_str(),open_flags,OPEN_MODE); //打开方式参数open_flags
if(fd < 0)
{
return -errno; //返回负数
}
return fd;
}
int main()
{
const char* filename = "./mapfile_test.txt";
//1.打开/创建一个文件,取得文件的句柄open
int fd = open_file(filename,O_RDWR|O_CREAT|O_LARGEFILE);
if(fd < 0)
{
//这里不用errno的原因:怕被覆盖,比如这里前面还有个read,出错则会重置errno
fprintf(stderr,"open file failed. filename:%s,error desc:%s\n",filename,strerror(-fd));//负负地正
return -1;
}
puts("创建fd成功");
//进行内存初始化
largefile::MMapFile* mmapfile = new largefile::MMapFile(mmapfile_size_option,fd);
puts("内存初始化成功");
//进行内存映射,设为可写
bool is_mapped = mmapfile->map_file(true);
puts("内存映射成功");
//映射成功
if(is_mapped)
{
puts("准备去分配内存空间");
//测试重新映射
mmapfile->remap_file(); //本来4K变成8K
//分配空间
memset(mmapfile->get_data(),'8',mmapfile->get_size());
//同步内容
mmapfile->sync_file();
//解除映射
mmapfile->munmap_file();
}
else
{
fprintf(stderr,"map file failed\n");
}
puts("成功");
close(fd);
return 0;
}
common.h公共头文件
为了健壮性更好,设置公共头文件
#ifndef _COMMON_H
#define _COMMON_H
#include<iostream>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h> //msync
#include<string>
#include<string.h>
#include<stdint.h> //int32_t
#include<errno.h> //errno stderr strerror
#include<stdio.h> //fprintf
#endif /*_COMMON_H*/
内存映射效果
从图中可以看到,初始分配的内存空间大小是4096即4K,起始地址是0x7fd95db5c000,
测试重新分配时,内存大小增大4K,重新到另外一个起始地址映射,而不是在原来的起始位置继续追加,根据mremap(data_,size_,newsize,MREMAP_MAYMOVE)参数设定。
最后文件mapfile_test.txt存在设定的内容。
文件操作FileOperation类
本类主要做的事情和流程:
指定一个文件名,进行文件操作对象的初始化,利用open_file函数中的open系统函数根据文件名,拿到文件句柄fd,接下来创建一个大小65字节buf,不断用buf来测试读写操作。先将buf除了结束符的64字节全置为6,写到文件的1024位置,然后将buf置空,从文件的1024位置读64字节到buf中,此时打印buf的内容应该都是6,然后再将buf全置为9,直接写入文件,会将文件的头64个字节都置为9。
file_op.h
文件操作初始化构造函数与析构函数:
- FileOperation(const std::string &filename,const int open_flags = O_RDWR|O_LARGEFILE); //记得参数的LARGEFILE
- ~FileOperation();
打开关闭文件:
- int open_file();
- void close_file();
保存删除文件:
- int flush_file(); //write方法操作系统会把文件写入缓存到内存,所以该方法将文件直接写到磁盘
- int unlink_file();
读写文件:
- virtual int pread_file(char* buf, const int32_t nbytes,const int64_t offset); //大文件,所以64位比较好。 从offset位置读nbytes到buf中
- virtual int pwrite_file(const char* buf,const int32_t nbytes,const int64_t offset); //从buf中读nbytes到offset位置
- int write_file(const char* buf,const int32_t nbytes);//在当前位置直接开始写
拿到文件大小:
- int64_t get_file_size();
截取文件:
- int ftruncate_file(const int64_t length);
文件内容定位:
- int seek_file(const int64_t offset);
拿到文件句柄:
- int get_fd() const;
查看文件是否打开,未打开则偷偷打开(比如拿到文件大小)(protected):
- int FileOperation::check_file()
文件操作的属性
- int fd_; //拿到文件描述句柄
- int open_flags_;//文件打开方式(读写、不存在则创建等)
- char* file_name_; //文件名
- static const mode_t OPEN_MODE = 0644; //文件打开方式用户权限
- static const int MAX_DISK_TIMES = 5; //读取文件失败,尝试5次就不读了(可能负载过高也可能磁盘出问题等)
#ifndef LARGE_FILE_OP_H
#define LARGE_FILE_OP_H
#include"common.h"
namespace program
{
namespace largefile
{
class FileOperation
{
public:
FileOperation(const std::string &filename,const int open_flags = O_RDWR|O_LARGEFILE);
~FileOperation();
//打开关闭
int open_file();
void close_file();
//保存删除
int flush_file(); //write方法操作系统会把文件写入缓存到内存,所以该方法将文件直接写到磁盘
int unlink_file();
//读写
virtual int pread_file(char* buf, const int32_t nbytes,const int64_t offset); //大文件,所以64位比较好
virtual int pwrite_file(const char* buf,const int32_t nbytes,const int64_t offset); //seek
int write_file(const char* buf,const int32_t nbytes);//在当前位置直接开始写
//拿到文件大小
int64_t get_file_size();
//截取文件
int ftruncate_file(const int64_t length);
//文件内容定位
int seek_file(const int64_t offset);
int get_fd() const
{
return fd_;
}
protected:
int check_file();
int fd_; //拿到文件描述句柄
int open_flags_;//文件打开方式(读写、不存在则创建等)
char* file_name_; //文件名
static const mode_t OPEN_MODE = 0644; //文件打开方式用户权限
static const int MAX_DISK_TIMES = 5; //读取文件失败,尝试5次就不读了(可能负载过高也可能磁盘出问题等)
};
}
}
#endif
file_op.cpp
#include "file_op.h"
#include "common.h"
static int debug = 1;
namespace program
{
namespace largefile
{
FileOperation::FileOperation(const std::string &filename,const int open_flags):
fd_(-1),open_flags_(open_flags)
{
//注意有分配内存
file_name_ = strdup(filename.c_str());//字符串复制,相当于重新分配一块内存,把内容复制过去
}
//关闭文件句柄,清空分配的内存
FileOperation::~FileOperation()
{
if(fd_>0)
{
::close(fd_); //说明是全局的不属于当前作用域,直接调用库文件中的函数
}
if(NULL!=file_name_)
{
free(file_name_);
file_name_ = NULL;
}
}
//打开关闭------------------------------------------------------------------------
int FileOperation::