问题解决了,我却不知道原因

关注公众号【高性能架构探索】,回复[pdf]免费获取计算机经典书籍

你好,我是雨乐!

上周在查一个诡异的coredump问题,今天,借助本文,重新复盘下整个问题的发生、排查以及解决过程。

背景

先说下需求背景吧。

今年因为整个行业处于萎靡状态,产品需求不像往常那么多了,所以,终于腾出手来做之前一直没有想做而没有精力做的事,从根本上优化整个引擎,提升引擎健壮性。

引擎中现有的两个比较重要的功能服务发现Promethus(普罗米修斯)监控系统。服务发现使用一个注册中心来记录分布式系统中的全部服务的信息,以便服务调用者能够快速的找到这些已注册的服务,而Promethus则是一套集监控、报警以及时间序的数据库组合。需要注意的是,Promethus需要单独起一个TCP端口供采集者调用使用。

在引擎中,使用服务发现来解决引擎服务中动态扩容或者缩容的问题,而使用Promethus则是为了监控和统计引擎中各个业务指标,比如服务rt、广告队列长度以及广告填充率等指标。本次问题,是因为服务发现和Promethus的结合使用导致的。为了能够让大家更方便的理解整个问题的过程,会从现状以及融合交互角度去讲述。

完全隔离

对于服务发现,当发现监控的节点发生变化时,重新获取节点下的ip:port端口,然后进行ReLoad(),向RPC调用方提供最新的活跃子服务信息,这样每次都向活跃的节点发生请求。

对于Promethus,在服务启动的时候,会指定默认ip列表,这样在数据统计的时候,仅针对默认ip列表中的ip进行统计。

std::string default_addr_list;
int OnChange(const std::vector<
        std::tuple<std::string, std::string>>& nodes) {
  std::vector<std::string> address_list;
  // get nodes info and push into address_list

  rpc->Reload(address_list);

}

int Init() {
  zk_client_ = std::make_shared<Zookeeper>();
  zk_client_->Init(zk_addr_);

  auto callback = std::bind(&OnChange, std::placeholders::_1);

  ret = zookeeper_client_->GetChildren(path, callback); // 设置回调函数,当监控的path路径下的节点有变化时,则调用OnChange

  prom_handler_ = std::make_shared<PromHander>(prom_config);
  prom_handler_->Init(default_addr_list);
}

在上述代码实现中,对于服务发现来说,当监测到的节点发生变化时候,重新获取该节点下所有的子节点信息,然后使用rpc->Reload()以加载最新节点列表信息。但是,对于Promethus来说,其对节点变化无感知,也就是说无论节点的增删,Promethus监控的节点都不会发生变化。

正常情况下,服务发现上的节点列表与Promethus的监控节点列表完全一致,如下图所示:

问题解决了,我却不知道原因

如果某一时刻,某个节点出现了故障导致服务不可用(假设以192.168.1.2所在机器发生了故障),那么服务发现会第一时间监测到,然后将其从可用列表中删除,而Promethus则无任何操作,如下图:

问题解决了,我却不知道原因

初次尝试

由于上一个方案不是很能满足现有的需求,尤其是当扩容的时候,不能获取新增节点的监控信息,所以就在想能不能使得服务发现和Promethus结合起来呢?也就是说,在服务发现监控到节点列表有变化的时候,在Promethus中使用最新的节点列表,但是,因为需要重新加载节点列表,所以需要新建一个Promethus Client,并使用新列表对其进行初始化。

代码如下:

std::string default_addr_list;
int OnChange(const std::vector<
        std::tuple<std::string, std::string>>& nodes) {
  std::vector<std::string> address_list;
  // get nodes info and push into address_list

  rpc->Reload(address_list);

  // 下面为新增逻辑

  prom_handler_ = std::make_shared<PromHander>(prom_config);
  prom_handler_->Init(address_list); // 此处使用最新活跃节点
}

int Init() {
  zk_client_ = std::make_shared<Zookeeper>();
  zk_client_->Init(zk_addr_);

  auto callback = std::bind(&OnChange, std::placeholders::_1);

  ret = zookeeper_client_->GetChildren(path, callback); // 设置回调函数,当监控的path路径下的节点有变化时,则调用OnChange

  prom_handler_ = std::make_shared<PromHander>(prom_config);
  prom_handler_->Init(default_addr_list);
}

测试环境下,一切正常,开始上线,灰度机器OK,开始全量。。。

突然运维发来一串消息,说是某个节点的Promethus端口不可达,我得乖乖,于是赶紧登陆该节点,netstat -antp | grep port,果然端口没有Listen。

分析源码发现,问题点在于如果Promethus Client连续两次Init(在Init接口中对端口),上一个Promethus正在被使用,也就是说端口还正在被使用,那么再次新建另外一个Promethus Client并调用Init接口的时候,会失败。

当新增节点192.168.1.5时候,Promethus重新进行初始化,然后192.168.1.1端口不可达,初始化失败(这是因为基于shared_ptr的特点,对handler重新赋值操作的时候,只会将之前的引用计数-1,由于其是shared_ptr,此时还有其他线程在使用,所以实际上并没有释放其资源,进而也就没有断开该连接,而其他节点Listen正常,完全是因为巧合),如下图:

问题解决了,我却不知道原因

再次尝试

既然我们已经知道了原因,那么有没有方式能够先断开连接,然后再进行释放操作呢?研究了Promethus CPP 客户端源码,发现其里面有Close()操作,但是并没有对外提供接口,看来,只能修改源码,将接口暴露出来。

问题解决了,我却不知道原因

重新编译三方库,一气呵成。

然后修改业务代码如下:

std::string default_addr_list;
int OnChange(const std::vector<
        std::tuple<std::string, std::string>>& nodes) {
  std::vector<std::string> address_list;
  // get nodes info and push into address_list

  rpc->Reload(address_list);

  Prom_handler_->Close(); // 新增代码,先关闭接口监听

  prom_handler_ = std::make_shared<PromHander>(prom_config);
  prom_handler_->Init(address_list); // 此处使用最新活跃节点
}

int Init() {
  zk_client_ = std::make_shared<Zookeeper>();
  zk_client_->Init(zk_addr_);

  auto callback = std::bind(&OnChange, std::placeholders::_1);

  ret = zookeeper_client_->GetChildren(path, callback); // 设置回调函数,当监控的path路径下的节点有变化时,则调用OnChange

  prom_handler_ = std::make_shared<PromHander>(prom_config);
  prom_handler_->Init(default_addr_list);
}

专门review了代码,一切OK。

问题解决了,我却不知道原因

然后编译,线上灰度,突然间收到报警,线上coredump:

问题解决了,我却不知道原因

此时的心情是这样的:

问题解决了,我却不知道原因

问题排查

赶紧登陆线上机器,使用屠龙术gdb xxx -c xxxx,查看堆栈信息:

问题解决了,我却不知道原因

看来跟这这次修改有关系(这不废话嘛)。在本地,使用 git diff命令查看本次的提交,研究了下代码,发现没啥问题呀,于是重新编译了下(此处为重点,本地默认使用了debug模式),然后再次在灰度机上启动,一切正常。

问题解决了,我却不知道原因

把线上的可执行文件拷贝到本地,尝试运行,与灰度机上现象一样:coredump。双端都是从master分支进行编译 ,所以代码是一样的,那么唯一的区别就是线上是release,而测试环境是debug,知道了这俩的区别后,在本地使用release方式进行编译,然后启动,与灰度机现象一样-coredump(只要能够复现,那就代表问题解决有望,万里长征走了一大半😃)。

使用优化前的代码(三方库不变,此时仍然感觉业务代码有问题),编译,本地运行,产生coredump,看来问题出在此次修改的三方库上(本次三方库增加的Close()函数没有在当前的业务代码中被使用,所以排除该函数原因)。

问题解决

在上一节中,定位到了原因是因为三方库导致,所以最便捷的方式是将三方库恢复到之前的版本,然后重新测试。

为了彻底解决问题,将本次增加的代码注释掉,重新编译三方库,结合优化前的业务代码,重新编译,运行。结果却出乎意料,仍然产生coredump。

问题解决了,我却不知道原因

这就太尴尬了,库的代码是之前的,业务代码也是之前的,仍然有问题。此时,只能将问题原因归咎于环境问题。

问题解决了,我却不知道原因

仔细查看了下编译环境,我滴乖乖,跟线上环境竟然不一致。

赶紧到另外一个环境进行编译,然后运行,一切正常。

使用最新的业务代码以及增加接口的三方库进行编译,然后运行,一切正常。

将可执行文件拷贝到线上灰度机,一切正常。

问题解决了,我却不知道原因

好了,截止到此,问题已经解决了,能够确认原因是因为编译环境不同导致的线上故障(三方库在本地编译然后提交代码库,而发布机则只编译业务代码),但是为什么编译环境能导致这个奇奇怪怪的问题,我也没有去深究(涉及到编译环境的,往往是个深坑,当然最根本的原因还是能力有限)。

问题解决了,我却不知道原因

结语

好了,此次问题终于解决了(虽然不知道最根本的原因 )。也算是给自己一个教训,后面在编译的时候,环境一定要跟线上完全一致,否则,只能自求多福了。

好了,本次的文章就到这,我们下期见!

版权归作者所有,如需转载请联系作者

(1)
上一篇 2022年4月15日 上午9:22
下一篇 2022年5月5日 上午10:13

相关推荐