一次生產環境CPU佔用高的排查

是奉壹呀發表於2023-02-10

1. 專案背景

甲方是保密級別非常高的政府部門。所以我們全程拿不到任何測試資料,只能是自己模擬資料進行測試。
專案部署的時候,公司派了一人到甲方現場,在甲方客戶全程監督下,進行部署,除錯,匯入資料等工作。因為前期看不到真實的資料,所以很多功能都是憑客戶口述進行,到了現場難免發現問題。
這時需要改動程式碼重新打包部署,需要客戶特製隨身碟需入密碼才能傳到內網。總之,非常麻煩。
如果有什麼BUG,全靠同事口述。如果是功能性的什麼BUG還好,溝通雖然困難,大體都能很快解決。

但是,對的,“但是”雖遲但到,同事在現場遇到一個所謂"很奇怪"的問題。當匯入某一批檔案的時候,程式就卡在那裡不動了。

2. 技術背景

長話短說就是,透過頁面批次上傳檔案,後端接到檔案後會做一些word,pdf轉換按頁按段落提取文字,並透過訊息佇列kafka非同步進行下一步處理:包括不限於語義分析透過演算法提取出關鍵要素,以及要素之間的關聯等等。
嗯,不涉及具體業務,不算涉密。

同事描述的現象包括:

  • 1.程式卡住了
  • 2.kafka報錯了
  • 3.演算法返回500
  • 4.重啟了spring boot應用,kafka,演算法服務都沒法恢復。

以上是同事在現場用手機打字交流的一個簡單覆盤。非嚴格按照時間線來的。

2.1 程式卡住了

同事只給出了程式卡住的現象描述,沒給能出具體的節點。我只能透過不停的加日誌來確定具體卡在哪裡。
最終確定卡在某個演算法呼叫期間。

結合同事曾說過演算法呼叫出了問題,看看演算法問題。

2.2 演算法問題

演算法是我使用django封裝了一個python web介面。具說http請求返回了500,那應該是web服務內部異常。
但是奇怪的是,同事說演算法日誌沒有看到異常資訊。

因為NLP涉及到vocab,如果有生僻字或者vocab檔案裡不包含的字元,可能會導致演算法異常或者解析異常。

但同事反饋說沒有相關的異常。

2.3 kafka報錯了

這應該是一個烏龍。
我在spring kafkatemplate傳送訊息的時候列印了回撥訊息。

    * 向kafka傳送doc相關資訊
     * @param info
     */
    public void sendDocInfo(String info) {
        this.template.send(docTopic, info);
        ListenableFuture listenableFuture = this.template.send(docTopic, info);
        boolean bool = listenableFuture.isDone();
        log.info("kafka傳送狀態 :" + bool);
    }

在某個演算法呼叫期間一直卡住,超過了kafka預設的5分鐘的限制,返回了false。
換句話說,這裡的日誌不能表明kafka出了問題。它只是程式卡住的一個連鎖現象反映,遠不是本質原因。

2.4 重啟服務都沒用

兜兜轉轉,打包上傳到現場,客戶再手動匯入。時間一步步流逝。

聽到同事說重啟了spring boot應用,kafka,演算法應用都沒用以後,我一度有點懵逼了。
如果是某個執行緒一直空耗沒釋放,一定會導致CPU升高,(這裡插一句,提過top jstack檢視CPU高的執行緒的堆疊日誌),但是重啟應用肯定會釋放的呀。

這怎麼會重啟了還是這樣呢?

th.jpg

小半天就這麼過了,下午5點客戶就催下班了。又是羨慕體制內的一天。

3.重新確定方向

經過一晚的大腦放空,我覺得不能被零散的資訊牽著鼻子走。
我覺得我應該放過多餘資訊,直搗黃龍。
程式卡住了?這種系統假死問題,第一反應肯定是檢視CPU啊,有沒有佔用CPU高的程式,再檢視應用裡具體的是哪個執行緒在佔用著cpu,再透過jstack找到具體的應用程式碼。

就算是最後不是這個問題,以上操作也是一個非新手程式設計師的基操啊。

而且,為什麼重啟應用後會重現卡住的現象?因為kafka啊。
kafka為了確保不丟訊息。在生產端,叢集,消費端都需要做出努力。當然,這其實不是kafka獨有,任何訊息佇列都是相同的思路。
具體到kafka消費端,為保證不丟訊息,至少保證在沒有異常的情況下才做ack.
當程式假死的時候,沒有執行ack操作,那麼當程式重啟的時候,kafka當然會去做重新拉取資訊的動作。又造成程式假死。

這很合理,完全可以說得通。

當然這只是我上班路上的推測。

一大早,新的一天,一切清零。到公司,跟同事說了,他重啟了程式,沒有上傳檔案,透過後臺日志,觀察到kafka確實在消費之前的資料。

很好,驗證得到證實,完美的開始。

接下來,就是透過top命令檢視是否有CPU佔用高的現象。

涉密專案哦,可不敢可沒有截圖哦。

企業微信截圖_16760201883784.png

同事說到有個程式佔用CPU超過100%,從佔用時間上推測,從重啟過後就一直沒釋放。

非常好,驗證了第2個猜想。

再透過 top -H -p pid命令得到應用內佔用CPU高的具體執行緒。將執行緒ID轉為十六程式,再透過jastck grep 執行緒碼就可以定位到具體的程式碼。

具體的定位過程因為專案原因,不便不能展示過多,技術方面的細節參考我的另一篇文章( 【工作隨手記】deaklock排查)

告訴同事重點檢視帶有自己公司包名的程式碼,很快就定位到了程式碼位置。

因為完全與業務無關,可以把程式碼放上來。

/**
 * 獲取一個字串和關鍵詞,查詢這個字串出現的次數
 * @param str 原文
 * @param key 關鍵詞
 * @return
 */
public static int getStringCount(String str, String key) {
    int count = 0;
    int index = 0;
    while ((index = str.indexOf(key)) != -1) {
        count++;
        str = str.substring(index + key.length());
    }
    return count;
}

看到這個while我一瞬間就知道,癥結找到了。

當傳入的關鍵詞為""空串時,直接就會陷入while死迴圈。這就是程式假死的本質原因。

4.總結

從一下午找不到重點,到第二天半小時解決問題。我也在思考,這中間的差異出了什麼問題。

專案的特殊背景,是一個原因,但不是主要原因。身處這個行業,以後總要有這方面的需求和場景,不能每次都拿專案特殊性來背書和推脫。

我覺得以下幾點是我從這件事當中學到的,在今後的工作的應該儘量避免或者學習的。

  • 1.技術人應該相信自己的直覺。
  • 2.溝通應該要有效率。要有目的。區分主次。
  • 3.從紛繁的反饋中提取有效的資訊,是一種能力且可鍛鍊。
  • 4.保持清醒的大腦。否則,寧願停下來。

相關文章