今天下午突然 出現(xiàn) 測試環(huán)境 cpu飆高,干到了 60%,其他項(xiàng)目 響應(yīng)時(shí)間明顯變長。。。有點(diǎn)嚇人,不想背鍋
項(xiàng)目背景
出問題的項(xiàng)目是 需要連接各個(gè)不同nacos 和不同的 namespace 進(jìn)行對應(yīng)操作的 一個(gè)項(xiàng)目,對nacos的操作都是httpClient 調(diào)用的api接口,httpClient方法 沒有問題,不用質(zhì)疑這個(gè)
定位問題
首先 這 cpu高了,直接top -Hp 看看
定位到 進(jìn)程id,然后 執(zhí)行 jstack 進(jìn)程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)
但是上面這個(gè)提示信息 顯示 是 線程內(nèi)部的,而且是nacos client 內(nèi)部的
你這么搞,讓我很難受啊,我都是http 調(diào)用的,當(dāng)時(shí)就是為了 防止開啟無用的線程,這。。。。。怎么
那我去 根據(jù)你的關(guān)鍵字找找 是哪里打印的,關(guān)鍵字 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; ????} });
這是構(gòu)造方法啊,應(yīng)該只初始化一次的啊,往上debug,我靠,NacosConfigService 類中調(diào)用了,debug 看什么時(shí)候調(diào)用了 不就行了嘛
項(xiàng)目初始化的時(shí)候 調(diào)用了一次,業(yè)務(wù)系統(tǒng)依賴nacos嘛,ok 可以理解
再就是漫長的等待,30s后 發(fā)現(xiàn)又是一次調(diào)用,我去,怎么可能。。。
往回debug,代碼如下
scheduler.schedule("定時(shí)校對灰度nacos?配置",?()?->?loadGrayConfig(grayFileName), ????1800,?1800,?TimeUnit.SECONDS);
/** ?*?灰度配置更新?解決?網(wǎng)絡(luò)隔離的問題 ?* ?*?@param?grayFileName?灰度文件的名稱 ?*/ private?void?loadGrayConfig(String?grayFileName)?{ ????synchronized?(this)?{ ????????System.err.println("loadGrayConfig?datetime:?"?+?DateUtils.formatDate(new?Date())); ????????//刷一次?緩存?重新獲取nacos?內(nèi)容?賦值 ????????grayConfigManager.loadNoCache(grayFileName); ????} }
?
?
等會(huì),難道 小丑是我。。。。
這當(dāng)時(shí)是為了灰度功能,定時(shí)數(shù)據(jù)校驗(yàn)用的 用了一個(gè)線程池,當(dāng)時(shí)以為用了線程池 妥妥的。。。還特意調(diào)用的 Nocache 方法,讓他創(chuàng)建新的nacos Config對象,做數(shù)據(jù)校對
但是每調(diào)用一次 NacosFactory.createConfigService(properties) ,nacos config 構(gòu)造器就會(huì)開一個(gè)線程,就導(dǎo)致了這個(gè)問題
這里可能你要問了你說為了防止網(wǎng)絡(luò)隔離才加的這個(gè)調(diào)度任務(wù),什么是網(wǎng)絡(luò)隔離啊?
我剛開始聽說這個(gè)概念是 當(dāng)時(shí)學(xué)習(xí) Raft
假設(shè)一個(gè)Raft集群擁有三個(gè)節(jié)點(diǎn),其中節(jié)點(diǎn)3的網(wǎng)絡(luò)被隔離,那么按照BasicRaft的實(shí)現(xiàn),集群會(huì)有以下動(dòng)作:
節(jié)點(diǎn)3由于網(wǎng)絡(luò)被隔離,收不到來自Leader的Heartbeat和AppendEntries,所以節(jié)點(diǎn)3會(huì)進(jìn)入選舉過程,當(dāng)然選舉過程也是收不到投票的,所以節(jié)點(diǎn)3會(huì)反復(fù)超時(shí)選舉;節(jié)點(diǎn)3的Term就會(huì)一直增大
節(jié)點(diǎn)1與節(jié)點(diǎn)2會(huì)正常工作,并停留在當(dāng)時(shí)的Term
網(wǎng)絡(luò)恢復(fù)之后,Leader給節(jié)點(diǎn)3發(fā)送RPC的時(shí)候,節(jié)點(diǎn)3會(huì)拒絕這些RPC理由是發(fā)送方任期太小。
Leader收到節(jié)點(diǎn)3發(fā)送的拒絕后,會(huì)增大自己的Term,然后變成Follower。
隨后,集群開始新的選舉,大概率原本的Leader會(huì)成為新一輪的Leader。
那么網(wǎng)絡(luò)隔離 Raft是怎么解決的呢?
多輪投票的安全問題是棘手的,必須避免同一高度不同輪數(shù)分別提交兩個(gè)不同區(qū)塊的情形。在Tendermint中,這個(gè)問題可以通過鎖機(jī)制(locking mechanism)得到解決。
鎖定規(guī)則:預(yù)投票鎖(Prevote-the-Lock):
驗(yàn)證者只能預(yù)投票(pre-vote) 他們被鎖定的區(qū)塊。這樣就阻止驗(yàn)證者在上一輪中預(yù)提交(pre-commit)一個(gè)區(qū)塊,之后又預(yù)投票了下一輪的另一個(gè)區(qū)塊。
波爾卡解鎖(Unlock-on-Polka ):驗(yàn)證者只有在看到更高一輪(相對于其當(dāng)前被鎖定區(qū)塊的輪數(shù))的波爾卡之后才能釋放該鎖。這樣就允許驗(yàn)證者解鎖,如果他們預(yù)提交了某個(gè)區(qū)塊,但是這個(gè)區(qū)塊網(wǎng)絡(luò)的剩余節(jié)點(diǎn)不想提交,這樣就保護(hù)了整個(gè)網(wǎng)絡(luò)的運(yùn)轉(zhuǎn),并且這樣做并沒有損害網(wǎng)絡(luò)安全性。
解決方案是把term替換成(term, nodeid),并且按照字典序比較大小(a > b === a.term > b.term || a.term == b.term && a.nodeid > b. node_id). 這是paxos里的做法, 保證不會(huì)出現(xiàn)raft里的沖突.
原理是, raft對voting的階段有2個(gè)值來描述: term和當(dāng)前投了哪個(gè)node_id, 即[term, nodeid], 由于raft不允許一個(gè)term vote2個(gè)不同的不同的node, 也就是說, vote_req.term > local.term && vote_req.nodeid == local.nodeid 才會(huì)grant這個(gè)vote請求.
把term替換成(term,nodeid)后, vote階段的大小比較變成了: vote_req.term > local.term || vote_req.term == local.term && vote_req.nodeid >= local.nodeid, 條件邊寬松了. 同一個(gè)term內(nèi), 較大nodeid的可以搶走較小nodeid 已經(jīng)建立的leader.
而日志中原本記錄的term也需要將其替換成(term, node_id), 因?yàn)檫@兩項(xiàng)加起來才能唯一確定一個(gè)leader. 之前raft里只需一個(gè)term就可以唯一確定一個(gè)leader.
vote中比較最大log id相應(yīng)的,從比較tuple (term, index) 改成比較tuple (term, node_id, index).
就這么點(diǎn)修改.
總結(jié)下來就是 按照字典排序 和 預(yù)投票鎖 保證 當(dāng)多個(gè) term 相同的 candidate 相遇后,肯定會(huì)有一個(gè) 獲得多數(shù)派投票
想法
我們?nèi)绻霈F(xiàn) 異常的網(wǎng)絡(luò)隔離情況再回來,可能導(dǎo)致 數(shù)據(jù)的不一致,但是上面的 解決辦法 因?yàn)?比較重,不適合我們,我們就單純 引入 定時(shí)校對的調(diào)度任務(wù) 進(jìn)行比較(和 對賬一樣)
修復(fù)
我對nacos config 連接進(jìn)行 遍歷查找 是否存活,不存活 我就shutdown,然后生成一個(gè)新的,而不是這種全部生成一邊,畢竟人家 構(gòu)造器開了線程。。。。
說回來還是因?yàn)?我當(dāng)時(shí)自信了,沒往這個(gè)調(diào)用下面看,在子類中 寫的開線程 哈哈,行吧,改改 ,跑到測試環(huán)境 看看效果(CPU)
嗯嗯 穩(wěn)定了,明天再看看,應(yīng)該沒問題了
編輯:黃飛
?
評(píng)論