緩存數據“消失”之謎

喫一塹,長一智。


“邪門!真是邪門!”自從踏入 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,方便通過一個關鍵字看到全流程。
  • 推測可能性,根據可能性設計實驗,排除可能性。
  • 不要急躁。冷靜下來,仔細思考,可能有哪些原因。
  • 遇到不解的問題,寫單測驗證想法(是否合理)。
  • 一定要注意那些最基礎的地方,你覺得最不太可能出問題的地方。
  • 拷貝代碼,一定要仔細審查代碼。可能有細小差別,卻能產生迥異結果。
  • 程序世界裏,事出必有因。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章