一次Namenode的RPC延迟故障排查引发的深入思考

35 篇文章 0 订阅
13 篇文章 1 订阅

前言

12月1日,多个省份的HDFS集群出现NameNodeRPC延迟高的问题,当时通宵了一个晚上,模糊的确认是存在频繁的du或者count操作导致的,后来也确认这个操作的发起方是开发人员用python写的采集脚本,统计数据量用的,会执行du命令,这次故障感觉还是挺有意思的,记录一下。

正文

问题排查

初步定位

过程的截图并不是记得很全,能列多少列多少吧。12月1日晚上20点左右接到电话,让协助查一下省集群任务跑不动的情况,当时看了下计算资源这些,并没发现异常的任务或者资源占用,可是任务就是跑不动。而且运行时间很久,按照经验当时就是怀疑是不是HDFS有点问题,于是看了下RPC情况,延迟高的吓人:
在这里插入图片描述
当时第一反应是查网络情况,确认网络都是正常的,也不存在丢包现象。NameNode的GC情况也很正常,整体来说,除了RPC高,就没有异常的指标了。于是整了30分钟的堆栈信息,看了下火焰图,感觉很可能是du或者count这种操作导致,但是并没有找到具体的操作,再加上HDFS的审计日志被关了,更是无从下手:
在这里插入图片描述

临时恢复

当天晚上我们把调度程序、计算任务等依次停了,然后又重启,依旧是没有找到问题的症结,由于当时依旧熬了一个通宵了,实在顶不住了,就没在继续看,交给乙方的厂商继续去排查;
emmm,没想到这事情一拖就是2天,这两天时间里,停了一些业务,任务倒是能够跑一些,但是集群整体可以看得出来还是不正常,RPC延迟一直居高不下:
在这里插入图片描述

定位可疑进程

于是和厂商人员协商,再停一次业务,彻底找到进程,这次不光停调度服务,要把所有访问的服务都停了,包括监控服务等,在停到一个数据量统计的监控服务的时候,发现RPC延迟出现了巨大的变化。
在这里插入图片描述
RPC监控图示中存在断点的原因是正好这个时候也把我的监控停了,20:20我们把嫌疑进程重启了一下,发现RPC再次冲高,而在20:30左右我们停了嫌疑进程后,RPC延迟立刻降低了:
在这里插入图片描述经过和开发人员确认,得知这个监控脚本回执行du -h的操作,但是理论上来说一个du命令不该会产生这么大的影响,临时的解决方案自然是停了采集脚本,问题原因却是还没找到

问题分析

本来这个事情我已经没在管了,后来某天,采集脚本的开发部门的一个成员找我,让我有时间看看脚本哪里有问题,他们是5分钟获取一次指标,正常来说不会出现这种问题,我就答应了,也算是自己学习下。

问题脚本分析

脚本的主函数如下:

def main():
    myname = socket.getfqdn(socket.gethostname())
    myip = socket.gethostbyname(myname)
    path = "/home/redpeak/location/jiajie/"
    info = pd.read_csv(path+"qzj_ip.csv",dtype=str)
    nn = info["nn1"].loc[info["ip"]==myip].iloc[0]
    try:
        port = int(9162)
        REGISTRY.register(NameNodeCollector("http://"+nn+":8088/cluster?user.name=admin", 'redpeak'))
        # REGISTRY.register(ResourceManagerNodeCollector(args.url, args.cluster))

        start_http_server(port)
        while True:
            time.sleep(300)
    except KeyboardInterrupt:
        print(" Interrupted")
        exit(0)

其实问题脚本的主要逻辑就是自己写了个prometheus的collect,然后就启用了一个http服务,看起来开发人员的逻辑确实是想300秒更新一次指标,但是按照他的逻辑我去写了一个测试的脚本:

from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, REGISTRY
from prometheus_client import start_http_server
from time import sleep
from random import random
class CustomCollector(object):
    def collect(self):
        G=GaugeMetricFamily('my_gauge', 'Help text', value=random())
        yield G
        print(G)
        c = CounterMetricFamily('my_counter_total', 'Help text', labels=['foo'])
        c.add_metric(['bar'], 1.7)
        c.add_metric(['baz'], 3.8)
        yield c
        print(c)
        # sleep模拟一下du延迟
        sleep(10)

REGISTRY.register(CustomCollector())
start_http_server(9163)
while True:
    sleep(300)

脚本内容很简单,就是注册一个Gauge指标,每次刷新都是一个随机数,然后我运行脚本,从浏览器去访问url直接看指标值,然后我发现一个严重的问题,貌似我每请求一次,这个值就变一次,也就是说如果我一秒发10次请求,就跑10次?那要是du命令不就完了。为了进一步验证我的想法,跟踪了一下线程:
在这里插入图片描述
程序没有获取到请求的时候,这里只有两个常驻的线程,然后我在浏览器开始请求,同时观察线程数:
在这里插入图片描述
这尼玛直接就炸了好吧。

问题原因分析

好了,现在很明显,脚本是有bug的:我们环境的prometheus是15秒去获取一次监控指标,若是上次在15秒能没获取到指标,du就还在后台跑,而这个时候,prometheus已经开始下一次的指标获取,由调用了一次,在遇到HDFS繁忙的时候,大量du命令积压,铁定会出现这个问题,而对于一些HDFS不繁忙的省份来说,由于每次请求能够快速返回结果,就不会出现这种情况,所以RPC延迟只在个别省份出现,但是这个隐患在全国都存在。
在这里插入图片描述

代码分析

问题的原因已经定位到了,接下来看看为什么这个代码会产生这种问题,我写了一个测试代码,也是获取HDFS指定路径的数据量大小,调的du命令,不过我是设置的Gauge指标:

测试代码

from prometheus_client import Gauge,start_http_server
import time
import os
hdfs_exporter_capacityTotal=Gauge("hdfs_exporter_capacityTotal","Space for HDFS configuration.",['provincecode','provincename','type'])

start_http_server(8003)
cmd="'hadoop fs -du -h hdfs://x/x/x/x/x/x/x/x/*"
while True:
    f=os.popen(cmd)
    shuchu = f.read().replace('\n',' ').split(' ')
    suchu=float(shuchu[0])
    hdfs_exporter_capacityTotal.labels(provincecode='province',provincename='provincename',type='servertype').set(suchu)
    time.sleep(15)

我这段代码就是15秒刷新一次指标,而不是每次访问都回去刷新指标那就只有去看看prometheus_client的源码了;

prometheus_client分析

测试代码

这段代码主要就是模仿官方的测试代码写的,主要就是相观察每次刷新web的时候,是怎么一个函数调用关系:

import random
import time
from prometheus_client import Gauge,start_http_server
from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, REGISTRY

hdfs_exporter_capacityTotal=Gauge("hdfs_exporter_capacityTotal","Space for HDFS configuration.",['provincecode','provincename','type'])

start_http_server(8003)

def random_gauge():
    time.sleep(2)
    return random.random()

class CustomCollector(object):
    def collect(self):
        G=GaugeMetricFamily('my_gauge', 'Help text', value=random_gauge())
        yield G
        print(G)
        c = CounterMetricFamily('my_counter_total', 'Help text', labels=['foo'])
        c.add_metric(['bar'], 1.7)
        c.add_metric(['baz'], 3.8)
        yield c
        print(c)

REGISTRY.register(CustomCollector())


while True:
    suchu=random.random()
    hdfs_exporter_capacityTotal.labels(provincecode='province',provincename='provincename',type='servertype').set(suchu)
    time.sleep(15)

直接使用Gauge和自定义Gauge,每次访问8003端口只有自定义的Gauge方法会被调用,看起来要从start_http_server方法入手,跟踪这个方法,根据我们的调试结果,定位到process_request方法:

def process_request(self, request, client_address):
    """Start a new thread to process the request."""
    if self.block_on_close:
        vars(self).setdefault('_threads', _Threads())
    t = threading.Thread(target = self.process_request_thread,
                         args = (request, client_address))
    t.daemon = self.daemon_threads
    self._threads.append(t)
    t.start()

找到启动的线程了:
在这里插入图片描述
在这里插入图片描述
这验证了每次请求会多启动一个线程,多个请求发过来,线程就会启动很多,而我的函数之所以不会产生这个问题,是因为我的业务逻辑是由while True控制的,无论发送多少次get请求,并没有绑定对应的方法。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Meepoljd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值