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高的執行緒的堆疊日誌),但是重啟應用肯定會釋放的呀。
這怎麼會重啟了還是這樣呢?
小半天就這麼過了,下午5點客戶就催下班了。又是羨慕體制內的一天。
3.重新確定方向
經過一晚的大腦放空,我覺得不能被零散的資訊牽著鼻子走。
我覺得我應該放過多餘資訊,直搗黃龍。
程式卡住了?這種系統假死問題,第一反應肯定是檢視CPU啊,有沒有佔用CPU高的程式,再檢視應用裡具體的是哪個執行緒在佔用著cpu,再透過jstack找到具體的應用程式碼。
就算是最後不是這個問題,以上操作也是一個非新手程式設計師的基操啊。
而且,為什麼重啟應用後會重現卡住的現象?因為kafka啊。
kafka為了確保不丟訊息。在生產端,叢集,消費端都需要做出努力。當然,這其實不是kafka獨有,任何訊息佇列都是相同的思路。
具體到kafka消費端,為保證不丟訊息,至少保證在沒有異常的情況下才做ack.
當程式假死的時候,沒有執行ack操作,那麼當程式重啟的時候,kafka當然會去做重新拉取資訊的動作。又造成程式假死。
這很合理,完全可以說得通。
當然這只是我上班路上的推測。
一大早,新的一天,一切清零。到公司,跟同事說了,他重啟了程式,沒有上傳檔案,透過後臺日志,觀察到kafka確實在消費之前的資料。
很好,驗證得到證實,完美的開始。
接下來,就是透過top命令檢視是否有CPU佔用高的現象。
涉密專案哦,可不敢可沒有截圖哦。
同事說到有個程式佔用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.保持清醒的大腦。否則,寧願停下來。