背景
Hadoop HDFS 3.x 引入了 Observer NameNode 这个新角色之后,典型的集群部署从一主一备,变为一主一备一OBNN,其中:
- Active NameNode 处理所有写类型请求
- Standby NameNode 做 HA 使用。
- Observer NameNode 处理所有的读类型请求。
实际上这就是读写分离,在集群读负载特别重时,甚至可以配置多个 Observer NameNode,从而更加分散读负载,更好地提升集群的处理能力。
几个典型读写分离场景如下:
- 写入文件 -> 读取文件
- append 文件 -> 读取文件
- delete 文件 -> 读取文件
- delete 目录 -> list 目录
- delete 目录 -> count 目录
- 其它
在做了读写分离之后,如果控制不当,就有可能导致“读”类型操作最终得到不符合预期的结果,包括但不限于:
- 读取到已被删除的文件。
- 读取到不完整的文件内容。
- list 得到不完整的目录内容。
- count 得到错误的结果。
- 其它
Observer NameNode 的主要难点就是如何保证读写的一致性,这也是我们最关心的点。
读写一致性保证
在上面列出的几个场景中,“写文件 -> 读文件” 是最常见的情况,因此,以它为例来进行分析。其它几种场景的原理也类似。
对于 "写文件 -> 读文件“ 的场景,有下面几种可能的情况,分别使用不同的机制来保证一致性。
本客户端写文件完成后,自己读取该文件。
自写自读,这是最简单的情况,也是 Observer NN 要解决的最基础的问题。为了解决该问题,HDFS 引入了一个新的的 RPC header 成员:state id,几个要点如下:
- state id 的取值实际上就是已有的 NameSystem Transaction ID(即 EditLog ID),这是一个单调递增的 long 型整数,由 Active NN 负责维护,Active NN 每执行一个写操作后便递增此 id。
- client 从 Active NN 获取最新的 state id,并保存在自己的内存中。
- client 每同 Active NN 执行一次 RPC 请求后,就会更新一次自己的 state id(通过 RPC response 从 Active NN 带回),以便尽可能地让自己跟踪到 NN 的最新状态。
- client 每次向 Observer NN 发送 RPC 请求时,都会带上这个 state id.
- Observer NN 收到 RPC 请求后:
- 如果 Observer NN 发现自己目前的 transaction id 比客户端的 state id 更大,那就说明他早就 apply 了这个客户端之前的写操作,那么他就可以立即执行,并给客户端返回结果。
- 如果 Observer NN 发现自己目前的 transaction id 没有客户端的 state id 大,那么说明他还没有来得及 apply 这个客户端之前的写操作,那么它暂时还不能处理这个 RPC,需要一直等到它的 transaction id 大于等于客户端的 state id 之后,才能开始处理。
本客户端写文件完成后,其它客户端启动进程,读取该文件。
自写之后他读,这也是比较简单的情况,为解决该问题,Observer NN 引入了一个新的操作,即:每当客户端启动的时候,都会主动向 Active NN 调用一次 msync(),确保自己的 state id 已更新到最新状态,在此基础上,它去读取之前已经写入完成的文件就没有问题。
关于 msync(),这是 Observer NN 新引入的一个 RPC 调用,也是一个非常轻量级的 PRC,所做的事情也很简单,即:从 Active NN 拿到最新的 transaction id,然后将自己的 static id 更新到最新状态。
两个客户端进程都早已启动,本客户端写文件完成后,其它客户端立即去读。
自写同时他读,这是最棘手的情况,处理不好很可能导致其它客户端读取到不符合预期的结果。
- 理想情况
- Observer NN 已经完整的 apply 了这个文件的 create + close 操作,所以已经 close 了这个文件,此时,client 可以读取到完整的文件。
- 几个可能的出错情况
- Observer NN 还没有来得及 apply 这个文件的 create 操作,因此无法找到这个文件,报错。
- Observer NN 已经 apply 这个文件的 create 操作,但尚未 apply 文件的 close 操作,那么此时在 Observer NN 这里,这个文件处于打开状态, 此时 client 可能会读到不完整的文件内容。
对于这种最复杂的场景,其实并没有什么好办法,在这种情况下,其它客户端只能等待一段时间之后,才能确保从 OBNN 读到最新的内容,而这个间隔的长短就成为关键,我们所能做的,就是尽量想办法,压缩这个间隔。目前的话,对于客户端而言,就是被动和主动两个策略。
两种等待策略
-
客户端被动等待
即客户端什么都不做,就等 OBNN 自己追赶 ANN,这是最简单的策略,以后应该也会成为我们主要的方案,在这种客户端完全无为的情况下,他所要等待的间隔,其实就是下面这个流程的耗时:ANN 执行写操作完成并将 EditLog 写到 JN 集群 -> OBNN 从 JN 集群拉取到 EditLog -> OBNN 将 EditLog 应用到自身
目前的话,在软件层面,通过各种措施和优化,这个间隔几乎已经被压缩到了极限。因此,这个间隔的最终值,基本取决于 NN 机器性能、JN 机器性能、网络性能等等硬件指标。
从现网机器的测试结果来看,这个间隔大概在 1s-5s 左右(现网实测结果:正常情况下 10ms,最差情况下 3s)。
-
客户端主动更新
除了被动依靠 OBNN 自己追赶 ANN 以外,Client 也可以发挥主动性,保证自己尽量拿到最新的结果,即:- 客户端首先主动对 ANN 来一次 msync() 调用,这是一个新增的 RPC 调用,执行非常简单,就是拿到 ANN 目前最新的 state id。
- 客户端接下来向 OBNN 发送 RPC 请求时,会带上这个 state id。而 OBNN 处理 RPC 请求时,如果它发现自身目前的状态比较老,还不足以处理这个 RPC(即:自身的 state id 小于客户端要求的 state id),那 OBNN 会暂时推迟这个 RPC 的处理,直到自己的状态足够新(即:自身的 state id 已经大于等于客户端要求的 state id 之后),才会开始处理这个 RPC,并给客户端返回结果。
主动策略需要客户端周期性的从 ANN 拿到最新的 state id,为此,HDFS 3.x 为客户端添加了一个自动的周期性 msync() 机制, mysnc() 周期通过 可配,默认为 -1,即禁用自动 msync 机制。
假设这个周期配置为 10s,则客户端每隔10s,就从 ANN 获取一次最新的 state id,这就能保证:所有 10s 以前的 ANN 写操作,client 都可以从 OBNN 处读取到正确结果(但注意它发给 OBNN 的 RPC 请求,可能需要在 OBNN 处停留一会儿,以等待 OBNN 满足处理条件).
目前现网最繁忙的 HDFS 集群,其客户端大概有 5W 个左右,如果自动 msync 周期配置为 10s,那么平均每秒钟会产生 5000 个 msync RPC 请求,量比较大。但是 msync() 调用比较特殊,它其实并没有真正做什么(既不会加写锁,也不会加读锁,也不会对 NameSystem 做任何操作),只是简单在 RPC client 和 RPC server 之间同步了一下 state id,因此,这个 RPC 对 ANN 造成的性能影响很小,可以忽略。
因此,作为一个兜底的机制,建议开启客户端自动 msync() 机制,以保证一个最长的等待时间,这个时间建议配置为 10s。
一致性结论
三种可能的情况:
-
客户端自写自读
这是最简单的情况,此时可以百分百确保读写一致。 -
客户端 A 写入完成之后,客户端 B 启动进程并读取
这是现网最常见的情况,此时,也可以百分百确保读写一致。 -
客户端 A、B 均为常驻进程,A 写入完成之后,B 立刻开始读取
这是最复杂的情况,在这种情况下,客户端 B 必须要等待一段时间,才能确保从 OBNN 读到准确的结果,而这个间隔的长短就成为关键:- 一般情况下,按照现网的机器配置,客户端需要等待 1s-5s 左右(现网实测结果:正常情况下 10ms,最差情况下 3s),就能保证 OBNN 追上 ANN,从而能从 OBNN 读取到正确的结果。
- 最差情况下,客户端需要一个兜底的机制,即客户端自动 msync() 机制,自动 msync() 周期建议配置为 10s,这样可以保证客户端最长需要等待 10s,然后即可从 OBNN 处读取到正确结果。当然,如果客户端等不了这么久,也可以将这个周期配置的更短,但同时也将加重 ANN 的负载(msync 请求只能由 ANN 处理)。
Observer NameNode 的几个关键前置特性
Observer NameNode 有几个比较重要的前置特性,基本上都是针对 EditLog tail 机制的优化,这也直接决定了 OBNN/SBNN 和 ANN 的差距到底会有多大,如果需要将 OBNN 特性合入低版本 Hadoop,那么这几个前置特性是首先要合入的。
-
RPC tail editlog
默认情况下,SBNN/OBNN 通过 http 方式从 JN 集群拉取 EditLog,现在也可以通过 RPC 方式拉取,这会比较明显得提高拉取性能。 -
Fast tail editlog
默认情况下,SBNN/OBNN 只会从 JN 集群拉取 Finalized 状态的 EditLog segment 文件(即写完并 close 的 EditLog segment 文件),按照现网配置,一个 EditLog segment 文件从 open 到 close 固定为2分钟,因此 SBNN/OBNN 与 ANN 至少会有2分钟的差距,这个太长。增加了快速 tail 机制后,SBNN/OBNN 不用等到一个 EditLog segment 文件写完,而是可以尽快拉取目前处于 inprogress 状态的 EditLog segment 文件,从而尽量缩小 SBNN/OBNN 与 ANN 的差距。该机制通过配置项 dfs.ha.tail-edits.in-progress 控制,默认为 false 即不打开。
-
JournalNode EditLog cache
现在为 JournalNode 增加了 EditLog 的内存 cache,cache 大小可配置(dfs.journalnode.edit-cache-size.bytes,默认1M),同理,这也是为了让 JournalNode 能够尽可能快的响应 SBNN/OBNN 的 tail 请求。 -
SBNN/OBNN tail EditLog 的指数退避机制
默认情况下,SNBB/OBNN 以一个固定的间隔从 JN 集群 tail EditLog(现网配置为60s),这个太长。现在为了尽可能缩小 SBNN/OBNN 和 ANN 的差距,这个值设置为0,即不间断地拉取,如果在拉取过程中,SBNN/OBNN 发现目前没有任何可用的 EditLog(可能是集群目前没有写请求),那么它们会做一个指数退避,逐渐拉长下一次拉取的间隔,最长间隔可配置(dfs.ha.tail-edits.period.backoff-max,默认0,即关闭指数退避,推荐配置为10s)。
OBNN 合入低版本 Hadoop 预估
OBNN 的合入将会是一个非常复杂的工程,和 EC、RBF 相比,他有几个显著的不同:
- 不仅需要修改 NameNode 组件,同时还要大幅度修改 JournalNode、客户端等组件,工作量较大。
- 特别地,EC 客户端和 RBF 都有自己独立的工程,但 OBNN 没有,相反,它是深度嵌入现有的 NN 代码之中,耦合非常之紧,这大大增大了合并难度。
网友评论