喫一塹,長一智。
“邪門!真是邪門!”自從踏入 Go 的領域之後,奇事怪事接連不斷。很多看上去似乎沒啥問題的代碼,可就是有問題,可怎麼也看不出問題所在。
問題背景
事情是這樣的:有兩個流程和一個緩存數據:
流程一:接收 kafka 數據,解析模型數據,並存入緩存 modelCache: localCache[hash]Model 裏;
流程二:接收告警數據,計算對應的 hash,從 modelCache 中取出對應 hash 的 model ,進行後續的匹配。
奇怪在哪裏呢 ?
在流程一中,通過打日誌,發現 hash 對應的模型確實是存在的;然而在流程二中從 modelCache 裏怎麼也取不到這個 hash 對應的模型。
排查過程
走查代碼
簡單走查了下代碼,檢查了下初始化、依賴注入、使用方式等,沒看出問題。
打印日誌
爲了方便查看,我把流程中都加了 "denoise detection" 關鍵字,這樣就能看到所有相關流程的日誌。
取不到模型,自然第一時間想到就是 hash 是不是對不上 ?於是我把 hash 打出來,是一個 hash。再通過 hash 搜索,發現也是同一個 hash。
進而想到,會不會在流程中設置緩存之後,又有地方把緩存給清了?於是,我把清緩存的所有地方都註釋掉了。可是還是拿不到模型。
我又在流程的結束處打印緩存裏的信息,是有的。
真是奇怪啊!加載模型的時候緩存是有的,流程結束時緩存也是有的,中間也沒有清緩存的地方,爲啥另一個流程來取的時候就沒有了呢?
試試替代方案
爲了調通流程,我就先用 Map 來直接替代緩存了。發現在一個流程中把模型存入 map 然後在另一個流程中取出 map 的 模型是可行的。
有了替代方案,心裏多少踏實了一點。
進一步探查
難道是在不同線程中運行取不到 ?可都是訪問的同一個單例對象的公共緩存,且取不到模型的日誌是在加載模型之後,應該也不存在時間先後導致的問題。爲了驗證這個問題,我寫了個單測,特地在一個協程里加載模型,在另一個協程裏訪問模型,是取得到的。
實在令人百思不得其解啊!
func TestDenoiseModelLoad(t *testing.T) {
str := `
{
"version_id": "1706608200001",
"is_end": true,
"detection_reduction_model": [{
"tenant_id": "77fc7d4a115bb8b512fa",
"model_key_hash": "9b7867b3dbf1533d7dbb87e1e2cc2c155bd136f72d53be2d7f36a72132d441d1",
"detection_method": "BYPASSUAC_FILE_MONITOR",
"detection_type": "BypassUAC",
"detection_model_id": "490262997608955948",
"reduction_process_entity": ["C:\\Windows\\System32\\dllhost.exe", "dllhost.exe"],
"profiles": [{
"create_time": 1706608200001,
"reduction_rule_type": null,
"reduction_process_command_serial": "{\"serial\":\"C:\\\\WINDOWS\\\\system32\\\\DllHost.exe /Processid:{3AD05575-8857-4850-9277-11B85BDB8E09}\",\"info\":[],\"rem\":1}",
"reduction_file_path_serial": "{\"serial\":\"C:\\\\Windows\\\\assembly\\\\NativeImages_v4.*.*_*\\\\Accessibility\\\\*\\\\Accessibility_測試.ni.dll\",\"info\":[{\"pos\":36,\"type\":\"[NUM]\",\"vals\":[]},{\"pos\":38,\"type\":\"[NUM]\",\"vals\":[]},{\"pos\":40,\"type\":\"[NUM]\",\"vals\":[]},{\"pos\":56,\"type\":\"[GEN]\",\"vals\":[]}],\"rem\":4}",
"reduction_rule_id": "44a20767-ee56-4061-b284-fb9f9b1a9627"
}]
}]
}`
denoiseMap := denoise_utils.LoadModelBySDK(str)
fmt.Println(denoiseMap)
modelCache := cache.NewLocalCache[denoisemodels.BaseModelDispatch](
cache.WithCapacity(1000),
cache.WithTTL(time.Hour*2))
for modelHash, model := range denoiseMap {
modelCache.Set(modelHash, model)
}
go func() {
model, _ := modelCache.Get("9b7867b3dbf1533d7dbb87e1e2cc2c155bd136f72d53be2d7f36a72132d441d1")
fmt.Println(model)
}()
}
請教同事
我去問了下公司比較厲害的一位同事,他運行了下緩存實現的多線程訪問的測試用例,也沒有看出問題。不過他提醒了一句:會不會是取到的不是同一個緩存?
也許有可能。他建議我打印這個緩存的地址看看。我打印了一下(如上圖所示),發現地址也是一致的!難道是初始化時有問題?但這是一個依賴注入,理論上,注入是沒有問題的。
靈光一現
直覺上,我覺得,這要麼是個天坑,要麼是一個很小的細節導致,但是我暫時不知道它藏在哪裏。不過,基本可以肯定的是,所依賴的緩存實現很可能是沒有問題的,雖然我一度懷疑它有問題,甚至想用另一個緩存實現來替代下。
現在,似乎越來越迷惑了,但越來越接近真相了。我感覺它近在咫尺,但我還是看不到它。
突然,有一個想法躍入我的腦海:既然加載模型取不到,而在流程結束時又取得到,而緩存又是在同一個對象裏訪問的,那麼它究竟是在什麼時候消失的呢?
想到一個主意:我在流程的結束處,每隔 5s 打印一次緩存信息。如果每隔 5s 能取到模型信息,那就沒理由在告警來的時候又取不到。
說幹就幹。我加了一些日誌。重新部署,然後查看日誌,發現:流程結束後模型是能取到的,但是隔 5s 後就取不到了。 讀者,看到這裏,你想到了什麼?
真相大白
我寫了個測試用例。發現隔幾秒後真的取不到。
咦?按理來說,不應該啊,API 怎麼會犯這種錯誤?我又把 timex.Hours*2 *改成 timex.Hours*100000000*2,還是取不到。
func TestLocalCache(t *testing.T) {
var emptyCache cache.LocalCache[denoisemodels.BaseModelDispatch]
fmt.Println(&emptyCache)
modelCache := cache.NewLocalCache[denoisemodels.BaseModelDispatch](
cache.WithCapacity(1000),
cache.WithTTL(timex.Hours*2))
fmt.Println(&modelCache)
modelCache.Set("d3aec278bab046cc9d31e6d72897153a103ec81ca8c658ec2bcecc5fad238e81", &behavior.BehaviorModelDispatch{})
modelGet, ok := modelCache.Get("d3aec278bab046cc9d31e6d72897153a103ec81ca8c658ec2bcecc5fad238e81")
fmt.Println(modelGet)
fmt.Println(ok)
time.Sleep(time.Second * 5)
modelGet2, ok := modelCache.Get("d3aec278bab046cc9d31e6d72897153a103ec81ca8c658ec2bcecc5fad238e81")
fmt.Println(modelGet2)
fmt.Println(ok)
}
然後請 TL 幫忙一起看下。突然發現,自定義的 timex.Hours = 24,但實際上 Go 表示時間間隔,有個 Duration 的概念。應該是用 time.Hour*2,其中 Hour 的定義如下。
改成 time.Hour 就能夠取到模型了!
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
啓示
遇到令人費解的問題,怎麼辦呢?
- 走查代碼。看看有木有肉眼可見的細小錯誤。
- 打詳細日誌。如果是長流程,可以加相同的前綴日誌,或者同一個業務ID,方便通過一個關鍵字看到全流程。
- 推測可能性,根據可能性設計實驗,排除可能性。
- 不要急躁。冷靜下來,仔細思考,可能有哪些原因。
- 遇到不解的問題,寫單測驗證想法(是否合理)。
- 一定要注意那些最基礎的地方,你覺得最不太可能出問題的地方。
- 拷貝代碼,一定要仔細審查代碼。可能有細小差別,卻能產生迥異結果。
- 程序世界裏,事出必有因。