【故障公告】數據庫服務器 CPU 近 100% 引起的故障(源於 .NET Core 3.0 的一個 bug)

很是抱歉,此次故障給您帶來麻煩了,請您諒解。html

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

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 左右才恢復正常。github

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

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.app

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

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

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

相關文章
相關標籤/搜索