V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
frank1256
V2EX  ›  程序员

springboot 的 @Scheduled cron 延迟

  •  
  •   frank1256 · 1 天前 · 216 次点击

    生产出现奇怪的事情

    代码里配置 @Scheduled(cron="0 0 2 * * ?") 生产 k8s 部署了 4 副本,但是 apm 发现有一个副本是 4 点钟执行任务。并且任务执行时会有数据库更新,库里都更新成了 4 点。

    进入容器内部,检查了 date 时区 ,代码版本,请求了接口的配置。4 个副本都一致。

    没啥头绪,有大佬指点下吗

    第 1 条附言  ·  1 天前
    而且,更新数据库时间的值,是 LocalDateTime.now()也不是数据库时间,说明当时 jvm ,时间就是 4 点,但 2 点的 cron 怎么才触发。。懵了
    NelsonZhao
        1
    NelsonZhao  
       1 天前
    Gemini 的回答
    这是一个非常经典且具有迷惑性的问题!你排查的方向(时区、代码、配置)是正确的,但既然它们都一致,那么问题很可能出在你看不到的地方:Spring 的任务调度线程池。

    你遇到的“诡异现象”最可能的原因是:Spring TaskScheduler 线程池饥饿( Thread Pool Starvation )。

    问题分析:为什么 2 点的 Cron 会在 4 点执行?
    你对 LocalDateTime.now() 的分析非常关键,它证明了“任务执行时,JVM 时间确实是 4 点”。

    这并不是 2 点的 Cron 延迟触发了,而是 2 点的 Cron 准时触发了,但任务被“排队”了,直到 4 点钟才“轮到”它执行。

    详细的执行链条推测如下:

    Spring @Scheduled 的默认配置: 默认情况下,Spring Boot 用于 @Scheduled 的任务调度线程池( TaskScheduler )大小为 1 。这意味着,在同一时间,一个应用实例中只能有一个 @Scheduled 任务在运行。

    2:00 AM 准时触发: 在 2:00:00 ,4 个副本的调度器都准时醒来,说:“好了,该执行 cron="0 0 2 * * ?" 这个任务了。”

    提交任务到线程池:

    副本 1, 2, 3: 它们的调度线程池是空闲的。任务被提交,立刻被线程池中的那 1 个线程取出并立即开始执行。此时 LocalDateTime.now() 是 2:00 AM ,数据库更新为 2 点。

    副本 4 (问题副本): 它的调度器也在 2:00:00 提交了任务。但是,它的那 1 个调度线程正在忙!

    副本 4 为何忙碌?

    最可能的原因是:你代码中还有另一个 @Scheduled 任务(比如一个 cron="0 0 1 * * ?" 在 1 点钟触发的任务,或者一个 @Scheduled(fixedDelay=...) 的任务)。

    在副本 4 上,这个“更早的任务”由于某种原因(如 I/O 阻塞、数据库死锁、调用第三方接口超时、或者进入了 bug 循环)执行了非常长的时间。

    假设这个“长任务”从 1:00 AM 开始执行,一直执行到了 4:00 AM 才结束。

    任务排队与执行:

    在 2:00 AM ,你这个 2 点的任务被提交到副本 4 的队列中,它在排队。

    在 4:00 AM ,“长任务”终于执行完毕,释放了线程。

    线程池立刻从队列中取出下一个任务——也就是你这个“本应 2 点执行”的任务,并开始执行它。

    结果:

    任务在 4:00 AM 开始执行。

    APM 监控到任务在 4:00 AM 开始。

    代码中的 LocalDateTime.now() 获取到的当前 JVM 时间就是 4:00 AM 。

    数据库被更新为 4:00 AM 。

    这个理论完美地符合了你的所有观察:时区没错,代码没错,配置也没错,错的是运行时的线程状态。

    如何验证和解决?
    如何验证
    检查 APM: 这是最快的方法。去 APM 查看那个“问题副本 4”,在 2:00 AM 到 4:00 AM 之间,是不是有另一个 @Scheduled 任务正在运行?你会在 APM 上看到一个从 1 点(或更早)开始,到 4 点才结束的超长执行链路。

    检查日志: 仔细翻看问题副本在 2:00 AM 之前的日志。看看有没有其他定时任务的“开始”日志,但迟迟没有打印“结束”日志。

    检查代码: 审查你项目中所有其他的 @Scheduled 方法。评估一下:

    它们有没有可能执行时间超过 1-2 个小时?

    它们有没有可能因为网络或数据库问题而卡住?

    jstack (如果能复现): 如果问题能复现,在 2:00 AM 刚过的时候,立即 ssh/exec 到问题容器,对 Java 进程执行 jstack 。你很可能会在线程 dump 中看到一个名为 scheduling-1 (或类似的) 线程,它正 RUNNABLE 或 BLOCKED 在某个长时间运行的任务代码上。

    如何解决
    你有两个层面的问题需要解决:

    问题一:调度线程池饥饿(你当前遇到的问题)

    方案 A (简单修复): 增加调度线程池的大小。这样即使一个任务卡住了,也不会阻塞其他任务。 在 application.yml (或 properties) 中配置:

    YAML

    spring:
    task:
    scheduling:
    pool:
    size: 10 # 默认是 1 ,改成 10 或更高
    方案 B (最佳实践): 如果你确认某个任务就是会执行很久,应该将它与普通的短任务隔离。

    让这个“长任务”的 @Scheduled 方法本身只做一件事:调用一个 @Async 异步方法。

    为这个 @Async 方法配置一个单独的、专用的线程池。

    这样,@Scheduled 线程(来自 scheduling-pool )会“秒级”执行完(仅仅是提交了一个异步任务),不会阻塞其他定时任务。真正的耗时工作会在另一个线程池( async-pool )中进行。

    问题二:4 个副本重复执行(你潜在的更大问题)

    你提到 K8s 部署了 4 个副本。这意味着,这个 2:00 AM 的任务,每天都会被执行 4 次(副本 1 、2 、3 在 2 点执行,副本 4 在 4 点执行)。

    对于“更新数据库”这样的操作,这通常是一个严重的问题,会导致数据不一致或重复处理。

    解决方案 (强烈推荐): 引入分布式调度锁,例如 ShedLock 。

    ShedLock 是一个轻量级的库,它利用数据库(或 Redis/ZooKeeper 等)来确保一个带注解的 @Scheduled 任务在同一时间只会被一个集群实例执行。

    使用方法很简单:

    引入 ShedLock 依赖。

    配置一个 LockProvider (比如指向你的数据库)。

    在你的定时任务上加注解:

    Java

    import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;

    @Scheduled(cron="0 0 2 * * ?")
    @SchedulerLock(name = "myDailyTask", lockAtMostFor = "1h", lockAtLeastFor = "5m")
    public void yourTask() {
    // ... 数据库更新 ...
    }
    这样配置后,在 2:00 AM ,4 个副本都会尝试获取 myDailyTask 这个锁。只有一个副本能成功,然后执行任务。其他 3 个副本获取锁失败,会直接跳过当次的执行。

    总结
    你的时区/时钟 99% 没问题。

    问题出在 Pod 4 的 Spring 调度线程池(默认大小为 1 )被另一个长任务占满了。

    导致 2:00 AM 的任务在队列中排队,直到 4:00 AM 才轮到它执行。

    请立刻检查 APM 和日志,找到那个“超长任务”并优化它。

    请立刻配置 spring.task.scheduling.pool.size > 1 来避免阻塞。

    请立刻使用 ShedLock 来防止 4 个副本重复执行任务。
    shiny
        2
    shiny  
       1 天前 via iPhone   ❤️ 1
    @NelsonZhao 不要在这里直接贴 AI 的答案
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   2445 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 04:37 · PVG 12:37 · LAX 21:37 · JFK 00:37
    ♥ Do have faith in what you're doing.