HDFS 用了这个优化后,性能直接翻倍
【背景】
前段时间在 HDFS 的 dn 节点规模 1000+的环境中,并且有 1 亿 block 数据量的情况下, 进行大量并发写文件测试时,发现部分客户端写异常并导致最终仅写入了部分数据,本文就该问题进行分析总结。
【表面现象分析】
出现该问题时,首先查看了客户端的日志,发现这些客户端的日志中,都出现了 NotReplicatedYetException 的告警日志,其他全部成功写入的客户端中有的也有这样的日志。
既然都出现了这个告警日志,那为什么有的能全部成功写入,有的就直接退出不写了呢?
这个现象背后的原理其实很简单:当客户端写文件过程中,向 nn 发送申请新的 block 的 rpc 请求时,如果 nn 以错误形式返回 NotReplicatedYetException,客户端的 rpc 处理会根据该错误构造对应的异常并向上抛出异常,接口调用的处理中又会捕获该异常,然后睡眠一段时间,并再次发送申请 block 的 rpc 请求。当重试到达一定次数后,仍旧是失败的,那么就不再继续尝试,直接退出。
关键代码如下:
因此,虽然有的客户端有告警日志,但都全部成功写入,只能说明出现了重试,但最终还是成功响应;而有的则连续 5 次均重试失败,导致仅写入了部分数据。
【问题深入】
既然知道了是因为 nn 对客户端请求 block 的 rpc 请求返回了错误,并且是连续多次请求都返回错误,最终引发客户端终止写入,那么 nn 为什么会一直返回 NotReplicatedYetException 的错误呢?
这里就涉及到 NN 中 block 的状态,以及申请新 block 的 rpc 请求处理逻辑了。
在 NN 内部,每个 block 的初始状态为 underConstruction,然后依次变为 committed,最终变为 complete。
客户端写同一个文件,申请新的 block 时,nn 默认将上一个 block 的状态置为 committed,等 dn 通过增量块汇报将 block 副本的 finalized 状态上报时,nn 将该 block 的状态置为 complete。
另外,nn 在处理客户端申请新的 block 的 rpc 请求时,需要检查上上一个 block 的状态是否为 complete。如果不是,则返回 NotReplicatedYetException。
也就是说,写同一个文件时,在申请第 3 个 block 时,会检查第 1 个 block 的状态是否为 complete,如果不是则返回错误,后面的依次类推。
实际测试过程中出现问题时,必然是前面的 block 状态不对导致的。
【问题根因】
了解了错误的产生原因之后,自然会追问,为什么 nn 中 block 的状态一直没有达到 complete 状态,是因为 dn 没有发送增量块汇报请求吗?
然而,从 dn 的日志来看,确实都有发送增量块汇报的请求。
在问题分析过程中,没有头绪时却注意到了另外一个细节。在整个测试中,在 nn 的 web 页面上,看到有部分 dn 出现了离线的情况。
顺着这个现象进行分析:nn 感知 dn 下线,肯定是心跳超时,而 dn 的增量块汇报和心跳是在同一个线程中复用同一个 tcp 连接串行发送的。因此心跳超时,增量块汇报请求肯定也会受影响。
再次测试复现时,发现 dn 的心跳线程的堆栈卡在发送增量块汇报的函数中,而在 nn 节点上通过 netstat 观察对应的 tcp 连接情况,发现连接的 Recv-Q 的数值一直很高。查看 nn 的 jmx 指标,发现 CallQueueLength 也一直维持在最大值,由此断定是 rpc 处理机制引起的问题。
为什么是 rpc 处理引起的,请继续往下看。
在 nn 内部,对于每个监听的端口,都有四种类型的线程来处理连接上的请求。
监听线程
负责在指定端口监听,当有新连接到来时,负责完成连接的建立,随后将连接转交给 reader 线程进行后续的处理。
reader 线程
负责连接建立之后,接收该连接上的请求(从 socket 上读取客户端发送的数据),并请请求封装成 callRpc 请求对象,然后将请求对象放到请求队列中。
handler 线程
通常有多个 handler 线程,负责从请求队列中取出请求,并进行实际的处理,处理完成后通过连接的 socket 直接发送请求响应内容,或者将响应内容放到响应队列中。
响应发送线程
负责从响应队列中取出请求对应的响应,然后通过请求对应连接的 socket,发送响应内容。
注意:
请求队列的长度是有上限的,具体上限为 handler 线程的个数乘以每个 handler 线程的队列长度。
当请求队列达到上限时,reader 线程将请求放到队列的动作会被阻塞,继而所有连接过来的请求堆在 tcp 连接的接收缓冲区中,而不会被处理。
按照默认的配置,10 个 handler 线程,每个线程的队列长度 100,那么该监听端口上的请求队列长度为 1000。
当有 10000 的并发写文件时,那么就有 10000 个申请 block 的 rpc 请求,这些请求瞬间将请求队列塞满(nn 的 jmx 指标中 CallQueueLenght 表示该队列的实际长度),剩余的请求堆积在连接的接收缓冲区中(netstat 看到的 Recv-Q),等待 reader 线程读取并处理,并且随着一个 block 的写完,还会继续写新的 block,也就是继续产生新的 rpc 请求,因此观察 nn 的 jmx 指标发现 rpc 请求队列持续达到最大值。
而 dn 的增量块汇报请求本质上也是一个 rpc 请求,这些请求和客户端申请 block 的请求都发往同一个端口,等待 reader 线程从连接的接收缓冲区读取,或在请求队列中等待 handler 线程进行实际处理。
因此就可能出现,某些 dn 的增量块汇报请求,虽然成功发送,但在连接的接收缓冲区上的请求一直未被 reader 读取处理。所以,在 nn 内部 block 的状态也就没有变化,导致客户端申请新的 block 时返回错误。同样,也能解释为什么有的客户端出现了离线的情况。
【问题优化】
知道了问题的所在,剩下来就是进行优化了。
首先采用了增加 handler 线程数的方法。加大请求并发处理,也变相加大了 rpc 请求队列的长度。这也是网上提得比较多的方案。
然而实际效果却不太理想,因为对于申请 block 请求的处理,内部有一把大锁锁住,这会导致其他 handler 线程频繁等锁。因此虽然增加 handler 线程看是提升了并发处理能力,但是实际多数 handler 线程都在等锁,导致 dn 的增量块汇报请求不能及时被处理。其提升的效果不明显。
我们注意到:将 dn 的增量块汇报请求和客户端申请 block 的请求混在一个队列中,是无法保证有限处理 dn 的增量块汇报请求的,因此考虑将其进行分离,即 dn 的 rpc 请求和客户端的 rpc 请求分别发往不同的端口。这只需要在 nn 上修改相应的配置即可。
端口分离后的测试效果:并发写文件的数量相比分离之前,直接翻倍提升,从并发 15000 提升到 30000+(受限于客户端所在机器的性能,没有继续往上压,实际网络带宽,nn 的各项指标表明并发读还可以继续增加),到此问题得到优化解决。
【总结】
来小结一下,本文通过实际测试过程中遇到的问题,结合 hdfs 中的客户端的请求逻辑、nn 内部 blocks 状态、申请 block 请求的处理机制、rpc 处理机制等原理,一步步进行分析,最终定位问题,并给出优化方案,且优化效果明显。
版权声明: 本文为 InfoQ 作者【hncscwc】的原创文章。
原文链接:【http://xie.infoq.cn/article/624810ee492ef7c352c394906】。文章转载请联系作者。
评论