背景
最近一位朋友找到我,讓我幫看他們的一個aspnet core service無端cpu高的問題。從描述上看,這個service之前沒有出現過cpu高的情況,最近也沒有改過實際的什麼code。很奇怪了,會有什麼變化導致cpu上去了呢?
分析
由於比較容易復現 (據說一啟動service,cpu就上去了),我便讓那位朋友在cpu高的時候直接手動把.net程式dump了一下。於是就開始用windbg分析了
先看一下案發時程式中的執行緒情況,畢竟它們是讓程式動起來的源泉哈。大部分執行緒都執行到如下類似位置(下面的callstack是虛擬化的,因為為了朋友的隱私,code已經虛擬化):
這裡可以看出有約38/2=19個執行緒執行到Services.CronJob+d__1.MoveNext這個位置。我又問了那位朋友,當時的執行環境是大約20個cpu core。真巧哈,幾乎所有cpu core都很有可能跑到了這個地方了。
注:上面如何知道有38/2個執行緒,而不是38個執行緒呢?這是因為一般來說,當某個函式正在被呼叫時,callstack中會顯示出兩次,如圖哈:
看到沒,在"current frame"下面顯示的上一層呼叫關係中會也顯示這個方法,此時它是callee哈。
那麼這個Services.CronJob+d__1.MoveNext是何方神聖呢,名字叫cpu killer更貼切吧??
跑題了,去看看這個方法的程式碼。注意到這個是compiler generated code,所以就先看看generated code長啥樣吧:
先利用上圖中透露出來的method的md地址,用!dumpmd看一下這個方法的資訊:
反編譯看一下:
天啦擼,這好像是async state machine code,沒事湊合看吧?仔細觀察那些標紅的位置後,我發現這裡面有些蹊蹺啊。如果schedules不是null的話,有兩種情況,這兩種情況最終都會到label_10的地方,在那裡,cpu可以得到休息;但如果schedules是null的話,會不會一直以同步的方式高速迴圈在while裡?我在所有threads的stack上找尋,找不到有型別為List<string>的instance被stack引用著,所以很有可能是這個原因了。
另外,從Services.CronJob+d__1.MoveNext這個名字看,這個closure應該是在user code class CronJob中生成的,我們去看看。用!dumpheap:
正好19個CronJob, 這和剛分析的有19個執行緒正高速執行吻合。因為心情不錯,所以再去看看CronJob的樣子吧:
看起來這個CronJob是個long running的IHostedService,在迴圈中有的flow沒有讓thread休息導致cpu上來了。
後記
1 protected override async Task ExecuteAsync(CancellationToken stoppingToken) 2 { 3 await InitializeJobAsync(); 4 5 while (!stoppingToken.IsCancellationRequested) 6 { 7 List<string>? schedules = PreferenceService.GetSchedules(Region); 8 if (schedules == null) 9 { 10 11 } 12 else 13 { 14 await ProcessJobAsync(schedules); 15 await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); 16 } 17 } 18 }
1 protected override async Task ExecuteAsync(CancellationToken stoppingToken) 2 { 3 await InitializeJobAsync(); 4 5 while (!stoppingToken.IsCancellationRequested) 6 { 7 List<string>? schedules = PreferenceService.GetSchedules(Region); 8 9 await ProcessJobAsync(schedules); 10 await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); 11 } 12 }
於是原因便是,當時加了這個empty if block後,由於PreferenceService.GetSchedules總不是null,所以沒有進入問題flow;後來配置變了,返回了null,進入問題flow,問題flow又錯過了await Task.Delay(),所以悲劇發生。。
據這位朋友說,他的同事後來說:"這個empty if block當時是個placeholder。現在不用了,可以刪了" ……。。唉,刪是好刪,不過在發現root cause之前可是讓這位朋友背了不少時間的鍋哈