使用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. 快照操作的连接/事务泄漏
快照恢复过程中如果没有正确释放数据库连接,或者存在未完成的事务残留,会导致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
相关产品推荐
相关产品推荐

