PAYATHON 2026

EF Core 空闲后查询变慢,生产环境该如何排查?

数据小陈

结论

托管服务定期执行轻量查询,可以作为临时缓解措施,但需要明确监控和控制负载。在原因尚未查明前,不宜把它当作根治方案。每 10 秒查询一次是否适合生产环境,取决于实例数量、数据库容量和连接配置。定时查询会持续增加数据库请求,也不能保证每次都复用同一个物理连接。

如果空闲一段时间后所有查询都变慢,应先分别测量应用唤醒、数据库连接建立、SQL 执行和结果读取各阶段的耗时。AddDbContextPool 复用的是 DbContext 实例,本身无法消除数据库连接重建、网络或数据库端的延迟。

先定位耗时发生在哪一段

记录请求总耗时,并分别测量获取 DbContext、打开数据库连接、执行命令和读取结果所用的时间。可以使用 EF Core 日志或 DbCommandInterceptor 记录连接和命令耗时,同时查看 SQL Server 的等待类型、活动请求和 Query Store 数据。日志中应避免记录敏感参数。

可以根据现象初步排查:

  • 如果只有第一次请求的应用端耗时较高,检查应用是否被托管平台休眠、实例是否按需启动,以及启动时是否需要初始化依赖项。
  • 如果打开连接耗时较高,检查连接池、网络、DNS、TLS 和数据库可达性,并确认连接池是否频繁耗尽或重建。
  • 如果连接很快打开,但 SQL 执行较慢,查看 SQL Server 负载、等待、执行计划和统计信息。
  • 如果 SQL 执行很快,但请求仍然较慢,测量结果读取、对象物化和应用自身工作的耗时。

“空闲几分钟后变慢”这一现象本身无法确定具体原因。EF Core 查询编译缓存通常不会因为短暂空闲而自动清空,因此不应先假定所有查询都需要重新编译。

检查连接池和托管环境

确认连接字符串没有意外禁用连接池,并检查 Max Pool Size、连接超时和应用的并发连接需求。可以根据连接池指标和实际负载评估 Min Pool Size,但它不能保证连接始终健康,也不会让休眠的应用实例继续运行。不同版本的 Microsoft.Data.SqlClient 和不同托管环境可能有不同的连接行为,应结合实际日志确认。

同时检查应用服务是否配置了空闲回收或缩容策略。如果应用实例被休眠,数据库 keep-alive 查询未必能阻止应用休眠;如果 SQL Server 实例或网络设备会清理空闲连接,则应检查相应的平台配置和连接错误日志。仅凭这一现象,不能推断 SQL Server 2017 会自动暂停。

keep-alive 的使用边界

如果短期内需要保留定时探测,每轮任务都应使用独立的 DbContext,遵守取消令牌,并记录失败情况和耗时。避免共享一个长期持有的 DbContext。下面的示例展示了使用 IDbContextFactory 执行轻量查询的基本方式,具体查询 API 可根据项目使用的 EF Core provider 调整:

public sealed class DatabaseKeepAliveService(
    IDbContextFactory<AppDbContext> contextFactory,
    ILogger<DatabaseKeepAliveService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                await using var db = await contextFactory
                    .CreateDbContextAsync(stoppingToken);

                await db.Database.SqlQuery<int>($"SELECT 1")
                    .SingleAsync(stoppingToken);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception ex)
            {
                logger.LogWarning(ex, "Database keep-alive query failed");
            }
        }
    }
}

每轮迭代结束后,这段代码都会释放上下文。连接通常由连接池管理,但连接池无法保证下一轮使用同一条物理连接。上线前应确认 SELECT 1 在所用 provider 和配置下能够正常执行,再根据实例数量、数据库负载和观测到的空闲时间选择查询频率。不要通过捕获并忽略所有异常来掩盖持续故障。

生产环境常见缓解方式

先根据测量结果处理实际瓶颈:保持应用实例常驻或调整平台的空闲回收策略,修复连接池、网络或数据库端的问题,为确有需要的查询优化执行计划和索引,并为首次请求延迟设置监控和告警。如果问题只在实例启动后出现,可以评估启动预热;预热不能替代对连接和 SQL 耗时的诊断。

UseQuerySplittingBehavior、投影优化和编译查询主要影响查询形态或 EF Core 查询处理开销。如果耗时主要来自建立连接、数据库唤醒或网络等待,这些优化通常无法解决根因。建议先获取空闲前后的各阶段耗时和 SQL Server 侧的等待数据,再决定是否保留 keep-alive。

备注:内容仅供参考。