【史上最全】Hadoop 核心 - HDFS 分布式文件系统详解 (上万字建议收藏)
1. HDFS 概述
Hadoop 分布式系统框架中,首要的基础功能就是文件系统,在 Hadoop 中使用 FileSystem 这个抽象类来表示我们的文件系统,这个抽象类下面有很多子实现类,究竟使用哪一种,需要看我们具体的实现类,在我们实际工作中,用到的最多的就是 HDFS(分布式文件系统)以及 LocalFileSystem(本地文件系统)了。
在现代的企业环境中,单机容量往往无法存储大量数据,需要跨机器存储。统一管理分布在集群上的文件系统称为分布式文件系统。
HDFS(Hadoop Distributed File System)是 Hadoop 项目的一个子项目。是 Hadoop 的核心组件之一, Hadoop 非常适于存储大型数据 (比如 TB 和 PB),其就是使用 HDFS 作为存储系统. HDFS 使用多台计算机存储文件,并且提供统一的访问接口,像是访问一个普通文件系统一样使用分布式文件系统。
2. HDFS 架构
HDFS 是一个主/从(Mater/Slave)体系结构,由三部分组成: NameNode 和 DataNode 以及 SecondaryNamenode:
NameNode 负责管理整个文件系统的元数据,以及每一个路径(文件)所对应的数据块信息。
DataNode 负责管理用户的文件数据块,每一个数据块都可以在多个 DataNode 上存储多个副本,默认为 3 个。
Secondary NameNode 用来监控 HDFS 状态的辅助后台程序,每隔一段时间获取 HDFS 元数据的快照。最主要作用是辅助 NameNode 管理元数据信息。
3. HDFS 的特性
首先,它是一个文件系统,用于存储文件,通过统一的命名空间目录树来定位文件;
其次,它是分布式的,由很多服务器联合起来实现其功能,集群中的服务器有各自的角色。
1. master/slave 架构(主从架构)
HDFS 采用 master/slave 架构。一般一个 HDFS 集群是有一个 Namenode 和一定数目的 Datanode 组成。Namenode 是 HDFS 集群主节点,Datanode 是 HDFS 集群从节点,两种角色各司其职,共同协调完成分布式的文件存储服务。
2. 分块存储
HDFS 中的文件在物理上是分块存储(block)的,块的大小可以通过配置参数来规定,默认大小在 hadoop2.x 版本中是 128M。
3. 名字空间(NameSpace)
HDFS 支持传统的层次型文件组织结构。用户或者应用程序可以创建目录,然后将文件保存在这些目录里。文件系统名字空间的层次结构和大多数现有的文件系统类似:用户可以创建、删除、移动或重命名文件。
Namenode 负责维护文件系统的名字空间,任何对文件系统名字空间或属性的修改都将被 Namenode 记录下来。
HDFS 会给客户端提供一个统一的抽象目录树,客户端通过路径来访问文件,形如:hdfs://namenode:port/dir-a/dir-b/dir-c/file.data。
4. NameNode 元数据管理
我们把目录结构及文件分块位置信息叫做元数据。NameNode 负责维护整个 HDFS 文件系统的目录树结构,以及每一个文件所对应的 block 块信息(block 的 id,及所在的 DataNode 服务器)。
5. DataNode 数据存储
文件的各个 block 的具体存储管理由 DataNode 节点承担。每一个 block 都可以在多个 DataNode 上。DataNode 需要定时向 NameNode 汇报自己持有的 block 信息。 存储多个副本(副本数量也可以通过参数设置 dfs.replication,默认是 3)
6. 副本机制
为了容错,文件的所有 block 都会有副本。每个文件的 block 大小和副本系数都是可配置的。应用程序可以指定某个文件的副本数目。副本系数可以在文件创建的时候指定,也可以在之后改变。
7. 一次写入,多次读出
HDFS 是设计成适应一次写入,多次读出的场景,且不支持文件的修改。
正因为如此,HDFS 适合用来做大数据分析的底层存储服务,并不适合用来做网盘等应用,因为修改不方便,延迟大,网络开销大,成本太高。
4. HDFS 的命令行使用
如果没有配置 hadoop 的环境变量,则在 hadoop 的安装目录下的 bin 目录中执行以下命令,如已配置 hadoop 环境变量,则可在任意目录下执行
help
ls
lsr
mkdir
put
moveFromLocal
copyFromLocal
appendToFile
moveToLocal
get
getmerge
copyToLocal
mv
rm
cp
cat
tail
text
chmod
chown
df
du
count
setrep
expunge (慎用)
5. hdfs 的高级使用命令
5.1. HDFS 文件限额配置
在多人共用 HDFS 的环境下,配置设置非常重要。特别是在 Hadoop 处理大量资料的环境,如果没有配额管理,很容易把所有的空间用完造成别人无法存取。HDFS 的配额设定是针对目录而不是针对账号,可以让每个账号仅操作某一个目录,然后对目录设置配置。
HDFS 文件的限额配置允许我们以文件个数,或者文件大小来限制我们在某个目录下上传的文件数量或者文件内容总量,以便达到我们类似百度网盘网盘等限制每个用户允许上传的最大的文件的量。
结果:
5.1.1. 数量限额
5.1.2. 空间大小限额
在设置空间配额时,设置的空间至少是 block_size * 3 大小
生成任意大小文件的命令:
清除空间配额限制
5.2. HDFS 的安全模式
安全模式是 hadoop 的一种保护机制,用于保证集群中的数据块的安全性。当集群启动的时候,会首先进入安全模式。当系统处于安全模式时会检查数据块的完整性。
假设我们设置的副本数(即参数 dfs.replication)是 3,那么在 datanode 上就应该有 3 个副本存在,假设只存在 2 个副本,那么比例就是 2/3=0.666。hdfs 默认的副本率 0.999。我们的副本率 0.666 明显小于 0.999,因此系统会自动的复制副本到其他 dataNode,使得副本率不小于 0.999。如果系统中有 5 个副本,超过我们设定的 3 个副本,那么系统也会删除多于的 2 个副本。
在安全模式状态下,文件系统只接受读数据请求,而不接受删除、修改等变更请求。在,当整个系统达到安全标准时,HDFS 自动离开安全模式。30s
安全模式操作命令
6. HDFS 的 block 块和副本机制
HDFS 将所有的文件全部抽象成为 block 块来进行存储,不管文件大小,全部一视同仁都是以 block 块的统一大小和形式进行存储,方便我们的分布式文件系统对文件的管理。
所有的文件都是以 block 块的方式存放在 hdfs 文件系统当中,在 Hadoop 1 版本当中,文件的 block 块默认大小是 64M,Hadoop 2 版本当中,文件的 block 块大小默认是 128M,block 块的大小可以通过 hdfs-site.xml 当中的配置文件进行指定。
6.1 抽象为 block 块的好处
1) 一个文件有可能大于集群中任意一个磁盘
10T*3/128 = xxx 块 2T,2T,2T 文件方式存—–>多个 block 块,这些 block 块属于一个文件
2) 使用块抽象而不是文件可以简化存储子系统
3) 块非常适合用于数据备份进而提供数据容错能力和可用性
6.2 块缓存
通常 DataNode 从磁盘中读取块,但对于访问频繁的文件,其对应的块可能被显示的缓存在 DataNode 的内存中,以堆外块缓存的形式存在。默认情况下,一个块仅缓存在一个 DataNode 的内存中,当然可以针对每个文件配置 DataNode 的数量。作业调度器通过在缓存块的 DataNode 上运行任务,可以利用块缓存的优势提高读操作的性能。
例如:
连接(join)操作中使用的一个小的查询表就是块缓存的一个很好的候选。 用户或应用通过在缓存池中增加一个 cache directive 来告诉 namenode 需要缓存哪些文件及存多久。缓存池(cache pool)是一个拥有管理缓存权限和资源使用的管理性分组。
例如:
一个文件 130M,会被切分成 2 个 block 块,保存在两个 block 块里面,实际占用磁盘 130M 空间,而不是占用 256M 的磁盘空间
6.3 hdfs 的文件权限验证
hdfs 的文件权限机制与 linux 系统的文件权限机制类似
r:read w:write x:execute
权限 x 对于文件表示忽略,对于文件夹表示是否有权限访问其内容
如果 linux 系统用户 zhangsan 使用 hadoop 命令创建一个文件,那么这个文件在 HDFS 当中的 owner 就是 zhangsan
HDFS 文件权限的目的,防止好人做错事,而不是阻止坏人做坏事。HDFS 相信你告诉我你是谁,你就是谁
6.4 hdfs 的副本因子
为了保证 block 块的安全性,也就是数据的安全性,在 hadoop2 当中,文件默认保存三个副本,我们可以更改副本数以提高数据的安全性
、在 hdfs-site.xml 当中修改以下配置属性,即可更改文件的副本数
7. HDFS 文件写入过程(非常重要)
Client 发起文件上传请求,通过 RPC 与 NameNode 建立通讯, NameNode 检查目标文件是否已存在,父目录是否存在,返回是否可以上传;
Client 请求第一个 block 该传输到哪些 DataNode 服务器上;
NameNode 根据配置文件中指定的备份数量及机架感知原理进行文件分配, 返回可用的 DataNode 的地址如:A, B, C;
Hadoop 在设计时考虑到数据的安全与高效, 数据文件默认在 HDFS 上存放三份, 存储策略为本地一份,同机架内其它某一节点上一份,不同机架的某一节点上一份。
Client 请求 3 台 DataNode 中的一台 A 上传数据(本质上是一个 RPC 调用,建立 pipeline ),A 收到请求会继续调用 B,然后 B 调用 C,将整个 pipeline 建立完成, 后逐级返回 client;
Client 开始往 A 上传第一个 block(先从磁盘读取数据放到一个本地内存缓存),以 packet 为单位(默认 64K),A 收到一个 packet 就会传给 B,B 传给 C。A 每传一个 packet 会放入一个应答队列等待应答;
数据被分割成一个个 packet 数据包在 pipeline 上依次传输,在 pipeline 反方向上, 逐个发送 ack(命令正确应答),最终由 pipeline 中第一个 DataNode 节点 A 将 pipelineack 发送给 Client;
当一个 block 传输完成之后,Client 再次请求 NameNode 上传第二个 block,重复步骤 2;
7.1 网络拓扑概念
在本地网络中,两个节点被称为“彼此近邻”是什么意思?在海量数据处理中,其主要限制因素是节点之间数据的传输速率——带宽很稀缺。这里的想法是将两个节点间的带宽作为距离的衡量标准。
节点距离:两个节点到达最近的共同祖先的距离总和。
例如,假设有数据中心 d1 机架 r1 中的节点 n1。该节点可以表示为/d1/r1/n1。利用这种标记,这里给出四种距离描述。
Distance(/d1/r1/n1, /d1/r1/n1)=0(同一节点上的进程)
Distance(/d1/r1/n1, /d1/r1/n2)=2(同一机架上的不同节点)
Distance(/d1/r1/n1, /d1/r3/n2)=4(同一数据中心不同机架上的节点)
Distance(/d1/r1/n1, /d2/r4/n2)=6(不同数据中心的节点)
7.2 机架感知(副本节点选择)
1)低版本 Hadoop 副本节点选择
第一个副本在 client 所处的节点上。如果客户端在集群外,随机选一个。
第二个副本和第一个副本位于不相同机架的随机节点上。
第三个副本和第二个副本位于相同机架,节点随机。
2) Hadoop2.7.2 副本节点选择
第一个副本在 client 所处的节点上。如果客户端在集群外,随机选一个。
第二个副本和第一个副本位于相同机架,随机节点。
第三个副本位于不同机架,随机节点。
8.HDFS 文件读取过程(非常重要)
Client 向 NameNode 发起 RPC 请求,来确定请求文件 block 所在的位置;
NameNode 会视情况返回文件的部分或者全部 block 列表,对于每个 block,NameNode 都会返回含有该 block 副本的 DataNode 地址; 这些返回的 DN 地址,会按照集群拓扑结构得出 DataNode 与客户端的距离,然后进行排序,排序两个规则:网络拓扑结构中距离 Client 近的排靠前;心跳机制中超时汇报的 DN 状态为 STALE,这样的排靠后;
Client 选取排序靠前的 DataNode 来读取 block,如果客户端本身就是 DataNode,那么将从本地直接获取数据(短路读取特性);
底层上本质是建立 Socket Stream(FSDataInputStream),重复的调用父类 DataInputStream 的 read 方法,直到这个块上的数据读取完毕;
当读完列表的 block 后,若文件读取还没有结束,客户端会继续向 NameNode 获取下一批的 block 列表;
读取完一个 block 都会进行 checksum 验证,如果读取 DataNode 时出现错误,客户端会通知 NameNode,然后再从下一个拥有该 block 副本的 DataNode 继续读。
read 方法是并行的读取 block 信息,不是一块一块的读取;NameNode 只是返回 Client 请求包含块的 DataNode 地址,并不是返回请求块的数据;
最终读取来所有的 block 会合并成一个完整的最终文件。
从 HDFS 文件读写过程中,可以看出,HDFS 文件写入时是串行写入的,数据包先发送给节点 A,然后节点 A 发送给 B,B 在给 C;而 HDFS 文件读取是并行的, 客户端 Client 直接并行读取 block 所在的节点。
9. NameNode 工作机制以及元数据管理(重要)
9.1 namenode 与 datanode 启动
namenode 工作机制
1. 第一次启动 namenode 格式化后,创建 fsimage 和 edits 文件。如果不是第一次启动,直接加载编辑日志和镜像文件到内存。
2. 客户端对元数据进行增删改的请求。
3. namenode 记录操作日志,更新滚动日志。
4. namenode 在内存中对数据进行增删改查。
secondary namenode
1. secondary namenode 询问 namenode 是否需要 checkpoint。直接带回 namenode 是否检查结果。
2. secondary namenode 请求执行 checkpoint。
3. namenode 滚动正在写的 edits 日志。
4. 将滚动前的编辑日志和镜像文件拷贝到 secondary namenode。
5. secondary namenode 加载编辑日志和镜像文件到内存,并合并。
6. 生成新的镜像文件 fsimage.chkpoint。
7. 拷贝 fsimage.chkpoint 到 namenode。
8. namenode 将 fsimage.chkpoint 重新命名成 fsimage。
9.2 FSImage 与 edits 详解
所有的元数据信息都保存在了 FsImage 与 Eidts 文件当中,这两个文件就记录了所有的数据的元数据信息,元数据信息的保存目录配置在了 hdfs-site.xml 当中
客户端对 hdfs 进行写文件时会首先被记录在 edits 文件中。
edits 修改时元数据也会更新。
每次 hdfs 更新时 edits 先更新后客户端才会看到最新信息。
fsimage:是 namenode 中关于元数据的镜像,一般称为检查点。
一般开始时对 namenode 的操作都放在 edits 中,为什么不放在 fsimage 中呢?
因为 fsimage 是 namenode 的完整的镜像,内容很大,如果每次都加载到内存的话生成树状拓扑结构,这是非常耗内存和 CPU。
fsimage 内容包含了 namenode 管理下的所有 datanode 中文件及文件 block 及 block 所在的 datanode 的元数据信息。随着 edits 内容增大,就需要在一定时间点和 fsimage 合并。
9.3 FSimage 文件当中的文件信息查看
使用命令 hdfs oiv
9.4 edits 当中的文件信息查看
查看命令 hdfs oev
9.5 secondarynameNode 如何辅助管理 FSImage 与 Edits 文件
secnonaryNN 通知 NameNode 切换 editlog。
secondaryNN 从 NameNode 中获得 FSImage 和 editlog(通过 http 方式)。
secondaryNN 将 FSImage 载入内存,然后开始合并 editlog,合并之后成为新的 fsimage。
secondaryNN 将新的 fsimage 发回给 NameNode。
NameNode 用新的 fsimage 替换旧的 fsimage。
完成合并的是 secondarynamenode,会请求 namenode 停止使用 edits,暂时将新写操作放入一个新的文件中(edits.new)。
secondarynamenode 从 namenode 中通过 http get 获得 edits,因为要和 fsimage 合并,所以也是通过 http get 的方式把 fsimage 加载到内存,然后逐一执行具体对文件系统的操作,与 fsimage 合并,生成新的 fsimage,然后把 fsimage 发送给 namenode,通过 http post 的方式。
namenode 从 secondarynamenode 获得了 fsimage 后会把原有的 fsimage 替换为新的 fsimage,把 edits.new 变成 edits。同时会更新 fsimage。
hadoop 进入安全模式时需要管理员使用 dfsadmin 的 save namespace 来创建新的检查点。
secondarynamenode 在合并 edits 和 fsimage 时需要消耗的内存和 namenode 差不多,所以一般把 namenode 和 secondarynamenode 放在不同的机器上。
fsimage 与 edits 的合并时机取决于两个参数,第一个参数是默认 1 小时 fsimage 与 edits 合并一次。
第一个参数:时间达到一个小时 fsimage 与 edits 就会进行合并
第二个参数:hdfs 操作达到 1000000 次也会进行合并
第三个参数:每隔多长时间检查一次 hdfs 的操作次数
9.6 namenode 元数据信息多目录配置
为了保证元数据的安全性,我们一般都是先确定好我们的磁盘挂载目录,将元数据的磁盘做 RAID1
namenode 的本地目录可以配置成多个,且每个目录存放内容相同,增加了可靠性。
具体配置方案:
hdfs-site.xml
9.7 namenode 故障恢复
在我们的 secondaryNamenode 对 namenode 当中的 fsimage 和 edits 进行合并的时候,每次都会先将 namenode 的 fsimage 与 edits 文件拷贝一份过来,所以 fsimage 与 edits 文件在 secondarNamendoe 当中也会保存有一份,如果 namenode 的 fsimage 与 edits 文件损坏,那么我们可以将 secondaryNamenode 当中的 fsimage 与 edits 拷贝过去给 namenode 继续使用,只不过有可能会丢失一部分数据。这里涉及到几个配置选项
namenode 保存 fsimage 的配置路径
namenode 保存 edits 文件的配置路径
secondaryNamenode 保存 fsimage 文件的配置路径
secondaryNamenode 保存 edits 文件的配置路径
接下来我们来模拟 namenode 的故障恢复功能
杀死 namenode 进程: 使用 jps 查看 namenode 的进程号 , kill -9 直接杀死。
删除 namenode 的 fsimage 文件和 edits 文件。
根据上述配置, 找到 namenode 放置 fsimage 和 edits 路径. 直接全部 rm -rf 删除。
拷贝 secondaryNamenode 的 fsimage 与 edits 文件到 namenode 的 fsimage 与 edits 文件夹下面去。
根据上述配置, 找到 secondaryNamenode 的 fsimage 和 edits 路径, 将内容 使用 cp -r 全部复制到 namenode 对应的目录下即可。
重新启动 namenode, 观察数据是否存在。
10 datanode 工作机制以及数据存储
datanode 工作机制
一个数据块在 datanode 上以文件形式存储在磁盘上,包括两个文件,一个是数据本身,一个是元数据包括数据块的长度,块数据的校验和,以及时间戳。
DataNode 启动后向 namenode 注册,通过后,周期性(1 小时)的向 namenode 上报所有的块信息。(dfs.blockreport.intervalMsec)。
心跳是每 3 秒一次,心跳返回结果带有 namenode 给该 datanode 的命令如复制块数据到另一台机器,或删除某个数据块。如果超过 10 分钟没有收到某个 datanode 的心跳,则认为该节点不可用。
集群运行中可以安全加入和退出一些机器。
数据完整性
当 DataNode 读取 block 的时候,它会计算 checksum。
如果计算后的 checksum,与 block 创建时值不一样,说明 block 已经损坏。
client 读取其他 DataNode 上的 block。
datanode 在其文件创建后周期验证 checksum。
掉线时限参数设置
datanode 进程死亡或者网络故障造成 datanode 无法与 namenode 通信,namenode 不会立即把该节点判定为死亡,要经过一段时间,这段时间暂称作超时时长。HDFS 默认的超时时长为 10 分钟+30 秒。如果定义超时时间为 timeout,则超时时长的计算公式为:
*timeout = 2 dfs.namenode.heartbeat.recheck-interval + 10 dfs.heartbeat.interval。*
而默认的 dfs.namenode.heartbeat.recheck-interval 大小为 5 分钟,dfs.heartbeat.interval 默认为 3 秒。
需要注意的是 hdfs-site.xml 配置文件中的 heartbeat.recheck.interval 的单位为毫秒,*dfs.heartbeat.interval 的单位为秒*。
DataNode 的目录结构
和 namenode 不同的是,datanode 的存储目录是初始阶段自动创建的,不需要额外格式化。
在/opt/hadoop-2.6.0-cdh5.14.0/hadoopDatas/datanodeDatas/current 这个目录下查看版本号
具体解释:
storageID:存储 id 号。
clusterID 集群 id,全局唯一。
cTime 属性标记了 datanode 存储系统的创建时间,对于刚刚格式化的存储系统,这个属性为 0;但是在文件系统升级之后,该值会更新到新的时间戳。
datanodeUuid:datanode 的唯一识别码。
storageType:存储类型。
layoutVersion 是一个负整数。通常只有 HDFS 增加新特性时才会更新这个版本号。
datanode 多目录配置
datanode 也可以配置成多个目录,每个目录存储的数据不一样。即:数据不是副本。具体配置如下:
- 只需要在 value 中使用逗号分隔出多个存储目录即可
10.1 服役新数据节点
需求说明:
随着公司业务的增长,数据量越来越大,原有的数据节点的容量已经不能满足存储数据的需求,需要在原有集群基础上动态添加新的数据节点。
10.1.1 环境准备
复制一台新的虚拟机出来
将我们纯净的虚拟机复制一台出来,作为我们新的节点
修改 mac 地址以及 IP 地址
关闭防火墙,关闭 selinux
更改主机名
四台机器更改主机名与 IP 地址映射
node04 服务器关机重启
node04 安装 jdk
然后解压 jdk 安装包,配置环境变量
解压 hadoop 安装包
将 node01 关于 hadoop 的配置文件全部拷贝到 node04
10.1.2 服役新节点具体步骤
创建 dfs.hosts 文件
node01 编辑 hdfs-site.xml 添加以下配置
在 namenode 的 hdfs-site.xml 配置文件中增加 dfs.hosts 属性
刷新 namenode
- node01 执行以下命令刷新 namenode
更新 resourceManager 节点
- node01 执行以下命令刷新 resourceManager
namenode 的 slaves 文件增加新服务节点主机名称
node01 编辑 slaves 文件,并添加新增节点的主机,更改完后,slaves 文件不需要分发到其他机器上面去
单独启动新增节点
使用负载均衡命令,让数据均匀负载所有机器
10.2 退役旧数据
创建 dfs.hosts.exclude 配置文件
在 namenod 所在服务器的/export/servers/hadoop-2.6.0-cdh5.14.0/etc/hadoop 目录下创建 dfs.hosts.exclude 文件,并添加需要退役的主机名称
编辑 namenode 所在机器的 hdfs-site.xml
编辑 namenode 所在的机器的 hdfs-site.xml 配置文件,添加以下配置
刷新 namenode,刷新 resourceManager
节点退役完成,停止该节点进程
等待退役节点状态为 decommissioned(所有块已经复制完成),停止该节点及节点资源管理器。注意:如果副本数是 3,服役的节点小于等于 3,是不能退役成功的,需要修改副本数后才能退役。
从 include 文件中删除退役节点
node01 执行一下命令刷新 namenode,刷新 resourceManager
从 namenode 的 slave 文件中删除退役节点
如果数据负载不均衡,执行以下命令进行均衡负载
11 block 块手动拼接成为完整数据
所有的数据都是以一个个的 block 块存储的,只要我们能够将文件的所有 block 块全部找出来,拼接到一起,又会成为一个完整的文件,接下来我们就来通过命令将文件进行拼接:
上传一个大于 128M 的文件到 hdfs 上面去
我们选择一个大于 128M 的文件上传到 hdfs 上面去,只有一个大于 128M 的文件才会有多个 block 块。
这里我们选择将我们的 jdk 安装包上传到 hdfs 上面去。
node01 执行以下命令上传 jdk 安装包
web 浏览器界面查看 jdk 的两个 block 块 id
这里我们看到两个 block 块 id 分别为
1073742699 和 1073742700
那么我们就可以通过 blockid 将我们两个 block 块进行手动拼接了。
根据我们的配置文件找到 block 块所在的路径
4. 执行 block 块的拼接
作者简介:
园陌,目前就职于一家大而温馨的互联网公司,数据开发工程师
公众号【五分钟学大数据】的原创作者
本篇文章首发于公众号,版权归 InfoQ 与公众号【五分钟学大数据】共同所有,转载请注明本声明!
版权声明: 本文为 InfoQ 作者【五分钟学大数据】的原创文章。
原文链接:【http://xie.infoq.cn/article/33bfd80a0c4ce2f3742aa1321】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论