问题表现
我们的系统是这样的组成:HDFS的每台DataNode机器上都有Hbase的regionserver,Hbase分成两个集群使用。 做离线计算使用的Hbase集群数量较多,经常会有MapReduce或者Spark程序来批量scan Hbase的表。 在运维的过程中,发现DataNode的log中经常会有类似于以下的大量log重复出现:
看起来好像是有文件块损坏,但查询dmesg
并没有发现磁盘异常,找到那个Block的文件,也能正常读取。 所以这一定有什么问题,虽然不影响正确性,但重复的读取同一个文件出错肯定会影响系统性能,浪费宝贵的资源。
由于当时人手有限,这个问题就先搁置了。近期来了一个做Hadoop的同事来帮我分析和查找问题。 这下面的工作很多是他动手完成的,这里要特别感谢邓志华。
步骤
下面讲我们是如何分步确定问题并给出解决方案的。
查询Block所属文件
这些出现问题的Block,在日志中能找到他们的BlockID,然后在DN的文件系统里找到这些Block对应的实际文件位置。 尝试了了几个机器后,发现这些Block的物理位置并没有什么特点,基本可以排除物理磁盘损坏或者文件系统损坏的问题。 接下来需要看这些Block在HDFS里有什么规律。 HDFS有一个工具叫oiv
(Offline Image Viewer),将HDFS的fsimage
导成一个xml文件,然后就可以对这个文件搜索Block所属的文件。
搜索了一些Block后,发现这些Block都是HBase的HFile文件。另外一个规律是,相近时间段的出问题的Block,经常属于同一个表,而且都是那些经常需要全表扫描的table。 结合Yarn中job执行历史,当一个表有大Job执行的时候,其所属HFile更容易出现这些问题。
由于业务场景的原因,我们不能关闭表而直接读取裸的HFile,所以只能用Hbase client
从regionserver读取数据。 到这里能基本确定,问题的根本原因跟Hbase regionserver读取HFile有关。
打印出更多信息
HBase是个复杂的系统,上面的分析还不足以确定具体问题点在哪,我们还需要一些其他信息。
首先,我们打开了一台DataNode的trace日志,监控了一段时间后发现了跟之前Not scheduling suspect block
想临近的错误,如下:
可以看出,是DataNode向Client发送数据的过程中,Client主动关闭了连接,而在DataNode认为旧的连接所对应的请求数据还未被完全写完,当往一条被客户端关闭的连接继续写入数据时,则抛出IOException: Connection reset by peer
的异常,但从这些Trace log中还是缺少一些我们需要的信息,例如:
DFSClient
的id。 - 这次读取的起始位置和终止位置是多少。
- 从哪个IP过来的连接。
原生的DataNode打印这个问题日志的地方并没有打印这些信息,这就需要我们做一些定制化的修改。大体的格式如下:
找一台DataNode上线观察,并收集一段时间的log进行分析,得到了如下的输出。
分析这段日志,这些不同的请求有着相同的endOffset, 其中利用这些信息, 可获得一个大致的请求图:
这些请求在时间上也有严格的先后顺序(从之前的日志可以看出,这部分日志的duration值是当前时间的ns表示), 可以假设,这些请求都归属于某一个比较大的请求,用于某种原因,这些请求被分割成这些小请求。 分析DFSInputStream的read方法,
在上一步的readBuffer中,
每一次发生数据的拷贝, DFSInputStream的实例都会将pos += result, 当拷贝发生异常时,DFSInputStream会从上次读取成功的位置(pos)从新尝试与当前的DataNode创建一条新的连接, 这与当前DataNode这样的日志输出符合, 也就是请求2之前, DFSInputStream读取请求1的数据失败, 尝试重新与c1-hd-dn10建立一条新的连接(请求2), 从新连接中读取相应量的数据并成功返回此次的读取过程, 当用户读取n次操作后( 即重复调用n次DFSInputStream的read方法),此时发生异常,重新建立连接3,继续往上次成功读取的地方开始读取,依此类推,直到该DataNode被标记为DeadNode,也就是一个大请求被分割成很多个并不成功的子请求。
在DFSInputstream上加入相应的日志信息后,此时显示并非如上的假设,Client端读取数据时并没有发生这样的异常,这种可能性情况排除在外。
分析Client端的DFSInputStream
,能得知在正常情况下(没有发生DN连接失败等错误),发生这种重连的原因基本只有一条:seek的时候发生了向前的seek操作。所以我们在这里增加了日志,位置在seek(long position)
,代码如下:
把加了日志的Client代码在一台Hbase RegionServer上线,观察一段时间,发现了如下的一些log:
相同时间段内的DataNode的log如下:
Debug信息的解读
从这些log里我们能得到如下的一些信息:
- 大体上是顺序读取,从前到后,但有一些小的偏差。
- 频度高,几百毫秒就会读取一次。
- 每次seekde偏差数都是一样的
33
,即前一个读取的最末尾在后一个读取的开始位置的下33个字节。
因为频度高跨度大并且连续,所以应该是个非常巨大的scan操作,验证了之前分析的场景:批量离线分析任务对表进行扫描。 这个神奇的数字33,每次都一样,那肯定是有原因的。我们用的是比较新的HBase,HFile是v3,看过HFile v3的说明文件就知道了,33是HFile的每个block(非HDFS里的Block)的header的长度。
另外,Client请求的offset和DataNode的实际的offset有些区别,是因为DataNode的实际的offset要用chunk进行对齐,所以会稍向前移动一点。
问题的大致位置
根据前面信息的解读,出问题的地方是执行scan的过程中,读取HFile的数据的时候,对于header的处理有问题,极有可能是之前读到了header,但因为某种原因再次读的时候没有找到,所以需要回过头去再读那33个字节。到目前为止,可论证该DataNode频繁输出这样日志的原因是由于client从过期的流中读取数据, 那么这样操作的原因是什么呢?
这是在HFileBlock中的代码, 在进行读block操作时,首先从ThreadLocal中取出上一次预读的block头信息。 根据日志输出栈的执行情况,可以知道每一个seek
操作在不同的线程 (B.defaultRpcServer.handler) 上执行的。
也就是说:
- A线程预读了下一个block blk的头信息,
- B线程从threadlocal中取出头信息,由于B线程的prefetchedHeader.offset = -1, 此时prefetchedHeader.offset == offset 等式不成立, 因此此次操作需要读取blk的头信息。
- 底层的流在实现上,由于预读了blk的头信息。因此, 此时出现 pos > targetPos的现象。 于是出现了类似于文中描述的现象。
regionserver scan的执行逻辑:
- http://www.cnblogs.com/foxmailed/p/3958546.html
- http://co2y.github.io/2016/07/26/hbase-sourcecode-reading-1/
- http://zjushch.iteye.com/blog/1235925
重现问题并最终确定问题点
在大致知道问题的原因后, 我们在测试环境中往一个表中插入大量的行, scan这个表之后,在_DataNode_上类似
这样的日志不期而至。使用patch修改过后,重新部署RegionServer之后,重新scan该表时,这些日志不复出现。
同时,注意到,并非仅仅有用户的scan操作才会产生这样的问题,Hbase Hlog的读也会产生这样的日志。将ThreadLocal切换成一个普通的synchronized方式,这为的是能够尽量的减少RegionServer与DataNode的建立连接的次数,有关更多细节以及讨论,可见下面 相关问题讨论
链接。
Patch
查看Jira ticket
Take away