SQL Server 重启后程序连不上,回收 IIS 应用池才恢复

AI 生成声明:本文内容由 AI 生成或经 AI 辅助完成,请注意甄别。

ASP.NET Core 部署在 IIS 上,数据库用的是 Microsoft.Data.SqlClient 7.0.1 加 Dapper。SQL Server 重启完成之后,接口一直报连不上,回收应用程序池才立刻正常。

第一反应容易落到客户端版本上。这次不是。业务代码每次 new SqlConnection,用完就关,写法本身没有握着一条长连接。卡住的是进程里的连接池。

池在进程里,不在某次请求里

ADO.NET 的连接池按进程存在。IIS 里就是那个 w3wp。默认最多大约 100 条物理连接,寿命不限。逻辑上每次请求都是新的 SqlConnection,物理 TCP 却被放回池里下次复用。

数据库进程或整机重启后:

  • 池里的旧 TCP 不会被主动作废。Windows 默认 TCP KeepAlive 大约 2 小时,半开连接看起来仍可用。
  • 下一次 Open 才会踩到这条死连接:被对端拒绝,或者一直重传到超时。池只丢掉被踩中的这一条,其余死连接继续留给后面的请求。
  • 流量不高时,要失败很多次池才会排空。表现就是库已经起来了,接口还是连不上。
  • 回收应用程序池会结束 w3wp,池随之销毁。新进程对已经起来的 SQL Server 建立新 TCP,所以马上正常。

/health 里执行 SELECT 1 也解决不了这件事,除非有人来打这个地址,并且失败时真的去清池。IIS 不会因为健康检查失败就自己回收。

自带重连只处理正在取出的那一条

Microsoft.Data.SqlClient 有空闲连接重连。缺省大约是重试 1 次、间隔 10 秒。它只在从池里取出某一条已经断开的连接时尝试重连,然后才把错误抛给这次请求。

它不会巡检整个池,也不会在服务器重新起来之后主动把剩下的死连接丢掉。半开连接若还没被判定为断开,这次重连甚至不会发生,请求会耗在 TCP 重传上。

所以升级客户端解决不了「必须回收应用池」这现象。7.0.1 已经是新包,缺的是池的寿命和失败后的清空。

连接串只补缺省,不覆盖已有值

在创建连接前规范化连接串。某一项如果运维已经写进连接串,就保持原值。没写才补上:

项 补上的值 作用
ConnectRetryCount 3 取出的空闲连接已断开时,先重连
ConnectRetryInterval 2 秒 缩短缺省的 10 秒间隔
Load Balance Timeout 60 秒 连接归还池时,超过 60 秒就销毁
Connect Timeout 15 秒 建连等待
Min Pool Size 0 不预留永远占着的物理连接
Pooling true 继续使用连接池

Load Balance Timeout 就是连接寿命(Connection Lifetime)。设成 0 时,一条物理连接可以活过整个应用池寿命。60 秒只是让归还到池里的旧连接尽快淘汰,不是关掉池。

不要把 Pooling 设成 false。那样每次请求都新建 TCP,数据库重启后确实不会留死连接,但建连压力会全部打到 SQL Server 上。

探活失败时清空整个池

重试和寿命仍然是「用到某条连接时才处理它」。数据库已经恢复、池里还堆着死连接时,低流量接口会一条一条失败。需要有人把池整个丢掉。

加一个后台任务,大约每 20 秒用同一套连接工厂执行 SELECT 1,命令超时 3 秒:

  • 失败:若距上次清空已超过约 10 秒,调用 SqlConnection.ClearAllPools(),记一条「数据库不可达,已清空连接池」。
  • 由失败变为成功:记一条「数据库已恢复」。成功本身不再清池,因为不可达期间已经清过。
  • 只处理这次探活的失败。业务 SQL 的死锁、唯一键冲突、慢查询超时不去清池。

/health 失败时走同一个清空方法。外面如果有监控来打健康检查,也能顺便把池清掉。10 秒防抖是为了探活和健康检查同时失败时不要连着清。

库还没起来时,清空是空操作。库起来之后,池里已经没有旧 TCP,下一个业务请求会新建连接。最坏大约一个探活周期,20 秒内恢复,不用再等人工回收应用池。

示意如下:

public static bool TryClearPools()
{
    var now = Environment.TickCount64;
    var last = Interlocked.Read(ref _lastClearTicks);
    if (last != long.MinValue && unchecked(now - last) < 10_000)
        return false;
    if (Interlocked.CompareExchange(ref _lastClearTicks, now, last) != last)
        return false;

    SqlConnection.ClearAllPools();
    return true;
}

ClearAllPools() 清的是当前进程里全部 SQL Server 连接池。如果同一个进程还连着别的 SQL Server,探活失败时会一起清掉。这次只有一个业务库,可以接受。MySQL 用的是另一套客户端,不受这次清空影响。

发布之后才对正在跑的进程生效

这些逻辑在新进程里才会执行。已经在 IIS 里跑着的 w3wp 仍是旧程序,连接池也还是旧的。要把新的 API 发布出去,让应用池加载新程序,之后的数据库重启才不必再手工回收。

posted @ 2026-11-13 14:19  catcherx  阅读(11)  评论(0)    收藏  举报