You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用SQL Server TestContainers集成测试时的间歇性登录失败问题

间歇性SQL Server登录失败(错误18456)排查与解决思路

问题背景

基于主API的Program.cs通过WebApplicationFactory启动做集成测试,用Docker TestContainers运行SQL Server,在GitLab/Github Actions流水线中执行。原本测试运行正常,新增测试后开始间歇性出现Microsoft.Data.SqlClient.SqlException: Login failed for user 'sa' (错误号18456)。不同环境失败率差异明显:GitLab流水线约2/9,本地笔记本约1/30,桌面环境更低。

已完成的排查动作

  • 模拟移除测试初始化时的定时任务,报错次数有所减少
  • 发现报错集中在触发多事务的端点,将该测试的并发调用改为串行后,失败频率降低,但仍偶发
  • 最近一次失败出现在测试代码通过DbContext读取数据库验证结果的环节
  • 禁用快照代码后测试完全恢复正常;但保留快照时会出现延迟失败:调用端点成功响应后,测试框架读取数据库失败,且失败发生在快照操作完成后的成功事务之后

核心疑问

  1. 快照恢复代码是否存在问题?
  2. 还有哪些可行的排查方向和解决方案?

可能原因分析

1. 快照操作的连接/事务泄漏

快照恢复过程中如果没有正确释放数据库连接,或者存在未完成的事务残留,会导致SQL Server连接池耗尽,进而触发登录失败——连接池无可用连接时,新的登录请求可能被拒绝。尤其是快照恢复时的后端请求报错,很可能导致连接未被正确回收。

2. SQL Server登录阈值限制

SQL Server默认有登录尝试频率限制,短时间内大量并发登录/连接请求会触发18456错误。这种情况在资源受限的CI容器环境中更容易出现,因为容器CPU、内存不足,SQL Server处理登录请求的能力会下降。

3. 连接池配置不合理

测试代码中如果未正确配置DbContext的连接池参数(比如Max Pool Size、Connection Lifetime),大量测试并发执行时连接池会被占满,新的连接请求无法获取连接,最终触发登录失败。

排查与解决步骤

针对快照代码的排查

  • 检查快照恢复代码中是否存在未释放的SqlConnection或DbContext实例:确保所有数据库操作都用using语句包裹,彻底避免连接泄漏。
  • 验证快照恢复的事务处理:如果快照恢复用到了事务,必须确保事务最终被提交或回滚,不能处于悬挂状态。
  • 调整快照恢复的超时设置:如果快照恢复操作耗时过长,会导致连接长时间被占用,影响后续测试的连接获取,可适当缩短超时时间或优化快照恢复逻辑。

连接池与SQL Server配置优化

  • 调整连接池参数:在测试的数据库连接字符串中添加Max Pool Size=100(根据测试并发数调整)、Connection Lifetime=30,避免连接长时间占用。
  • 提升SQL Server的连接与登录限制:启动TestContainers时,添加SQL Server启动参数-c login_mode=mixed -c max_connections=1000,提高最大连接数并放宽登录限制(仅用于测试环境)。
  • 给CI环境增加资源配额:为GitLab/Github Actions的运行容器分配更多CPU和内存,避免SQL Server因资源不足导致登录处理延迟。

测试代码优化

  • 强化测试隔离性:每个测试用例执行后,在TearDown阶段调用DbContext.Dispose(),并执行SqlConnection.ClearAllPools()清空连接池,确保测试间无连接残留。
  • 在测试层添加重试逻辑:针对登录失败的情况,在测试代码的DbContext操作中添加重试(不修改生产代码),比如用Polly库实现仅捕获18456错误的重试策略。
  • 进一步控制测试并发:如果CI环境资源有限,可将所有测试改为串行执行,或按模块分组串行,避免短时间内产生大量连接请求。

日志排查

  • 开启SQL Server登录失败日志:启动TestContainers时配置SQL Server启用登录失败日志,查看具体失败原因(比如密码错误、连接池满、登录频率限制等),日志可通过TestContainers映射到宿主机器方便查看。
  • 跟踪连接池状态:在测试代码中添加连接池状态日志,比如调用SqlConnection.GetPooledConnectionCount(),查看连接池是否被占满。

附相关代码与日志

数据库初始化代码

public async Task InitializeDatabaseAsync()
{
    var sqlContainer = new SqlServerBuilder()
        .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
        .WithPassword("YourStrong!Passw0rd")
        .Build();
    await sqlContainer.StartAsync();
    // 执行数据库迁移等初始化操作
}

快照恢复代码

public async Task RestoreSnapshotAsync()
{
    using var connection = new SqlConnection(_testConnectionString);
    await connection.OpenAsync();
    
    var restoreCmd = connection.CreateCommand();
    restoreCmd.CommandText = @"RESTORE DATABASE [TestDB] FROM DISK = '/var/opt/mssql/backup/test_snapshot.bak' WITH REPLACE";
    await restoreCmd.ExecuteNonQueryAsync();
}

报错日志

Microsoft.Data.SqlClient.SqlException (0x80131904): Login failed for user 'sa'.
Error Number:18456,State:1,Class:14
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at Microsoft.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.CompleteLogin(Boolean enlistOK)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.AttemptOneLogin(ServerInfo serverInfo, String newPassword, SecureString newSecurePassword, Boolean ignoreSniOpenTimeout, TimeoutTimer timeout, Boolean withFailover)

内容的提问来源于stack exchange,提问作者The Lemon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 19:16:04