海量数据存储之存储设计(一)

 

相关文章推荐:

海量数据存储之Key-Value存储简介

海里数据存储之存储设计(二)

Je的排版真的让人难过......

从本文开始着重讲解存储细节,思路比较飘逸,观者多包涵。

翻译了一篇Redis作者antirez文章做为本文的切入点,翻译得不好,这部分可以大致一览,后面会有分析。

Append OnlyReuse Blocks的一些区别

对于一颗append only btree(以下简称AOB)来说,最有趣的属性就是它不可能出现corrupt(可以理解为数据不一致状态)。另外一个有趣的属性就是并发访问没有任何问题,因为无论你访问根节点或其它节点,它们总是一致的(Valid)

但是有一点我不喜欢的,AOB需要一个额外的Compaction(压缩)进程,否则文件会越来越大。如果你的访问需求是绝大部分是读,那么这样是没问题的。但如果你有非常多的写需求,那么问题就来了。

试想一下:如果你的写请求非常大,已经到了磁盘IO的极限,此时你的AOB文件会越来越大,你需要Compaction。但是你已经达到了IO瓶颈,这时你是重新开一个文件还是重写这棵树呢?这些额外的IO将会影响到这棵btree的性能。你能先降低写请求来降低这种冲撞吗?很遗憾不能,另外,rewrite并没有compact这个文件的速度快(大量随机写).

我希望我的系统性能能尽可能的高,因此我不喜欢这样的设计。虽然它在很多情况下可以运转良好,但是我可以用不rewrite的单文件取得更好的效果。

但是这样的情况下,btree可能会很容易出现corrupt,特别是在你不使用fsync系统调用的情况下。磁盘或操作系统可能会重新排序这些写操作,那么假设你要做这些事情:

l  创建一个新节点

l  链接这个节点到某个老的节点

如果在这两个操作之间没有调用fsync,那么有可能写操作被重排序了,那么会可能这个链接被先于节点写入了磁盘。如果在这两个操作之间发生了crash,那么此时你的树是corrupt的(有一个节点找不到它的子节点)。Append onlyreuse blocks之间是矛盾的吗?未必。

在我的实验中,我发现了一个可能可以解决这个问题的办法,虽然它不存在于任何一本算法书中。那就是,如果我的空闲节点有一个保障机制呢(60秒内不被重用)

在这样的情况下,近期的节点将不会被重用。

不幸的是,我们仍然需要更改根节点的指针,那么fsync仍然需要每次调用以保证数据一致。

Append onlyReuse block之间的抉择

毫无疑问,正如每一种NoSQL都有其适用场景,这里每一种访问需求都有不同的答案,针对用户的访问需求做决定和测试产品是唯一正确的选择。

简单来说,antirez的观点就是:

1.         Append only btree不会出现数据不一致的情况。因为它是只追加的,没有重用文件中的失效块。因为在block I/O layer,内核会做一些IO上的调度,以致我们的IO请求会和我们预期的不一致。(FIXME)这一点我后面会开一篇文章详细讲解。

2.         Append only btree的过期数据清理会是很大的问题,很可能会出现compaction永远赶不上write,这样就会一直在做compaction

关于这一点,我曾经在内部邮件里跟人探讨过,过期数据清理如何才能不影响系统性能。

最重要的两点:

a)  数据文件分多文件。在这一点上除了Berkeley DB Java Edition(以下简称JE)我还没有见到哪个NOSQL产品是如此设计的。

b)  Btree的数据结构。这一点其实要略次,不如上一点重要。

 

下面就JEcompaction(官方称clean)操作进行讲解:

目前JE里面的clean操作即这里的compaction,后台clean线程会根据其记录的数据目录下每个文件的utilization(利用率)判断是否需要对该文件执行clean操作,默认的配置是5%,如果某个文件有效数据低于了5%,那么clean线程将会读取这个文件里面的有效数据,append到目录下的最后一个文件(这个操作是可以在内存中的,叫defer write),最后直接删除这个文件,整个流程是可以多线程并行的(每个线程对应当前的若干文件)。另外,最重要的是,因为b+tree的设计读取某个节点需要将其父节点load到内存(父节点维护了到子节点的指针及子节点的key),因此即使进行所有数据的clean,那么只需要其b+tree非叶节点在内存中,你只需要在父节点上删除指向该节点的指针即可,除了最后拷贝文件的那5%的有效数据(这些数据还很有可能在Cache里),基本不会存在任何的io操作(删除文件并不耗时)。以1亿数据量为例,假设单条记录是1kb,默认每个非叶子节点可以有128个子节点,也就是说只要你为JE分配4.5GB内存,那么你的B+tree所有索引(非叶子节点,100w个左右)都可以在内存中了,此时你做clean的话,其IO消耗几乎可以不计了。这一设计在JE中叫Lazy migration

 

看到这里我想大家应该就能明白,对于append only file,为什么多文件的设计会更好,这也算是解决了去年我的海量数据存储之Key-Value存储简介一文中的伏笔。

而选择B+tree而不使用Hash的原因又多了一个-_-

至于Reuse blocks为什么不好,下面给出几条数据:

SSD盘上读、写、擦除操作耗时依次是:

20 microseconds, 200 microseconds, 1500 microseconds

擦除操作是写操作的7倍多。也就是说在SSD上,我们重用块的代价远大于重写节点,这还没有考虑写入放大以及SSD寿命的因素,众所周知,SSD在随机写多的情况下性能损耗非常厉害。

结论,这里虽然append only胜出,但是并不是说reuse block不好,至少innodb的成功证明了它是正确的,而如此选择的前提是,数据如果用纯内存去装太不明智了,因为互联网数据访问是有热点的,在“内存是新的硬盘 硬盘是新的磁带“到来之前,SSD毫无疑问是这个过度时期的最佳选择。

内存使用

不同的NoSQL对内存的使用方法可能不太一样,但是毫无疑问,即使在号称神速的SSD上,读取数据消耗的时间仍然远大于我们的内存,因此,内存中应该放最热的数据。一般来说,内存使用上有以下三种(其实都是殊途同归)。

Row Cache

 

这是大部分Java为开发语言的存储的首选,由于对底层的控制能力有限,因此采用自己实现的缓存可能会更加有效。这时候有人可能会说,由于虚拟内存的介入,这样很可能会出现双重缓存,事实上,我们一般会将机器的绝大部分内存分配给JVM,比如24G的内存分配给JVM16G或者更多,并且在我的实际测试中,部分row cache+系统Cache结合往往比任意单独的cache更有效。

 

MMAP

这是C程序员同学的首选吧,个人对C不熟悉,不做更多评论。但是有一点,MMAP的设计决定了,活跃数据的总量基本不能超过可用内存太大,否则性能急剧下降,系统不停的做swap。另外,Java调用mmap以后,性能监控会失效或不准确了

Purely Cache

纯内存的设计,这种设计本身就定位为缓存,这一点我其实还是希望缓存和后端存储不分离,这样系统结构会尽量简单。这一实现上Redis是其杰出代表(虽然它也有持久的概念)。一般来说,这样的系统,网络会是唯一瓶颈。

大致写这么多,下一篇文章会讲Linux IO的一些细节,包括IO调度,监控以及IO的整个生命周期;再接下来会就事务设计做详细讲解。

 

 

 

 

展开阅读全文

没有更多推荐了,返回首页