很是抱歉,此次故障给您带来麻烦了,请您谅解。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。