TT Lab
开始
学习 学习路径 课程

Apache Hadoop — 在一个 Pod 里搭起并运维 HDFS 与 YARN

NameNode 掌管名字,DataNode 保存字节

在 TT Lab 中继续学习

一句话总结

HDFS 是一个从一开始就把知道名字的一方(NameNode)与持有字节的一方(DataNode)分开的文件系统。无论是用 shell 读取还是用 HTTP 读取,“向 NameNode 询问在哪里,再从 DataNode 取回字节”的顺序都是一样的。

为什么要把职责一分为二

笔记本电脑的文件系统在同一块磁盘上同时管理名字和内容。找到目录项,旁边就记着内容在哪里,两者在同一台机器上。HDFS 要解决的问题与此规模不同。

HDFS 设计文档在开头就给出了两个前提:硬件故障不是例外而是常态,HDFS 上的典型文件大小在 GB 到 TB 级别。要把大文件分散存放到几百台机器的磁盘上,就必须有人在一个地方知道“这个文件的第几片在哪台机器上”。同时,实际的字节必须由多台机器一起送出,才能获得吞吐量。

于是职责被拆开。NameNode 掌管命名空间——目录树、所有者与权限、每个文件的副本系数——以及“哪个块在哪个 DataNode 上”这张映射表。DataNode 负责读写挂在自己机器上的磁盘里的块。设计文档用一句话点明了这个结构的重心:它的设计保证用户数据永远不会流经 NameNode。

反过来想,就能明白这一句话为什么重要。如果 NameNode 连字节也要中转,整个集群的吞吐量就会被绑死在一台 NameNode 的网卡和磁盘上。正因为它只处理元数据,一台机器才能指挥几百台机器。但这也有代价:所有名字操作都要经过这一台机器,所以 NameNode 成了集群中最重要的单点。

工作原理

读取。客户端打开文件时,先向 NameNode 询问该文件的块列表,以及每个块的副本所在的 DataNode。然后直接连接 DataNode 取回字节。设计文档中关于副本选择的部分说明,读取方会选择离自己最近的副本——如果同一个机架上有,就优先使用它。

写入。写入的形态相同。客户端向 NameNode 要一个新块,NameNode 返回块 ID 和接收副本的 DataNode 列表。字节只发送给列表中的第一个 DataNode。这就是设计文档所说的复制管道。第一个 DataNode 收到片段后,一边写入自己的磁盘,一边同时传给第二个,第二个再传给第三个。

HDFS 写入过程。客户端向 NameNode 请求新块,只会得到块 ID 和三台 DataNode 的列表。字节只发送给第一个 DataNode,每个 DataNode 一边写入收到的片段,一边传给下一个 DataNode。DataNode 们另行向 NameNode 发送心跳和块报告

DataNode 不认识文件。按设计文档的原话来说,DataNode 对 HDFS 文件一无所知,它把每个块分别存成本地文件系统中的单独文件。启动时,它会扫描本地磁盘,列出自己持有的块并发送给 NameNode,这就是块报告。平时它则定期发送心跳。间隔由 hdfs-default.xml 的 dfs.heartbeat.interval 决定,默认为 3 秒;对于心跳中断的 DataNode,默认要等超过 10 分钟才判定其已死亡,这是相当保守的时间。这是为了避免因为状态不稳定的节点引发复制风暴(实验 Pod 为了缩短等待,把这个判定提前到了 1 分 30 秒)。

由此得到一个重要性质。设计文档指出,NameNode 从不主动发起 RPC,只应答 DataNode 和客户端的请求。NameNode 要让 DataNode“删除这个块、复制这个块”时,也是把指令附在心跳的应答里发回去。

mv 不会移动字节。重命名和移动目录属于设计文档归类为 NameNode 职责的命名空间操作。文件移到新路径后,块保持不变,块 ID 也不会改变。这就是为什么对几十 GB 的目录执行 mv 也能瞬间完成。反过来,cp 会把字节重新写入新的块。

用两条路径查看同一个文件

到达 HDFS 的路径有很多条。最熟悉的是 hdfs dfs shell,它通过 NameNode 的 RPC 端口通信。另一条是 HTTP。WebHDFS 文档规定了地址的形态:在路径前加上 /webhdfs/v1,后面接 op= 查询。NameNode 的 HTTP 地址在 hdfs-default.xml 中由 dfs.namenode.http-address 指定,默认为 0.0.0.0:9870。

# 목록은 NameNode 혼자 답한다 — 메타데이터뿐이다
curl -s "http://localhost:9870/webhdfs/v1/user/root?op=LISTSTATUS"

# 내용은 DataNode 로 돌려보낸다 — 307 을 따라가야 바이트가 온다
curl -i "http://localhost:9870/webhdfs/v1/user/root/app.log?op=OPEN"
# HTTP/1.1 307 TEMPORARY_REDIRECT
# Location: http://<DataNode>:<포트>/webhdfs/v1/user/root/app.log?op=OPEN...

即使换成 HTTP,职责分工依然清晰可见。LISTSTATUS 由 NameNode 直接以 JSON 应答。OPEN 和 CREATE 如 WebHDFS 文档所述,通常返回指向 DataNode 的 307 重定向。用 curl -L 跟随重定向,字节才会到来。shell 在内部完成的“先问,再去取”,在 HTTP 中变成了肉眼可见的两次请求。

还有一点。文档指出,在关闭安全的集群中,WebHDFS 会把 user.name 查询参数的值直接当作已认证的用户。也就是说,对方说自己是谁就信是谁,这一点会在权限模块中再讨论。

在现场相遇的样子

第一,文件看得到,读取却失败。名字在 NameNode 中完好无损,但持有这些块的 DataNode 全部宕机时,ls 能用,cat 却会失败。这是两种职责分开之后出现的典型症状。遇到这种情况,先用 HDFS 命令文档中的 hdfs dfsadmin -report 查看存活的 DataNode 和容量。

第二,通过 WebHDFS 能列目录,却读不了。只在防火墙外开放 9870 端口时经常遇到。列目录由 NameNode 应答,而读取会被重定向到 DataNode 的地址,如果客户端访问不到那个地址,第二次请求就会失败。这是同一种结构以另一种面貌出现。

第三,NameNode 慢,一切都慢。字节由 DataNode 搬运,但执行一次 ls、打开一个小文件,都要与 NameNode 往返一次。小文件多达数百万个的集群,首先吃不消的就是 NameNode,原因就在这里。

第四,文件一次写入,多次读取。设计文档采用的模型是:文件创建并关闭之后,除了追加和截断,不再修改,而且一个文件任何时候都只有一个写入者。不会有人去改文件中间的内容。这适合日志这类持续累积的数据,不适合按行修改的数据。

实际工作中真正重要的事

下一项实验要做什么

先用 hdfs getconf 查询默认文件系统地址、副本系数和块大小各是什么值,再通过管理报告看到已挂上一个 DataNode。把一份访问日志上传到 HDFS,用 shell 统计行数,再通过 WebHDFS 的 LISTSTATUS 对同一个目录发送 HTTP 请求,以 JSON 列表的形式取回。最后把文件移到另一个目录,用 fsck 检查块,亲眼看到移动前后 fileId 和块 ID 保持不变——也就是说,名字操作并没有触碰字节。