Blazor Server 長時間運行 CPU 飆升到 99%?你可能忽略了這些細節
Copyright Notice: This article is an original work licensed under the CC 4.0 BY-NC-ND license.
If you wish to repost this article, please include the original source link and this copyright notice.
Source link: https://v2know.com/article/1135
主題:Blazor Server 長時間運行導致 CPU 飆升的根本原因與解法。
在使用 Blazor Server 架構開發網站時,我發現一個非常詭異的現象:
網站沒幾個訪客,卻在三天左右 CPU 緩慢飆升到 99%,只能靠重啟救急。
這個問題一直讓我懷疑是不是被攻擊,但檢查系統日誌、SSH 連線紀錄、.bash_history
等一切正常,最終才發現——是 Blazor Server 的「慢性資源洩漏」在搞鬼。
這篇文章總結了我實際經歷的幾個坑與完整解法,希望能幫助遇到相同問題的你。
問題症狀
-
部署的是 Blazor Server 應用(非 WASM)
-
平常訪客很少,但 CPU 長時間運行會從 2% 緩慢升到 99%
-
每次只能靠
reboot
解決,重啟後又回到 2%,然後又開始慢慢上升
常見誤解:SignalR 的 Timeout 就夠了?
我原本在 Program.cs
裡設定過:
builder.Services.Configure<HubOptions>(options =>
{
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
options.HandshakeTimeout = TimeSpan.FromSeconds(10);
});
這確實能讓斷線後 30 秒就關閉 SignalR 連線,但這「只處理網路層」,Blazor Server 會默默地把那個使用者的狀態(Circuit)保留 3 分鐘!
這段時間內,使用者頁面裡的元件、事件訂閱、計時器還在跑!
真正的核心問題
✅ 1. Circuit 沒及時清理 → 內存與 Timer 不斷累積
Blazor Server 的 Circuit 預設會在斷線後保留 3 分鐘。如果使用者不會 reconnect(例如已經關掉頁面),那這些元件就會變成「僵屍」。
✅ 2. 元件訂閱事件後沒取消 → 無法 GC
我有一個全局系統監控服務 SystemMetricsService
,每秒發送 CPU/RAM 使用率,元件透過事件訂閱接收:
MetricsService.OnMetricsUpdated += MetricsUpdated;
但沒有在元件銷毀時取消訂閱:
// 應該加上這段
public void Dispose()
{
MetricsService.OnMetricsUpdated -= MetricsUpdated;
}
這會讓 GC 認為該元件還在使用中,永遠無法回收,Component 越堆越多,CPU 越來越高。
解法總結
✅ 1. 正確配置 CircuitOptions
在 Program.cs
加上:
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents(options =>
{
options.DetailedErrors = builder.Configuration.GetValue<bool>("DetailedErrors");
// 設定 Circuit 斷線 30 秒內未恢復,就清理
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromSeconds(30);
// JS 端調用失敗不再卡住整個執行緒
options.JSInteropDefaultCallTimeout = TimeSpan.FromSeconds(5);
});
這段才是真正讓伺服器「丟掉使用者狀態」的關鍵!
✅ 2. 每個訂閱事件的元件都必須實作 IDisposable
@implements IDisposable
@code {
protected override void OnInitialized()
{
MetricsService.OnMetricsUpdated += Handle;
}
public void Dispose()
{
MetricsService.OnMetricsUpdated -= Handle;
}
}
不這樣做,Blazor 離開頁面後事件還是引用著整個元件,無法 GC,長時間下來必定造成資源壅塞。
✅ 3. 若使用 JSInterop + DotNetObjectReference,記得釋放
private DotNetObjectReference<MyComponent>? _ref;
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
_ref = DotNetObjectReference.Create(this);
JSRuntime.InvokeVoidAsync("yourInterop.init", _ref);
}
}
public void Dispose()
{
_ref?.Dispose(); // 🔥 這一行非常關鍵!
}
不 Dispose 的話,Blazor 還會一直記著這些 JS 端回傳 handle。
✅ 4. 可選補強:Circuit 活躍監控端點
加個 /metrics/circuits
端點,讓你在 production 可即時查看目前有多少活躍 Circuit:
public class TrackingCircuitHandler : CircuitHandler
{
private int _count;
public int CurrentCount => _count;
public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken ct)
{
Interlocked.Increment(ref _count);
return Task.CompletedTask;
}
public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken ct)
{
Interlocked.Decrement(ref _count);
return Task.CompletedTask;
}
}
註冊並加入:
builder.Services.AddSingleton<CircuitHandler, TrackingCircuitHandler>();
app.MapGet("/metrics/circuits", (TrackingCircuitHandler h) => new { active = h.CurrentCount });
成效觀察
我部署這些修改後,CPU 再也沒有出現「3 天慢慢爬到 99%」的情況,整體資源使用明顯下降,也不再需要定時重啟來“洗掉垃圾”。
總結
問題原因 | 解法 |
---|---|
Circuit 過久未釋放 | DisconnectedCircuitRetentionPeriod 設短一點 |
Component 訂閱事件未解除 | Dispose() 中解除事件 |
DotNetObjectReference 泄漏 | 手動呼叫 .Dispose() |
無法追蹤 Circuit 狀態 | 實作 TrackingCircuitHandler 曝露 /metrics/circuits |
Blazor Server 是強大但對資源非常敏感的架構,尤其要注意元件生命週期與服務註冊方式。如果你也遇到 CPU 飆高、記憶體膨脹的問題,不妨檢查一下上面幾個點,也許你會發現根本原因。
This article was last edited at