1. 程式人生 > >【故障公告】資料庫伺服器 CPU 近 100% 引發的故障(源於 .NET Core 3.0 的一個 bug)

【故障公告】資料庫伺服器 CPU 近 100% 引發的故障(源於 .NET Core 3.0 的一個 bug)

非常抱歉,這次故障給您帶來麻煩了,請您諒解。

今天早上 10:54 左右,我們所使用的資料庫服務(阿里雲 RDS 例項 SQL Server 2016 標準版)CPU 突然飆升至 90% 以上,應用日誌中出現大量資料庫查詢超時的錯誤。

Microsoft.Data.SqlClient.SqlException (0x80131904): Execution Timeout Expired.  The timeout period elapsed prior to completion of the operation or the server is not responding.
 ---> System.ComponentModel.Win32Exception (258): Unknown error 258

我們收到告警通知並確認問題後,在 11:06 啟動了阿里雲 RDS 的主備切換, 11:08 完成切換,資料庫 CPU 恢復正常。但是關鍵時候 docker swarm 總是雪上加霜,在資料庫恢復正常後,部署部落格站點的 docker swarm 叢集有一個節點出現異常情況,部分請求會出現 50x 錯誤,將這個異常節點退出叢集並啟動新的節點後在 11:15 左右才恢復正常。

通過阿里雲 RDS 控制檯的 CloudDBA 發現了 CPU 近 100% 期間執行次數異常多的 SQL 語句。

SELECT TOP @__p_1 [b].[TagName] AS [Name], [b].[TagID] AS [Id], [b].[UseCount], [b].[BlogId]
FROM [blog_Tag] [b]
WHERE [b].[BlogId] = @__blogId_0
    AND @__blogId_0 IS NOT NULL
    AND [b].[UseCount] > ?
ORDER BY [b].[UseCount] DESC

上面的 SQL 語句是 EF Core 3.0 生成的,其中加粗的  IS NOT NULL  就是 EF Core 3.0 的一個臭名還沒昭著的 bug —— 生成 SQL 語句時會生成額外的  IS NOT NULL  查詢條件。

誰也沒想到(連微軟自己也沒想到)這個看似無傷大雅的多此一舉卻存在致命隱患 —— 在某些情況下會讓整個資料庫伺服器 CPU 持續 100% (或者近 100%)。一開始遇到這個問題時,我們也沒想到,還因此錯怪了阿里雲(博文連結),後來在阿里雲資料庫專家分析了我們遇到的問題後才發現原來罪魁禍首是 EF Core 生成的多餘的 "IS NOT NULL" ,它會在某些情況下會造成 SQL Server 快取了效能極其低下(很耗CPU)的執行計劃,然後後續的查詢都走這個執行計劃,CPU 就會居高不下。這個錯誤的執行計劃有雙重殺傷力,一邊巨耗資料庫 CPU ,一邊造成對應的查詢無法正常完成從而查詢結果不能被快取到 memcached ,於是針對這個執行計劃的查詢就越多,雪崩效應就發生了。唯一的解決方法就是清除這個錯誤的執行計劃快取,主備切換或者重啟伺服器只是清除執行計劃快取的一種簡單粗暴的方法。

在我們開始遇到這個問題,就已經有人在 github 上反饋了這個問題:

Yeah this needs to be fixed asap. We just deployed code that uses 3.0 and had to immediately revert to 2.2 because simple queries blew up our SQL Azure CPU usage. Went from under 50% to 100% and stayed there until we rolled back.

但當時沒有引起微軟的足夠重視,在我們知道錯怪了阿里雲實際是微軟的問題之後,我們向微軟 .NET 團隊反饋了這個問題,這次得到了微軟的重視,很快就修復了,但是是通過 .NET Core 3.0 Preview 版釋出的,我們在非生產環境下驗證了  IS NOT NULL 的確修復了,由於是 Preview 版,再加上 .NET Core 3.1 正式版年底前會發布,所以我們沒有在生產環境中更新這個修復,只是將上次出現問題的複雜 SQL 語句改為用 Dapper 呼叫儲存過程。後來阿里雲資料庫專家進一步對我們的資料庫進行分析,連平時資料庫 CPU 的毛刺(偶爾跑高的波動)都與  IS NOT NULL  有關。

這就是這次故障的背景,在我們等待 .NET Core 3.1 正式版修復這個 bug 的過程中又被坑了一次,與上次不同的是這次出現問題的 SQL 語句非常簡單,而且只有一個 "IS NOT NULL" ,由此可見這個坑的殺傷力。

這個坑足以載入 .NET Core 的史冊,另一個讓我們記憶猶新的那次也讓我們錯怪阿里雲的 .NET Core 坑是正式版的 .NET Core 中 SqlClient 竟然漏寫了 Dispose ,詳見 雲端計算之路-阿里雲上:資料庫連線數過萬的真相,從阿里雲RDS到微軟.NET Core。