Hadoop分布式文件系统(HDFS)设计

Hadoop分布式文件系统是设计初衷是可靠的存储大数据集,并且使应用程序高带宽的流式处理存储的大数据集。在一个成千个server的大集群中,每个server不仅要管理存储的这些数据,而且可以执行应用程序任务。通过分布式存储和在各个server间交叉运算,集群和存储可以按需动态经济增长。以下的设计原则和经验是根据yahoo通过HDFS管理的40PB得来的。

1. HDFS简介

HDFS是一个分布式文件系统,并且为MapReduce分布式算法提供了一分析和传输大数据的框架。HDFS使用java编写,它的接口是对unix文件系统接口的更高层抽象,从而提高应用程序的性能。

HDFS的重要特点是如何分割数据、如何在成百上千的主机上完成交叉计算(找到数据,完成计算)以及如何让计算尽可能的“接近”所需的数据(理想是计算和数据在一台server上,此时性能最高,负责要承担传输数据的消耗)。Hadoop集群通过简单的增加普通的server机器来提高计算能力、存储能力和IO带宽。Yahoo hadoop集群有4000台机器,存储了40PB数据。

注意:HDFS上元数据(文件系统管理数据)和应用程序数据是分开的。如果有单机文件系统的经验,HDFS的元数据可以理解为磁盘inode文件和目录的组织信息,只不过此时HDFS存储的是“此范围数据在serverA上,下一段数据在serverB上, 。。。”。和其他分布式文件系统PVFS,GFS类似,HDFS将元数据存储在带有备份的server上,我们一般把这台server叫NameNode, 应用程序数据就是普通数据我们把这些数据存储在DataNode server上。所有的NameNode和DataNode server通过基于TCP的协议进行连接通讯以及信息交换。与Lustre和PVFS不同的是HDFS datanode上的数据不是通过RAID机制进行保护的,它采用的方法与GFS相同 - 通过将数据文件内容在多个DataNode上进行备份来完成可靠性保护(具体一份数据在几台机器上复制是可配置的)。这种可靠性保护同时增加了数据在传输时的带宽,并且有利于让计算所在server找到距离它更近的datanode server,从而提升计算速度。

2. HDFS体系结构

2.1 NameNode

HDFS的命名空间就是HDFS中所存储的文件和目录体系结构。在NameNode中,文件和目录同样用inode代表。Inode记录了文件的属性信息,如权限、修改和访问时间,命名空间和磁盘空间配额。一个文件的内容被分成很多大的数据块(典型块大小是128M,用户可自行配置),文件的每一块都在多个DataNode上进行备份存储(典型值是3台机器备份,用户可配置)。

NameNode核心作用是维护命名空间树并完成文件块到所在DataNode servers的映射。当前的HDFS设计在一个cluster中仅包含一个单独的NameNode。但是,一个cluster中包含数以千记的DataNode server和更多的HDFS client,而且一个datanode可以并发的执行多个应用程序任务。

2.2 镜像和日志(Image and Jornal)

inode和连接的所有的块信息组成了HDFS的元数据系统,这就是所谓的镜像。NameNode将整个命名空间的镜像报存在RAM(内存)中,但ram中的数据会在断点后消失。所以将镜像永久的存储在namenode的本地文件系统中,这里叫做checkpoint(checkpoint是内存镜像的某个时间点的一个copy,所以涉及到checkpoint的时间点问题,后文描述。)任何对HDFS命名空间的改变都要先于操作记录在本地文件系统中,这些记录叫做日志。注意:每个块数据所在的多个datanode位置信息不包含在永久的checkpoint中。因为这些位置是动态改变的,且datanode在启动时会主动联系namenode汇报信息。

任何client初始化的事务操作在返回应答之前都需要先刷新同步(flush&synced)到本地磁盘上的日志文件中。但是,NameNode从不改变Checkpoint文件。 如果管理员和CheckPointNode(后文描述)要求,一个新的checkpoint文件会在Hadoop系统启动时被重新创建。在namenode启动时,命名空间镜像从checkpoint copy,然后重新执行jorurnal文件的每个条目完成最新元数据的创建。同时,一个新的checkpoint和空的日志文件在namenode真正工作之前被创建。

为了提高可靠性,一般会保存多份checkpoint和日志文件在不同的独立磁盘卷上和NFS网络文件系统上。这样,一个磁盘损坏和整个namenode损坏都不会影响HDFS的元数据。为了更好的保护最珍贵的元数据信息,如果namenode在向任何一个上文提到的目录写日志失败时,namenode将会将损坏的目录删除。如果没有存储目录可用,namenode将自动关闭,从而HDFS关闭。

Namenode是采用多线程的来处理多个客户端的并发请求,如上文所属,所有客户端的事务请求在返回前都要刷新同步到本地磁盘日志中,这将成为系统的一个瓶颈点,因为其他线程也将在等待磁盘的IO返回。为了优化这个瓶颈,namenode将相同的同步刷新操作进行批处理操作,而不是每个线程分别刷新一次。

2.3 DataNode

 在本地文件系统的Datanode上,一个复制的块有两个文件表示。一个文件包含数据本身,另外一个文件记录块的元数据信息,包括数据块校验和和块产生时的时间戳。数据文件的大小等于块中数据的实际大小,而不像传统文件系统一样在不满一个磁盘块时要占满磁盘块。

在系统启动阶段,每一个DataNode向Namenode主动发送握手信息。握手的主要目的是验证datanode的命名空间ID软件版本是否和Namenode一致。如果任何一项与Namenode不符,DataNode将自动关闭退出cluster。因为它的数据已经不再有效。

当HDFS格式化时会自动分配一个命名空间ID,这个命名空间ID被cluster中的每个节点永久保存。不同的命名空间ID不能加入到cluster中,所以命名空间ID很好的保证了分布式文件系统的完整性。当然,一个最新初始化的还没有命名空间ID的datanode节点允许加入cluster,但需要永久的保存当前文件系统的命名空间ID。

在与Namenode握手注册之后,每个datanode接收并永久保存一个独一无二的的存储ID。存储ID是一个内部的标示符,这样当datanode以一个不同的IP地址启动时,namenode利用这个存储ID来识别该datanode。再次强调,存储ID是在datanode第一次和namenode注册时分配的,并永远不会改变。

Datanode通过向NameNode发送块报告信息来标示其所拥有的本地数据块副本。一个块报告包含块id信息、创建时间戳信息以及每个块长度信息。Datanode在namenode上注册自己之后,立刻第一次发送快报告信息。后面每个一小时发送一次块报告信息,NameNode通过块报告信息标示datanode存储数据块的最新状态。

正常操作情况下,DataNode会定期给Namenode发送心跳包信息。通过心跳包信息,Namenode知道DataNode和其上报告的块副本是可用的。默认的心跳间隔时间是3秒钟。如果Namenode在十分钟内没有接收到某一个datanode的心跳包信息,Namenode认为该datanode server已经down掉,所以它上面存储的数据块也将不可用。为保证每个数据块的副本数(默认3),namenode将开始调度其他datanode来赋值原本存在于down机datanode上的数据块。

Datanode的心跳包信息还包括本机的存储总容量、已使用容量和正在传输的数据大小。这些信息将被NameNode用于分配和load balance考虑。

Namenode从来不直接给DataNode发送请求,它利用心跳包的回复信息来给datanode发送指令。指令可能包括,复制本地的某一块到另外一个datanode、移除本地的某一块数据、重新注册、立刻发送快报告信息和关闭等指令。以上的这些命令信息对于维护整个文件系统的完整性至关重要,所以即使在大型的cluster集群中,频繁的心跳包信息依然是必须的。NameNode可以每秒处理数千个心跳包信息而不影响其他的NameNode操作。

2.4 HDFS client客户端

 HDFS客户端是一个提供了HDFS文件系统接口的库,用户应用程序通过HDFS client来使用HDFS文件系统。和传统文件系统相似,HDFS提供了文件的读、写和删除,目录的写和删除。用户通过命名空间的路径来操作具体的文件或目录。应用程序不需要知道某一数据块有多少份拷贝、存在那台server上以及文件系统的元数据信息。

当应用程序要求读文件,HDFS客户端首先向Namenode询问保存该文件各块及其副本的datanode列表信息。通过这个列表信息,HDFS client可以找到文件中各块所在的datanode。并且,这个列表中的datanode是按照与客户度的网络拓扑距离进行排序的,这样有助于client查找最近/优的datanode加快传输数据速度。拿到列表后,client直接与选择的datanode通信来传输需要的数据块。当要写数据的时候,client询问Namenode已获得一个datanode列表来存储第一块数据,当拿到这个datanode列表后,client将这个列表以管道的形式进行组织,然后完成第一块数据的写入。然后再次询问namenode来获取datanode列表以存储第二块数据,拿到列表后依然以管道形式组织写入数据。以此类推,知道所有块数据都写完。此时,每一块数据都按照之前的设置有n份拷贝在不同的datanode上。具体的示意图如下所示:

与传统文件系统不同的是,HDFS提供了一个API来返回一个文件的各块所在的位置。这个API可以帮助像MapReduce这样的框架来调度任务,即可以将任务放在所要操作的数据相同的datanode上以提高读的性能。HDFS也提供了由应用程序来设置副本数的功能,默认副本数是3。可以对经常访问或重要的文件设置更大的副本数,从而增加容错性和读的带宽性能。

2.5 CheckPointNode

HDFS中的NameNode除了主要用于服务Client请求和文件命名空间树管理外,它还可以在启动时选择以下两个功能之一CheckpointNode or BackupNode。

CheckpointNode定期的将当前存在的checkpoint和日志文件合并以产生一个新的checkpoint和空的日志文件。一般情况下checkpointNode和Namenode运行在不同的机器上,因为这两个node要求相同的内存(因为checkpoint合并日之后与当前内存的镜像是相等的)。通常情况下,checkpointnode从namenode下载checkpoint和日志文件,然后在本地merge,然后将checkpoint文件返回给namenode。

创建周期性的checkpoints是保护系统元数据的一种方式,如果其他所有的永久性镜像和日志都不可用,系统可以从最近的一次checkpoint启动。创建新的checkpoint也使日志文件清零。我们知道如果系统长期运行或者cluster中机器较多的话将导致日志文件变得很大,从而影响启动时间。建议每天创建一个checkpoint,这样日志文件不至于很大。

2.6 BackupNode

BackupNode是最近引入的一个功能,它可以像CheckPointNode一样周期性的创建checkpoints,而且BackupNode在内存中维护了一个和NameNode同步并实时更新的文件系统命名空间镜像。

BackupNode接收来自namenode的日志事务流,并将它们保存在本地存储中,并且将这些日志事务merge到其内存中的命名空间镜像。在Namenode看来,BackupNode就想是它本地存储的日志文件一样。如果Namenode失败,BackupNode的内存镜像和checkpoint记录了几乎同步的最新命名空间状态。

BackupNode不用从namenode下载checkpoint和日志文件就可以创建新的checkpoint,因为它自己在内存中已经保存了最新的命名空间镜像。这就是Backupnode创建checkpoint非常简单化,直接将内存空间镜像保存到本地存储代替当前的checkpoint并清空日志即可。

Backupnode可以看做是只读的NameNode,它包含了文件系统除块位置之外的所有元数据信息。它可以执行除更改命名空间和要求块位置之外的所有操作。

2.7 升级和文件系统快照

文件系统升级可能由于软件bug或者认为错误导致整个文件系统损坏。HDFS快照的作用就是减少在升级过程中对数据存储带来的危害。快照机制可以让系统管理员永久性的保存当前系统的状态,所以如果在升级过程中导致数据丢失或者损坏,系统管理员可以回滚到快照时状态。

在文件系统启动时管理员可以选在是否创建快照(系统中只能有唯一一个快照)。如果选择创建快照,Namenode首先读checkpoint和日志文件并merge到内存中,然后namenode写新的checkpoint和日志到另外的位置。所以旧的checkpoint和日志文件是未改变的,如果发生升级错误,可以立刻回滚。

在握手期间,NameNode指示DataNode是否创建一个本地快照。Datanode不是简单的复制目录中的数据文件,因为这样会导致两倍的存储空间。Datanode只是增加已存在文件的硬链接树。如果移除文件,硬连接数减一,文件仍然未被删除。如果是写文件,则采用copy-on-write技术。

HDFS系统管理员可以在重启系统时选择回滚到快照。如果选择回滚,Namenode将从快照时刻点保存的checkpoint和日志文件开始启动。Datanode将启动一个后台进程将在快照点后生成的块删除,并将之前保存的块恢复存储。注意:一旦回滚将没有机会恢复到回滚前。系统管理员也可以删除快照以释放期所占用的存储空间。删除升级前产生的快照是文件系统软件升级的最后一步。

系统的不断升级发展有可能导致NameNode上的checkpoint和日志文件格式变化,或者datanode上的块数据副本表示的变化。在Datanode和Namenode的存储目录中永久的保存了一个版本格式标识文件。在每个node启动时,它们自动比较当前的软件版本和自己机器上存储的软件版本,如果不相同,它们自动将旧的数据格式转变成新的。这个转换要求在新的软件布局版本启动时手动的创建快照。

8.3 文件的I/O操作和副本管理

 文件系统的主要作用是在文件中存储数据,为了更好的理解HDFS是如何存储数据管理数据的,我们下面一起来看一下数据的读写和块管理的设计

8.3.1 文件读写

HDFS基于应用场景考虑,实习的是单次写多次读模型。具体理解为:HDFS client通过创建并打开一个新文件来向文件写数据,当这个文件被关闭后。写入文件的数据是不能被修改或删除的。但是文件可以通过append形式的reopen来向文件末尾添加数据。

HDFS是通过lease租约来管理并发的文件读写操作的。在HDFS申请打开一个文件的时候,NameNode同时返回给client一个lease,这样其他的client就不能再向这个文件写数据了。当前持有lease的client需要通过心跳包向NameNode周期性的续租lease。客户端在close文件时持有的lease也同时被消除,这样其他的client可以reopen这个文件进行附加写操作了。Lease持有期间有两个限制--软限制和硬限制。在软限制过期之前,client可以排他性的获取文件。如果在软限制过期之前client没有关闭文件且没有向NameNode续租lease,另外的client可以抢占这个lease,这意味着之前的client不能向文件进行写操作。如果在硬限制(一个小时)过期之前client没有续租lease,HDFS假定client已经退出,DataNode会自动的关闭文件并消除lease。注意,写lease不会阻止其他client读文件,一个文件多数情况下会有多个并发client作为readers。

一个HDFS文件是由数据块组成的,当需要创建新数据块时,NameNode首先会分配一个独一无二的数据块ID标示符(一个数字),然后选择决定由那几个DataNode来存储这个数据块。被选择出的DataNode按照最小网络距离的形式组织成管道线,数据以数据包的形式被写入管道线。具体为:应用程序在client端向buffer写数据,当buffer填满后(典型64k),这个buffer会被发送给管道线的第一个DataNode,然后管道线顺序写到最后一个DataNode。在未得到本次写确认(所有DataNode都已写完)之前,如果未决数据包个数小于最大值,客户端可以直接写下一个数据包。

如果按照上面的形式写数据,在文件关闭之前,HDFS是不保证其他的reader可以读到之前写入的数据。如果应用程序需要这种保证,可以调用hflush操作,这个操作可以想象成普通文件系统的flush操作,只不过hflush刷新的是DataNode管道线。所以,当hflush返回后,所有在它之前调用的写数据操作都已经写到DataNodes中,reader可以安心的读了。

 如上图所示,如果没有错误发生,块构建过程经历了上面3个阶段,上图示例了三个DN的管道线流式写了5个数据包。图中的加粗黑线表示数据包,虚线表示从DN返回的确认message,正常实线表示用于构建和关闭管道线的控制命令。竖着的线表示随着事件变化,clent和3个DNs的行为动作。t0 - t1期间,管道线被建立。t1 - t2期间,是数据包的传输阶段,第一个数据包在t1被发送,最后一个数据包的确认信息在t2时刻返回。注意,图中未标识出在数据包2时发送的hflush操作,因为hflush操作和数据包一起发送,而不需要一个单独的命令流。 t2 - t3期间,块的管道线被关闭。

在一个包含数千个节点的cluster中,一个节点的错误(经常是存储出错)是经常发生的。在一个DN上的副本存储可能由于内存、硬盘或者网络的错误导致数据失效。HDFS使用数据校验和来保证每一块数据的有效性。client在读取数据后会领用checksum校验和来判断数据的有效性。当client在创建一个文件时,它计算没个块文件的校验和,并将这个校验和和数据一起发送给客户端,DN将校验和校验和存储在元数据文件中。注意:元数据文件和数据文件在DN上是分开存储的。当HDFS读数据时,DN将校验和和数据一起发送给client,client接到数据后对数据计算校验和,并与接收到的原来的校验和进行比较,如果结果不相同,说明数据已经损坏。这时client会通知NameNode,然后从另外一个DN副本中重新获取文件。

当一个client打开一个文件进行读之前,它从NameNode获取该文件所有的块列表,以及每个块副本所在的DN位置信息。每一个块的位置信息是按照和该client的距离进行排序的。开始读文件时,client先从距离它最近的块副本所在DN开始,如果读取失败,client会依次尝试相同块所在的后续DN。一个读操作可能再下面三种情况下失败,目标DN不可达、目标DN不在存储所要求的副本和块副本校验和失败。

HDFS允许以写的方式打开文件。当以写的方式打开文件时,NameNode是不知道当前要写的最后一块的长度的。这种情况下,client需要从最新副本中获取长度。

HDFS的I/O设计针对批处理系统做了特别的优化。例如:MapReduce。这些系统的显著特点是要求序列化读写的高吞吐量。JDFS后续版本的改进目标在于提高读写操作的实时性和随机读写。

8.3.2 块布局

对于一个大型cluster而言,让所有节点线性部署是不明智的行为。实践证明,最明智的做法是将所有的节点交叉的放置在不同的机架上。一个机架上的所有节点共享一个交换机,所有的这些交换机连接到核心交换机,通过交换机将这些机架以及之上的节点连接起来。两个节点之前通信通常需要通过多台交换机。通常情况下,同一机架上的两个节点通信速度要快于不同机架通过交换机通信。下图示例了一个cluster中包含2个机架,每个机架中包含3个节点:

HDFS通过两个节点的距离来估计网络传输带宽。一个节点到它父节点的距离假设为1,两个节点间的距离是两个节点到他们公共父节点的和。如果两个节点之间的距离越短,HDFS认为其具有更高的网络传输带宽。

系统管理员可以通过一个脚本从一个节点的位置来获取其所在的机架标识。NameNode负责解析一个DN机架位置信息。当一个DN注册到NameNode时,NameNode运行之前配置的脚本来判断当前注册的DN属于哪一个机架。如果脚本没有被配置,NameNode认为所有的节点都属于一个默认的机架。

块副本的布局对于HDFS的可靠性以及读写性能至关重要。好的块布局策略可以提高数据可靠性、可用性以及网络带宽的使用。当前HDFS提供了一个可选择配置的块布局策略接口,用户或者研究人员可以通过策略接口测试不同策略,从而优化他们的应用程序。

默认的HDFS块布局策略在最小化写cost、最大化数据可靠性、可用性以及聚合的读带宽上做了权衡取舍。当一个新块被创建时,HDFS将第一个块副本创建在与Writer client相同的DataNode。第二份和第三份块副本将被放置在另外两个不同机架的两个DN上。其他的副本在满足下面两个条件的DN上随机放置。条件为:任何两个相同副本不能在同一个DN上;同一机架上的DN不能多于两个相同的副本(副本不要都存在一个机架上,否则机架down机,所有副本全部失效。)以上的这些分布策略就是为了保证数据的安全性和带宽。一个单独的机架失效不会影响整个cluster,因为副本分布在不同的机架上。

在所有目标DN节点选择之后,节点以接近第一个副本的顺序排序。如前文所属,数据按照这个管道线顺序依次写入各个节点。对于读操作,NameNode首先检查客户主机是否在cluster中,如果client在cluster中,NameNode将按距离client近的顺序组织副本节点位置返回给client。

上面的策略减少了在写操作时机架交互和节点本身交互,从而提高了写操作的性能。因为机架失效的概率要远低于节点失效,这个策略在可靠性和可用性上所做的保护很有限。在最常见的每块三个副本的条件下,这个策略可能减少网络带宽,因为一个块数据分布在两个不同机架上,而没有分布在3个机架上。

8.3.3 副本赋值策略

NameNode管理和保证每一个块总是具有最初配置的副本数。NameNode从一个DataNode的块报告中检测每一个块的副本总数量。当一个块的副本数量多于配置数时,NameNode选择移除一个副本。移除策略是:尽量不减少存有副本机架的数量(在同一机架上选择一个节点副本移除);选择一个剩余可用空间最小的DN,移除其上的多于副本,从而在不影响块的可用性下平衡不同DN上的磁盘利用率。当一个副本的数量小于配置的副本数时,这个块将被放置在待复制优先级队列中,一个只拥有一个副本的块优先级最高,拥有两个或者两个以上副本的块优先级较低。一个后台线程周期性的扫描这个优先级队列头,它按照下面的规则(与首次创建的策略基本相同)创建新的副本从而满足每一个块的副本数:如果当前只有一个副本,下一个副本将会在另外一个机架上创建,如果当前有两个副本,且这两个副本在同一个机架上,第三个待创建副本仍然后选择一个不同的机架。否则,第三个副本将在同一个机架的不同DN上创建。这里的总目标是减少创建新副本所带来的cost。

NameNode也需要判断是否一个块的所有的副本都在同一个机架上,如果发现这种情况,NameNode会认为这个块的复制不正确,从而应用上面的策略将副本复制到不同的机架上。当NameNode接收到复制完成的确认后,NameNode会按上面的移除策略从先前同一机架上的副本中移除一个。

8.3.4 Balancer

HDFS的块副本布局策略并没有过多的考虑DN的磁盘利用率问题。这个可能导致数据块只在极小的数据块子集中复制,而其他大量DN还有很多存储空间可用。不平衡可能再新加入cluster的节点上发生。

Balancer是一个用于在HDFS cluster中平衡磁盘利用率的工具。它有一个输入参数,这个参数可以在0-1间取值,是一个阀值。检查每一个DN磁盘利用率,如果某一个DN的磁盘利用率与cluster的总磁盘利用率之差大于这个阀值,Balancer就会开始做平衡工作。

Balancer以应用程序的形式部署,系统管理员可以运行该工具。它工作时,按照上面的阀值原理,迭代的将高磁盘利用率的副本复制到低磁盘利用率的副本。这个工具在工作时还有一个重要的需求 - 保证数据的可用性。所以当他选择复制目标DN时,保证不能减少副本的数量,和副本所在机架的数量。

Balancer在balance是对于机架间的拷贝做了优化处理。如果balancer决定将副本A移到一个不同机架的DN,而目的机架上有一个相同的块副本B,数据将从B直接拷贝到目的DN。从而避免了机架间的数据复制。balancer的带宽消耗也是可以配置的,分配给balancer越高的带宽,cluster会以更快的速度完成balance。但高带宽将导致balancer与应用程序的带宽竞争。

8.3.5 块扫描(block scanner)

每一个DN都会在本机定期的运行快扫描程序,这个程序检测本机的块副本数并验证每个块的数据校验和是否正确。在每个扫描周期,块扫描器都调整其读带宽速度以在规定的周期时间内完成验证的全部工作。如果客户端读了一个完整的块,并且认为校验和是正确的,client会通知DN。DN将这种通知也作为该副本验证通过的依据。

每一个块的验证时间以可读的形式存在于文件中。任何时候再DN的顶层目录中都有两个文件,分别是当前的logs和先前的logs。新的验证时间存放在current logs文件中。对应的,DN内存中有一个按照副本验证时间排序的扫描列表。无论何时client和快扫描器发现一个坏的数据块,DN都要通知NameNode。NameNode将该DN上的副本标记为损坏,但是不立即删除,而是立即启动复制一个好的副本。只有在好的副本数达到配置数时,坏的数据块才会调度删除。这个策略的目标是尽可能长的保存数据,所以,即使这个块数据副本已经损坏了,这种策略也是允许用户检索的。

8.3.6 关闭操作

Cluster管理员可以指定关闭的node列表。一旦一个node被标记成关闭,它将不会被选择为副本目标复制节点。但是这个node还是会继续为读操作服务。NameNode会调度将其上的块副本复制到其他的节点上。当NameNode检测到所有在待关闭节点上数据都已完成复制时,该节点才会真正进入关闭状态。这时可以安全的移除该台机器。

8.3.7 cluster内部数据拷贝

在HDFS中拷贝或者拷出大数据集文件是一个让人望而生畏的工作。HDFS提供了一个叫做DistCp的工具用于大型数据集在Cluster或者Cluster间并行拷贝。这个工具是一个MapReduce job,每一个Map任务拷贝部分源数据到目标文件系统。MapReduce宽肩会自动的处理并行任务调度、错误检测和恢复等问题。

转载注明出处,谢谢:

posted on 2014-10-17 03:55  Stephen_init  阅读(904)  评论(0编辑  收藏  举报