SQL Server 启动失败(3417/15173/912)排查:通过 -T902 跳过升级并清理 ##MS_AgentSigningCertificate## 残留授权
1. 问题现象
我的某个 Spring Boot 项目启动时报错,核心日志如下:
Unable to start embedded Tomcat
...
ErrorCreateDataSourceException: druid create error
...
com.microsoft.sqlserver.jdbc.SQLServerException:
通过端口 1433 连接到主机 xxx.xxx.xxx.xxx 的 TCP/IP 连接失败。错误:“connect timed out”
进一步排查后发现,并不是应用本身有问题,而是 SQL Server 服务无法正常启动。
在 SQL Server ERRORLOG 中看到关键报错:
Error: 15173, Severity: 16, State: 1.
Server principal '##MS_AgentSigningCertificate##' has granted one or more permission(s).
Revoke the permission(s) before dropping the server principal.
Error: 912, Severity: 21, State: 2.
Script level upgrade for database 'master' failed because upgrade step 'msdb110_upgrade.sql' encountered error 15173.
Error: 3417, Severity: 21, State: 3.
Cannot recover the master database. SQL Server is unable to run.
2. 问题根因
这类问题的本质不是业务库连接失败,而是 SQL Server 在启动时执行系统库升级脚本失败。
从日志可以看出:
-
SQL Server 启动时在执行
msdb110_upgrade.sql -
升级过程中要处理系统登录
##MS_AgentSigningCertificate## -
但是该主体仍然存在权限依赖
-
导致升级脚本失败
-
进一步触发
912 -
最终
master数据库无法恢复,实例启动失败,报3417
一句话总结就是:
##MS_AgentSigningCertificate##这个系统主体没有被清理干净,导致 SQL Server 系统升级流程卡死。
3. 处理思路
由于实例正常启动时会立刻执行升级脚本,而升级脚本又会失败,所以常规方式无法登录数据库处理。
正确思路是:
-
使用
-T902跳过升级脚本启动 SQL Server -
用单用户模式连进去
-
查询
##MS_AgentSigningCertificate##相关的所有残留授权 -
动态生成并执行
REVOKE -
删除该登录
-
正常重启 SQL Server
4. 具体处理步骤
4.1 单用户模式 + 跳过升级脚本启动
若是找不到自己的Sql Server 路径则执行:
sc qc MSSQLSERVER
你会看到一段:
BINARY_PATH_NAME : "C:\Program Files\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQL\Binn\sqlservr.exe" -s MSSQLSERVER
这就是你的真实路径;
先进入 SQL Server 的 Binn 目录,例如:
cd /d "D:\Program Files\SQLServer\MSSQL16.MSSQLSERVER\MSSQL\Binn"
然后执行:
sqlservr.exe -mSQLCMD -T902
说明:
-
-mSQLCMD:单用户模式,只允许sqlcmd连接 -
-T902:跳过失败的升级脚本
如果不加 -T902,实例通常会在执行升级脚本时立刻再次退出。
4.2 用 sqlcmd 连接实例
另开一个管理员 CMD,执行:
sqlcmd -S localhost -E
5. 查询问题主体的残留权限
这里有一个非常重要的点:
不要把别人环境里的登录名写死成
[saa]。
在我的环境里,最终查出来的 grantee 确实是 saa,但在其他环境中,它可能是任意 login 或其他服务器主体。
因此应该先查,再生成语句。
5.1 查询直接授予 ##MS_AgentSigningCertificate## 的权限
先查看当前有哪些权限是直接授给它本身的:
SELECT
perm.class_desc,
perm.major_id,
perm.permission_name,
perm.state_desc,
grantee.name AS grantee_name,
grantee.type_desc AS grantee_type,
target.name AS target_name,
target.type_desc AS target_type,
ep.name AS endpoint_name
FROM sys.server_permissions perm
LEFT JOIN sys.server_principals grantee
ON perm.grantee_principal_id = grantee.principal_id
LEFT JOIN sys.server_principals target
ON perm.major_id = target.principal_id
LEFT JOIN sys.endpoints ep
ON perm.major_id = ep.endpoint_id
WHERE grantee.name = '##MS_AgentSigningCertificate##';
GO
如果查到了结果,就要先把这些权限撤掉。
为了避免手工拼 SQL 出错,可以直接生成撤销语句:
SELECT
'REVOKE '
+ CONVERT(nvarchar(128), perm.permission_name) COLLATE DATABASE_DEFAULT
+ CASE
WHEN perm.class_desc = 'SERVER' THEN ''
WHEN perm.class_desc = 'SERVER_PRINCIPAL' THEN
' ON ' +
CASE
WHEN target.type_desc = 'SERVER_ROLE' THEN 'SERVER ROLE::'
ELSE 'LOGIN::'
END +
QUOTENAME(CONVERT(nvarchar(256), target.name) COLLATE DATABASE_DEFAULT)
WHEN perm.class_desc = 'ENDPOINT' THEN
' ON ENDPOINT::' +
QUOTENAME(CONVERT(nvarchar(256), ep.name) COLLATE DATABASE_DEFAULT)
ELSE ''
END
+ ' FROM '
+ QUOTENAME(CONVERT(nvarchar(256), grantee.name) COLLATE DATABASE_DEFAULT)
+ ';' AS revoke_sql
FROM sys.server_permissions perm
LEFT JOIN sys.server_principals grantee
ON perm.grantee_principal_id = grantee.principal_id
LEFT JOIN sys.server_principals target
ON perm.major_id = target.principal_id
LEFT JOIN sys.endpoints ep
ON perm.major_id = ep.endpoint_id
WHERE grantee.name = '##MS_AgentSigningCertificate##';
GO
把查询结果中生成的 revoke_sql 执行即可。
5.2 查询由 ##MS_AgentSigningCertificate## 授给别人的权限
这一步是最关键的。
很多时候,即使已经撤掉了它“自己拥有的权限”,DROP LOGIN 仍然会失败,原因是:
它可能还作为 grantor,给别人授过权限。
查询语句如下:
SELECT
perm.class_desc,
perm.major_id,
perm.permission_name,
perm.state_desc,
grantor.name AS grantor_name,
grantor.type_desc AS grantor_type,
grantee.name AS grantee_name,
grantee.type_desc AS grantee_type,
target.name AS target_name,
target.type_desc AS target_type,
ep.name AS endpoint_name
FROM sys.server_permissions perm
LEFT JOIN sys.server_principals grantor
ON perm.grantor_principal_id = grantor.principal_id
LEFT JOIN sys.server_principals grantee
ON perm.grantee_principal_id = grantee.principal_id
LEFT JOIN sys.server_principals target
ON perm.major_id = target.principal_id
LEFT JOIN sys.endpoints ep
ON perm.major_id = ep.endpoint_id
WHERE grantor.name = '##MS_AgentSigningCertificate##';
GO
如果这里查到了记录,说明 ##MS_AgentSigningCertificate## 还在给别人“背书”,必须把这些授权也撤掉。
同样,不要写死别人环境中的 login 名,直接动态生成 REVOKE:
SELECT
'REVOKE '
+ CONVERT(nvarchar(128), perm.permission_name) COLLATE DATABASE_DEFAULT
+ CASE
WHEN perm.class_desc = 'SERVER' THEN ''
WHEN perm.class_desc = 'SERVER_PRINCIPAL' THEN
' ON ' +
CASE
WHEN target.type_desc = 'SERVER_ROLE' THEN 'SERVER ROLE::'
ELSE 'LOGIN::'
END +
QUOTENAME(CONVERT(nvarchar(256), target.name) COLLATE DATABASE_DEFAULT)
WHEN perm.class_desc = 'ENDPOINT' THEN
' ON ENDPOINT::' +
QUOTENAME(CONVERT(nvarchar(256), ep.name) COLLATE DATABASE_DEFAULT)
ELSE ''
END
+ ' FROM '
+ QUOTENAME(CONVERT(nvarchar(256), grantee.name) COLLATE DATABASE_DEFAULT)
+ ' CASCADE AS '
+ QUOTENAME(CONVERT(nvarchar(256), grantor.name) COLLATE DATABASE_DEFAULT)
+ ';' AS revoke_sql
FROM sys.server_permissions perm
LEFT JOIN sys.server_principals grantor
ON perm.grantor_principal_id = grantor.principal_id
LEFT JOIN sys.server_principals grantee
ON perm.grantee_principal_id = grantee.principal_id
LEFT JOIN sys.server_principals target
ON perm.major_id = target.principal_id
LEFT JOIN sys.endpoints ep
ON perm.major_id = ep.endpoint_id
WHERE grantor.name = '##MS_AgentSigningCertificate##';
GO
把生成出来的 revoke_sql 全部执行掉。
6. 我这次环境中的实际情况
本次环境里,查询结果大致是这样:
-
grantor_name = ##MS_AgentSigningCertificate## -
grantee_name = saa -
permission_name = ALTER -
class_desc = SERVER_PRINCIPAL -
major_id对应的对象也是##MS_AgentSigningCertificate##自己
所以最终执行的实际语句是:
REVOKE ALTER ON LOGIN::[##MS_AgentSigningCertificate##]
FROM [saa]
CASCADE
AS [##MS_AgentSigningCertificate##];
GO
但要强调的是:
这里的
[saa]只是当前环境查出来的结果,别人环境里不一定是这个值。
所以文章中应该保留“查询 + 动态生成”的处理方式,而不是直接写死。
7. 删除异常主体
当以上两类权限都清理完成后,再次检查:
SELECT
perm.class_desc,
perm.major_id,
perm.permission_name,
perm.state_desc,
grantor.name AS grantor_name,
grantee.name AS grantee_name
FROM sys.server_permissions perm
LEFT JOIN sys.server_principals grantor
ON perm.grantor_principal_id = grantor.principal_id
LEFT JOIN sys.server_principals grantee
ON perm.grantee_principal_id = grantee.principal_id
WHERE grantor.name = '##MS_AgentSigningCertificate##'
OR grantee.name = '##MS_AgentSigningCertificate##';
GO
如果返回 0 行,说明权限依赖已经清理干净。
此时就可以删除该登录:
DROP LOGIN [##MS_AgentSigningCertificate##];
GO
8. 恢复正常启动
退出 sqlcmd,回到前台运行 sqlservr.exe -mSQLCMD -T902 的窗口,按:
Ctrl + C
然后正常启动 SQL Server 服务:
net start MSSQLSERVER
如果服务正常启动,说明问题已经修复。
9. 排查过程中遇到的一个坑:排序规则冲突
在动态拼接 SQL 时,可能遇到类似错误:
无法解决 add 运算符中“Chinese_PRC_CI_AS”和“Latin1_General_CI_AS_KS_WS”之间的排序规则冲突
这通常是因为系统表字段和字符串字面量使用了不同排序规则。
处理方式是在拼接 SQL 时增加:
COLLATE DATABASE_DEFAULT
上面的动态 SQL 模板中已经加入了这部分处理。
10. 问题总结
这次问题表面上看是应用连接数据库失败,实际上根因在 SQL Server 系统库升级失败。
整个链路如下:
-
SQL Server 启动时执行
msdb110_upgrade.sql -
发现
##MS_AgentSigningCertificate##存在未清理的权限依赖 -
升级脚本失败,报
15173 -
触发
912 -
master数据库无法恢复,报3417 -
实例无法启动,最终导致应用启动失败
这类问题的关键不在于“重启服务”,而在于:
-
用
-T902跳过升级脚本 -
找出残留权限
-
先查询、再动态生成
REVOKE -
不要把特定环境中的 login 名写死
11. 结论
遇到 3417 + 15173 + 912 组合报错时,可以优先怀疑:
-
SQL Server 系统升级脚本执行失败
-
系统主体存在权限残留
-
master/msdb相关系统对象状态不一致
更多推荐



所有评论(0)