当前位置:首页 > 技术分析 > 正文内容

记一次 Nacos 导致的 CPU 飙高问题 !

ruisui883个月前 (02-03)技术分析17

来源:juejin.cn/post/7166075966615126029

今天下午突然 出现 测试环境 cpu飙高,干到了 60%,其他项目 响应时间明显变长。。。有点吓人,不想背锅

项目背景

出问题的项目是 需要连接各个不同nacos 和不同的 namespace 进行对应操作的 一个项目,对nacos的操作都是httpClient 调用的api接口,「httpClient方法 没有问题,不用质疑这个」

定位问题

首先 这 cpu高了,直接top -Hp 看看

定位到 进程id,然后 执行 jstack 进程id -> 1.txt

看到堆栈信息 ,下面提示信息有很多

"com.alibaba.nacos.client.config.security.updater" #2269 daemon prio=5 os_prio=0 tid=0x00007fa3ec401800 nid=0x8d85 waiting on condition [0x00007fa314396000]
   java.lang.Thread.State: TIMED_WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000f7f3eae0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1093)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

但是上面这个提示信息 显示 是 线程内部的,而且是nacos client 内部的

图片

你这么搞,让我很难受啊,我都是http 调用的,当时就是为了 防止开启无用的线程,这。。。。。「怎么」

那我去 根据你的关键字找找 是哪里打印的,「关键字 com.alibaba.nacos.client.config.security.updater」

ServerHttpAgent 类的方法

// init executorService
this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setName("com.alibaba.nacos.client.config.security.updater");
        t.setDaemon(true);
        return t;
    }
});

这是构造方法啊,应该只初始化一次的啊,往上debug,我靠,NacosConfigService 类中调用了,「debug 看什么时候调用了 不就行了嘛」

项目初始化的时候 调用了一次,业务系统依赖nacos嘛,ok 可以理解

再就是漫长的等待,30s后 发现又是一次调用,我去,怎么可能。。。

往回debug,代码如下

scheduler.schedule("定时校对灰度nacos 配置", () -> loadGrayConfig(grayFileName),
    1800, 1800, TimeUnit.SECONDS);
/**
 * 灰度配置更新 解决 网络隔离的问题
 *
 * @param grayFileName 灰度文件的名称
 */
private void loadGrayConfig(String grayFileName) {
    synchronized (this) {
        System.err.println("loadGrayConfig datetime: " + DateUtils.formatDate(new Date()));
        //刷一次 缓存 重新获取nacos 内容 赋值
        grayConfigManager.loadNoCache(grayFileName);
    }
}

图片

等会,难道 小丑是我。。。。

图片

这当时是为了灰度功能,定时数据校验用的 用了一个线程池,当时以为用了线程池 妥妥的。。。还特意调用的 Nocache 方法,让他创建新的nacos Config对象,做数据校对

「但是每调用一次 NacosFactory.createConfigService(properties) ,nacos config 构造器就会开一个线程,就导致了这个问题」

这里可能你要问了 你说 为了防止网络隔离 才加的这个调度任务,什么是网络隔离啊?

图片

我刚开始听说这个概念是 当时学习 Raft

假设一个Raft集群拥有三个节点,其中节点3的「网络被隔离」,那么按照「BasicRaft」的实现,集群会有以下动作:

  • 节点3由于网络被隔离,收不到来自Leader的Heartbeat和AppendEntries,所以节点3会进入选举过程,当然选举过程也是收不到投票的,所以节点3会反复超时选举;节点3的Term就会一直增大
  • 节点1与节点2会正常工作,并停留在当时的Term

网络恢复之后,Leader给节点3发送RPC的时候,节点3会拒绝这些RPC理由是发送方任期太小。

Leader收到节点3发送的拒绝后,会增大自己的Term,然后变成Follower。

随后,集群开始新的选举,大概率原本的Leader会成为新一轮的Leader。

那么网络隔离 Raft是怎么解决的呢?

多轮投票的安全问题是棘手的,必须避免同一高度不同轮数分别提交两个不同区块的情形。在Tendermint中,这个问题可以通过锁机制(locking mechanism)得到解决。

锁定规则:「预投票锁(Prevote-the-Lock)」

验证者只能「预投票(pre-vote)」 他们被锁定的区块。这样就阻止验证者在上一轮中预提交(pre-commit)一个区块,之后又预投票了下一轮的另一个区块。

· 波尔卡解锁(Unlock-on-Polka ):验证者只有在看到更高一轮(相对于其当前被锁定区块的轮数)的波尔卡之后才能释放该锁。这样就允许验证者解锁,如果他们预提交了某个区块,但是这个区块网络的剩余节点不想提交,这样就保护了整个网络的运转,并且这样做并没有损害网络安全性。

「解决方案是把term替换成(term, nodeid),并且按照字典序比较大小(a > b === a.term > b.term || a.term == b.term && a.nodeid > b. node_id). 这是paxos里的做法, 保证不会出现raft里的冲突.」

原理是, raft对voting的阶段有2个值来描述: term和当前投了哪个node_id, 即[term, nodeid], 由于raft不允许一个term vote2个不同的不同的node, 也就是说, vote_req.term > local.term && vote_req.nodeid == local.nodeid 才会grant这个vote请求.

把term替换成(term,nodeid)后, vote阶段的大小比较变成了: vote_req.term > local.term || vote_req.term == local.term && vote_req.nodeid >= local.nodeid, 条件边宽松了. 同一个term内, 较大nodeid的可以抢走较小nodeid 已经建立的leader.

而日志中原本记录的term也需要将其替换成(term, node_id), 因为这两项加起来才能唯一确定一个leader. 之前raft里只需一个term就可以唯一确定一个leader.

vote中比较最大log id相应的,从比较tuple (term, index) 改成比较tuple (term, node_id, index).

就这么点修改.

「总结下来就是 按照字典排序 和 预投票锁 保证 当多个 term 相同的 candidate 相遇后,肯定会有一个 获得多数派投票」

想法

我们如果出现 异常的网络隔离情况再回来,可能导致 数据的不一致,但是上面的 解决办法 因为 比较重,不适合我们,我们就单纯 引入 「定时校对的调度任务 进行比较(和 对账一样)」

修复

我对nacos config 连接进行 遍历查找 是否存活,不存活 我就shutdown,然后生成一个新的,而不是这种全部生成一边,毕竟人家 构造器开了线程。。。。

说回来还是因为 我当时自信了,没往这个调用下面看,在子类中 写的开线程 哈哈,行吧,改改 ,跑到测试环境 看看效果(CPU)

图片

嗯嗯 稳定了,明天再看看,应该没问题了

彩蛋

好像 测试环境响应时间 变长,和我没关系。。。。是别人压测呢,把带宽吃了。。。。。「看透不说透」


原文链接:
https://mp.weixin.qq.com/s/duH1fslmIqUVEyeHJxc0aA

扫描二维码推送至手机访问。

版权声明:本文由ruisui88发布,如需转载请注明出处。

本文链接:http://www.ruisui88.com/post/1100.html

标签: nocache
分享给朋友:

“记一次 Nacos 导致的 CPU 飙高问题 !” 的相关文章

vue中组件之间的通信方式

** 1.1 父子组件**a. 父向子传数据: 第1种: 父通过属性传值,子组件通过props接收数据(注:props传过来的数据是单向的,不可以进行修改)第2种:子组件可以通过$parent来获取父组件里的数据和调用父组件的方法(注:数据是双向的,还要注意如用了UI组件并且在该UI组件里重新定义一...

学会使用Vue JSX,一车老干妈都是你的

作者:子君转发链接:https://mp.weixin.qq.com/s/eAOivpHeowLShfwPfW8-BA?君自前端来,应知前端事。需求时时变,bug改不完。?连续几篇文章,每篇都有女神,被老铁给吐槽了,今天不提了女神了,反正女神都是别人的(扎心了)。这两天小编看了腾讯与老干妈的事情,晚...

GitLab-合并请求

描述合并请求可用于在您对项目进行的其他人员之间交换代码,并轻松与他们讨论更改。合并请求的步骤步骤1-在创建新的合并请求之前,GitLab中应该有一个创建的分支。您可以参考本章来创建分支-步骤2-登录到您的GitLab帐户,然后转到“ 项目”部分下的项目 -步骤3-单击“ 合并请求”选项卡,然后单击“...

最快清除数组空值?分享 1 段优质 JS 代码片段!

本内容首发于工粽号:程序员大澈,每日分享一段优质代码片段,欢迎关注和投稿!大家好,我是大澈!本文约 600+ 字,整篇阅读约需 1 分钟。今天分享一段优质 JS 代码片段,用最简洁的代码清除了数组中的空值。老规矩,先阅读代码片段并思考,再看代码解析再思考,最后评论区留下你的见解!const arr...

Vue真是太好了 壹万多字的Vue知识点 超详细!

1??、Vue和其他两大框架的区别Angular 学习成本太高React 代码可读性差Vue 学习成本较低 很容易上手VUE官方: https://cn.vuejs.org/v2/guide/comparison.html?2??、Vue是什么Vue是一套用于构建用户界面的渐进式框架 "前端...

SpringBoot2.X+Vue+UniAPP,全栈开发医疗小程序

//xia仔のke:chaoxingit.com/208/全栈开发医疗小程序:利用Spring Boot 2.X、Vue和UniApp在当今数字化时代,医疗行业也在不断地迈向信息化和智能化的方向。开发一款医疗小程序,能够为用户提供便捷的医疗服务和信息查询,为医疗机构提供高效的管理和服务渠道。本文将介...