前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >问题解决了,我却不知道原因

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

作者头像
高性能架构探索
发布2022-08-25 16:08:13
3900
发布2022-08-25 16:08:13
举报
文章被收录于专栏:技术随笔心得

你好,我是雨乐!

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

背景

先说下需求背景吧。

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

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

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

完全隔离

完全隔离,是服务发现的节点列表和Promethus的节点列表可以允许不一致,这种方式实现最简单,也不会出现其它问题。

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

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

代码语言:javascript
复制
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,并使用新列表对其进行初始化。

代码如下:

代码语言:javascript
复制
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()操作,但是并没有对外提供接口,看来,只能修改源码,将接口暴露出来。

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

然后修改业务代码如下:

代码语言:javascript
复制
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。

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

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

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

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

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

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

结语

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能架构探索 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 完全隔离
  • 初次尝试
  • 再次尝试
  • 问题排查
  • 问题解决
  • 结语
相关产品与服务
微服务引擎 TSE
微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档