首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

存储扩展

Scaling Storage

数据库越来越大,组成表空间的文件也会不断增大,往往达到数百GB。时至今日,磁盘上有一些上百GB的文件不是个大问题——因为可以购买便宜的500GB大小的磁盘——但大多数据库中,文件因素都不重要,即使存在问题需要密切关注的数据库也是如此。随着磁盘读写速度的增加,这些操作失败的几率也变得更高,这是很麻烦的,因为磁盘本身就是最容易出故障的部件。

真正需要的是一些冗余能力。通过配备一些克隆,在机器外部就早有一定程度的冗余,但这些克隆是很昂贵的。如果相比于运行克隆,可以花更少的钱来让每个节点都更加可靠(还有管理故障转移过程),那么通过减少每个节点崩溃的可能性,就可以保有数目更少的克隆。要能够扩展存储平台,那么就需要确保我们能应付所需要的文件数目和大小,同时保持所有事物都有细粒度的冗余。

文件系统

Filesystems

现代文件系统支持在一个目录中存放数十万个文件,而不会带来负面影响。不幸的是,通常Unix下面使用的文件系统不是特别先进的。当在文件系统的一个目录中查找文件时,需要检查文件表找到inode。假设应用程序需要磁盘上的文件/frobs/92363212.dat。首先需要在根命名空间中查找frobs目录。我们遍历root对象下的列表,并且找到这个目录。接着打开这个目录的索引,并且遍历列表,直到找到92363212.dat这个条目,该条目中含有我们所需要的inode。当目录中有很多文件时,就会带来问题。如果目录中有100 000个文件,那么需要遍历100 000个名字,对每个名字执行文件匹配,检查它是否是我们要找的名字。如果在目录中加入更多的文件,情况就会更加糟糕。

要在每个目录中支持很多文件,需要使用一个不同的文件系统,它采用非顺序的方式来查找目录索引,以找到文件条目。基于2.6内核的Reiser和ext3都可以达成这个目标,它们通过对文件名的hash操作,找到相应的inode条目,极大提高了查找速度。随着目录中文件数目增加,用来查找每个文件的条目所花费的时间还相对保持为一个常量。

当工作对象是一组磁盘上的很多文件时,有些文件系统选项可能会引起你的兴趣。Reiser的日志功能可以进行调整,以提高写速度或者恢复速度。如果降低了日志的强度,那么在一次大的故障之后,恢复磁盘时就会碰到困难,另一方面,也可以写性能为代价,确保快速地恢复。

要达到更好的读写性能,在挂载文件系统时加上noatime选项总是不错的注意。atime是访问时间(Access Time)的缩写。默认情况下,Unix会存放文件最后一次访问的时间(对读和写操作皆是),但这意味着每次读操作都伴随着一次小的写操作。写操作往往比读操作代价要昂贵得多,所以为每次读操作添加一次写操作,对性能有很大的影响。noatime选项免去为每个文件记录atime时间戳——除非你明确表明应用程序需要atime的值,所以添加这个选项会很大地提高读写能力。

协议

Protocols

对于任何存储组件,都需要决定如何向它写和从它读文件。对于本地磁盘,这很容易——只需要像通常一样读和写文件系统即可。但是有多台机器共享同一存储的时候,就会出现问题。要写入一个没有关联起来的存储,需要某种接口和协议与存储所在的主机交流(往往称之为the head)。

Unix下和远程磁盘交流的最简单的办法是通过网络文件系统NFS挂载它。NFS让你能像本地磁盘一样挂载远程磁盘,并像本地磁盘一样读写它们。

历史上,NFS在性能和故障转移语义方面都存在一些问题。在前一些版本的NFS中,一台崩溃的服务器会造成Linux下的客户端的一些重大问题。对于“软”挂载,向远程的一台已经崩溃的服务器的写入会被安静地丢弃,而“硬”挂载会将写进程置于“D”状态。“D”状态意味着不可打断的休眠状态,是一种非常特殊的状态,进程用它来表明它们完全不可接触和修改。处于“D”状态的进程是无法被杀死的,唯一的修复办法就是重启机器。死机的远程服务器也无法被解除挂载,甚至它重新上线时也不能重新挂载。客户端机器(或一些机器)都需要重启。使用NFS的intr(可打断)模式可以在服务器重新上线时恢复客户端,但在此之前,客户端会一直保持挂起状态。

不同版本的NFS对服务器不可用的反应各不相同,所以在生产环境中开始使用NFS之前,测试这种场景是非常重要的。如果做不到别的,那么为NFS锁住的情况准备一个恢复计划总是错不了的。

NFS设计的目的是提供访问远程文件系统的能力,并且让它们显得像在本地一样。对很多应用程序而言,这都是很好的,但对于和Web应用相关的文件仓储而言,就有些过头了,对文件存储,常常有的一个典型的需求是写一个文件、读一个文件和删除一个文件,但却从来不在文件内操作、修改和添加。由于NFS提供的内容超出所需,它会带来一些专用协议所没有的额外负荷。对在每个客户端和服务器端之间挂载的每一卷,NFS都保持打开一个套接字,这会显著增加网络的负载,即使是没有任何工作在进行的时候。

Samba怎么样?

NFS的一个常用替代选择是使用Samba,它是SMB(服务端消息块)在Unix下的一个自由实现,它能够访问基于Microsoft Windows的文件共享。Samba避免使用运行于NetBIOS之上的基于数据包的服务广播,从而避免了Windows文件共享带来的低性能。但不管怎么说,Samba和NFS一样,受制于那些相同的问题,比如服务器和客户端崩溃时的处理非常差。但是,它的情况相对好一些,还是值得一试的。

要消除NFS的这些不足,可以使用更加贴合需求的不同的协议。只是要简单地做到放置和删除文件,那么可以使用FTP(或者SCP,出于安全考虑),它去掉了持久化带来的额外负载。对于读操作,可以在存储服务器上使用HTTP。要在我们的应用程序中向其他机器提供文件,可以使用内核版的HTTPD,比如用TUX(http://www.redhat.com/docs/manuals/ tux/),来提供极为轻量级的服务。这让我们可以很方便地用基于标准的HTTP负载均衡系统对文件的读操作进行负载均衡。我们也可以将HTTP用于写操作,使用PUT和DELETE方法。通过使用一份Apache加上一些自制的代码,就能够基于HTTP完成所有的存储I/O。

使用HTTP时不能像把文件系统挂在本地那样方便的读和写了。大多数编程环境都会为我们提供基于HTTP文件的操作方便,减少整合存储到应用程序的工作量。

另一个选择是创建某种自定义的协议和软件。如果需要添加一些自定义的在其他协议中没有的语义,或者在性能方面要求极高,那么这是合理的。对于一个联合文件系统(本章稍后会有涉及),存储协议可能需要返回额外的状态,这些状态是关于文件存储最终位置

的,还有删除操作的同步性等。一个自定义的协议能完成这一类的工作,但也要记得HTTP是一个相当可扩展的协议——我们可以通过添加请求和响应标头来传达特殊的应用需求——特定的语义,这能提供自定义系统的灵活性,同时还享有HTTP的可互操作性。

RAID

如果需要冗余,那就需要数据有多份副本。如果需要容量,那么就希望能够有成队的磁盘组合成更大的磁盘。独立冗余磁盘阵列(或者RAID)就应运而生了,它让我们能够为了冗余性而建立数据镜像,同时又能为了扩展需要而创建大于一个单独磁盘的容量。

RAID提供了很大范围的配置性,能创建不同目的的系统,为了扩展的、冗余的或两者的任意混合。不同的RAID配置被称为模式,每个模式都被赋予唯一的一个数字。当模式之间进行组合时,它们的名字被连接在一起,因此RAID 1和RAID 5一起就被称为RAID 1+5或者RAID15。目前有5种核心的RAID模式(包括常用的混合)在广为使用,如表9-1所示。

表9-1:常用的RAID模式

模  式

描  述

RAID 0

条带化

RAID 1

镜像

RAID 5

带奇偶校验的条带化

RAID 01 (0+1)

先条带化,再镜像

RAID 10 (1+0)

先镜像,再条带化

条带化和镜像是RAID思想的核心。条带化是指对于每一个数据单元(字节、数据块等),将它的一部分数据放到不同的磁盘上。这样在读或写这个数据单元的时候,就可以并行地从多个磁盘读写,获得了更高的性能。读和写时都需要分割或组合这些数据部分,这会带来一些额外支出,但整体的I/O吞吐能力(两块磁盘时是两倍的整体吞吐能力,三块时是3倍,依此类推)使得总体的读写更快。

在镜像设置中,一块磁盘的内容被镜像到另一块磁盘,在其中一块磁盘发生故障时,能提供冗余。镜像设置有时会略微影响写性能,因为需要检查两个副本都已成功写入,也会略微影响读性能,因为需要检查来自两个镜像的内容的校验和异同。这种情况实际上不太可能发生,因为磁盘自带的错误检测和纠正系统会进行相应处理。在理想的设置中,两个磁盘应该都能被读取,从而提供两倍的读能力,但大多数RAID控制器只从其中一个磁盘读取数据。

为了添加冗余,同时不用为了镜像添加双份的磁盘,一些条带系统有奇偶校验条带。奇偶校验数据的原理相当简单,如果我们有一组四个磁盘,加上一个奇偶校验磁盘,每四分之一的数据作为一个单独的条带被写入到四个磁盘的每个磁盘中,并且有额外的一个奇偶校验条带被写入奇偶校验磁盘。奇偶校验条带是其他四个条带的一种特殊的和,在其他条带中的任意一个丢失时,丢失的内容能通过其他条带和这个校验和条带恢复。通过保有这样一个奇偶校验条带,可以恢复任何单一条带的丢失(包括校验条带自身)。奇偶校验模式会带来相当大的写开销,因为作为校验的数据要先行计算。

通过RAID模式的组合,理论上有无限种可能的配置(先条带化再镜像、对奇偶校验条带集进行镜像,等等),尽管还有些模式不如最主要的5种RAID模式那么广为使用的核心模式,如表9-2所示。

表9-2:一些其他的RAID模式

模  式

描  述

RAID 2

位条带化

RAID 3

位条带化,加奇偶校验盘

RAID 4

条带化,加奇偶校验盘

RAID 6

条带化,双内嵌奇偶校验

RAID 15 (1+5)

镜像,条带化,加奇偶校验

RAID 50 (5+0)

条带化,加奇偶校验,再条带化

通过双奇偶校验配置,就像RAID 4,我们为每个数据块保有两份奇偶校验块,在损失两块磁盘的情况下,都能做到不丢失数据。当然我们还能增加更多的校验块,但每增加一个,就相应增加读和写的开销,到某种程度,还不如直接建立数据的镜像。NetApp Filers使用RAID 4的一个变种,允许一组磁盘中的两个磁盘出现故障,而不丢失数据,同时也避免了RAID 10那样的磁盘开销。

RAID可以用软件来模拟,但往往这还是属于硬件RAID控制器的领域。硬件控制器处理RAID集的建立和维护,并把一个RAID阵列作为单个设备向OS展现,在这个设备之上,可以创建多个卷。RAID组完全就像一个独立的挂载上的磁盘一样工作——应用程序完全可以这样看待它。区别在于,就像预料的那样,当RAID中的一个磁盘发生故障时,我们还是可以继续正常运行,就好像没发生这回事一样。

发生故障时,RAID控制器处理故障转移,将工作转交给正常的磁盘,并且当坏的磁盘被替换后,进行后续的重建工作。镜像和奇偶校验的磁盘队列可以完全做到用新磁盘替换坏掉的磁盘,并把新的磁盘填充上数据,尽管这个过程中,读和写的性能都会受到影响。根据控制器的不同,可能需要一些工具来监测磁盘故障和管理驱动器的添加删除。这个检测

工作应该是主要检测设置的一部分,这些内容会在第10章讨论。

想要在多个主机之间共享存储,可以在一台简单的主要的机器上通过SCSI或者SATA挂接一个RAID集,然后这台机器通过NFS或者HTTP提供文件服务,但有时需求增长会超出一台机器所能容纳的范围。对于这种情况,如果尚属于一个机架所能容纳的范围,可以使用所谓的直接附加存储,这时一组磁盘都通过SCSI或SATA之类连接到一台首要机器上。这种方案足以提供一定的磁盘大小和吞吐能力,但缺少一些其他的能力,比如主要机器的在线故障转移和I/O总线。

对于独立的存储集群,我们有不同的选择,可以考虑使用网络附加存储(NAS)或者存储区域网络(SAN)。这两者之间的区别在于,SAN往往使用一个专用的网络用于传输数据(称作存储交换网-storage switch fabric),而且SAN使用块级别的协议来移动数据,而不是文件级别的协议。当你用NFS从一个NAS集群中请求一个文件时,你可能是通过SCSI从SAN中请求一块数据。现代的SAN也能使用iSCSI(基于TCP/IP的SCSI),所以也可以通过常规的以太网建立连接,而不用光纤网。

对于大规模的存储集群,看一看大型存储设备的供货商往往是值得的。NetApp “filers”是一个自包含的存储集群,最高可达100TB磁盘、多首机、多总线冗余、复制软件,还有其他所有你可能需要的好的特性。当然,这些都不是无偿的,但随着所需存储容量的增长,它的成本效益会越来越显著。使用小的1TB的机器作为存储,在一开始很便宜,但随着时间地推移,对这些机器的管理所需对TCO会产生实质的影响。一个单独的filer和一百台Linux机器之间所需管理工作量的对比,使得选用大型设备成为诱人的选择。有几家比较大的厂商制造大型的存储,在作出决定前,有必要先看看它们各自的优点。Rackable的OmniStor系列、SGI的InfiniteStorage系列和Sun的StorEdge就是三个可能的选择。

联合

Federation

如果一个大型的NetApp filer能给你100TB的存储空间(也就是千亿字节),但你还需要更多空间时,你会怎么做?这就是扩展的核心问题——当所需超出当前配置的最大容量时,你能做什么?

正如大型系统的每个组件一样,扩展和冗余的关键都在于拥有具有特定性质的组件,这些组件可以通过简单地使硬件加倍来实现增长。要在存储层实现这个目标,需要按特定的方式构建我们的系统,从而让它能够进行文件存储的联合。和数据库联合一样,我们有多个区段,每个都包含整个数据集的一部分。因为存储有硬性限制,和数据库查询性能不同,它的联合系统的结构和数据库联合有轻微不同。我们不会把所有属于同一个用户的文件保

存在一个区段中,相反,可以把这些文件散布在多个区段中。如图9-18所示,获取这些文件的关键是询问数据库:每个文件的地址(它的区段号)都存放在这个文件相关的记录中。

图9-18:文件分散存放在多个区段

想要增加存储容量,只需要简单的增加更多的区段来增加总体的容量。新加的机器可以是任何大小的,从1U的服务器到100TB的服务器,如果想要,还可以混合这两者。从应用程序的观点来看,它们都只是有特定大小剩余空间的区段而已。

但这确实在各个区段的管理方面给应用程序添加了一些新的需求。每台需要访问存储进行读或写的机器都需要以某种方式挂载所有的区段。对于20台Web服务器和20个存储区段的配置,就需要400个打开的NFS挂载。当有一个区段出问题时,需要能够干净利落地处理好故障转移,确保停止向它写数据。与此同时,还需要处理冗余问题——一台死机的机器不应该造成一部分数据集的丢失。最快的实现冗余的办法就是为每个文件同步写两份或者多份副本,在机器死机时转而从其他区段中读取不同的副本。应用程序还要处理空间不够的情况,即一种特殊的机器“故障”。具体视所用机器和文件系统的不同,你可能遭遇极大的性能降低,分别在磁盘用量达到90%、95%或者98%时应该对磁盘快填满时的磁盘I/O进行基准测试,了解一下所用特定硬件的工作极限。当某个区段达到预设的上限时,把它标记为不可写,这时应用程序要能够正确处理,转而使用有更多空闲空间的区段。

这些是相当复杂而特殊的需求集合,而且很可能不属于应用程序的核心逻辑——我们所在乎的只是能够存储和获取文件而已。如图9-19所示,可以把存储系统抽象成单独的一个层次。给我们的核心应用程序一个简单的接口,用于访问底层联合区段。

图9-19:将存储抽象成为额外的层次

中间件层和Web服务器(或者其他任何需要读或写的地方)之间的接口表示非常简单的文件存储API,它能够存储、替换、删除文件等。存储操作返回使用的区段号,而其他操作则要求提供一个区段号以进行操作。存储层处理所有的空间分配、空间已满的区段的故障转移、死机的区段的故障转移和记录日志以便于恢复出过问题的区段。

这一切都和Google文件系统(GFS)的方法非常相似。有一台中心的主控服务器或者一组服务器,它们负责执行空间分配和提供抽象。我们的数据库扮演元数据集群的角色,存放文件的具体位置和其他的文件相关的数据,而我们所说的区段则类似于“chunk”服务器,是存放真实数据带有冗余性的镜像集合。现在有不少产品提供和这个系统类似的功能,包括MogileFS(http://www.danga.com/mogilefs/),它实现了服务器端和客户端部分的软件,并且使用的是HTTP接口。

缓存

Caching

想要为给定的组件提高性能,可以在它前端添加一个缓存,以减少读请求。这本质上是一个性能问题,但在某种程度上也涉足了扩展的领域,因为这是一种提高组件性能或容量而不用去想方设法把组件设计成可线性扩展的一种方法。如果能够把组件的使用减少5倍,那么就能把流量扩展到原来的5倍,而根本不用去更改这个组件。这对于难以扩展的组件尤其有效——因为它完全绕开了扩展问题(至少能延后),除非再次达到了新的性能或容量的上限。

几乎所有能从中读取数据的组件,都可以使用缓存来扩展。即使缓存无法获得多少速度上的提高(而且对于极为快速的系统,它可能实际上降低了读速度),但通过减少后端的负载和设计一个可扩展的缓存,可以获得容量上的扩展,这是我们的长期目标。因为缓存可以布置在任何层级,所以可以使用很多不同的产品和技术来建立可扩展的缓存。这里只看两种通用的缓存,但请记住,还有其他不少解决方案是成熟可用的。

缓存数据

Caching Data

memcached早就被谈得相当多了。作为一个通用的数据缓存,memcached让我们能够存储任何想要存放的条目,混合的标量值、数据库行集、对象和任何能想到的事物。

memcached和其他简单的内存缓存把大量的缓存的职责置于客户端。要读取一个对象,需要检查缓存,接着有可能需要请求源组件,并把查询结果缓存起来,以备后用。

作为一个通用目的的缓存,memcached可以把应用程序内部的各种数据缓存在一个服务器集群中。使用同一组件为其他数个组件提供缓存之后,只要一次性解决整合问题,就能接着从一个入口处读取和写入缓存内容。这样做简化了设置,也降低了管理成本。我们不采用拥有不同配置和需求的多个集群的做法,改而使用有相同机器的一个独立的集群。这也意味着我们能从硬件中获取更多效益,这在早期预算很紧的时候尤其重要。与其为5个不同的集群(比如,数据库缓存、远程Web页面缓存、临时计算结果缓存等)都准备两台机器,不如使用更少的机器但将它们合并在一起成为一个单独的集群。如果每个缓存需求只用半台服务器就足以支持(另外还需要冗余),后一种方法就能避免浪费大量的机器。按前一种做法需要10台机器,而现在只要4台机器就足以支持所有的缓存,3台足以处理所有5个缓存的总体负载,还有1台用于冗余。通过组合这些缓存,可以把各个小缓存的负载组成一个大的负载,从而降低总体负载,而又不会丢失冗余性。

不仅仅是中间数据可以存放在缓存中,最终处理过的数据也可以。如果应用程序中有些页面需要密集运算,或者很频繁地失效,那么可以把最终的HTML中的片段缓存在memcached中。通常来说,在一个动态程序中,需要在每个页面显示用户特定的数据——至少显示登录状态——但如果缓存页面的不变部分,就可以避免为每次页面请求都重复渲染这些不变的内容。对于采用数种不同的模板输出相同数据的应用程序,我们可以在正要渲染前缓存模板环境。Smarty中,这包括正好在调用display()之前存储$smarty对象。

对于后续的页面请求,可以从缓存中取出对象,激活它,然后调用所需的任何渲染函数。在应用程序的各个层次都要考虑缓存,尤其是在接近最终输出的层次,可以提高缓存的使用率,降低其他组件的成本。

缓存HTTP请求

Caching HTTP Requests

Squid(http://www.squid-cache.org/)同时作为Web缓存代理服务器和反向HTTP加速代理存在。我们所关心的是后一种——作为反向加速代理,它为全部客户端代理一组特定服务器的流量,而不是为一组客户端代理全部服务器的流量。通过添加反向缓存代理,客户端请求会首先寻求缓存命中。如果没有命中,那么缓存直接将请求传递给原来的目的组件。

和大多数内存缓存不同,Squid是完全读穿透的缓存。客户端只需要命中缓存,以得到相应资源的副本,这个副本可能来自于缓存,也可能来自于随机就会被自动缓存的原目标来源。客户端无需添加任何逻辑,这个特性便可以让我们使用Squid为用户代理缓存资源,而不仅仅用于系统内部。来自客户端的请求不知道也不在乎结果是来自于缓存还是来自于实际的后端,它对客户端而言看起来都一样。应用读穿透模型,也可以在内部使用Squid缓存,而不用添加任何缓存检查逻辑,这样可以降低耦合,增加抽象性和简单性,并让我们能够自由换入或换出缓存,而不用对客户端代码做任何修改。

对Squid缓存的读取完全是用HTTP的GET命令控制的,它在需要的时候也潜在触发写命令。要能够明确地从缓存中移除对象,Squid允许PURGE HTTP方法。像执行GET一样执行PURGE,就可以在对象过期或失效之前从缓存中移除这个对象。

PURGE /459837458973.dat HTTP/1.1

Host: cache.myapp.com

HTTP/1.0 200 OK

Server: squid/2.5.STABLE10

Mime-Version: 1.0

Date: Fri, 02 Dec 2005 05:20:12 GMT

Content-Length: 0

这在有一些内容基本不会发生变化的时候尤其有效。可以告诉Squid,这些内容要几年的时间才会过期,这样它就一直保留在缓存中。如果你需要更改或者删除这个内容,从缓存中PURGE它,强迫Squid在客户端再度请求的时候重新缓存它。

在Duane Wessels的书《Squid: The Definitive Guide》(O’Reilly)中,可能找到超出你所需的内容。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论