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. 处理思路

由于实例正常启动时会立刻执行升级脚本,而升级脚本又会失败,所以常规方式无法登录数据库处理。

正确思路是:

  1. 使用 -T902 跳过升级脚本启动 SQL Server

  2. 用单用户模式连进去

  3. 查询 ##MS_AgentSigningCertificate## 相关的所有残留授权

  4. 动态生成并执行 REVOKE

  5. 删除该登录

  6. 正常重启 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 系统库升级失败。

整个链路如下:

  1. SQL Server 启动时执行 msdb110_upgrade.sql

  2. 发现 ##MS_AgentSigningCertificate## 存在未清理的权限依赖

  3. 升级脚本失败,报 15173

  4. 触发 912

  5. master 数据库无法恢复,报 3417

  6. 实例无法启动,最终导致应用启动失败

这类问题的关键不在于“重启服务”,而在于:

  • -T902 跳过升级脚本

  • 找出残留权限

  • 先查询、再动态生成 REVOKE

  • 不要把特定环境中的 login 名写死


11. 结论

遇到 3417 + 15173 + 912 组合报错时,可以优先怀疑:

  • SQL Server 系统升级脚本执行失败

  • 系统主体存在权限残留

  • master/msdb 相关系统对象状态不一致

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐