
生产出现奇怪的事情
代码里配置 @Scheduled(cron="0 0 2 * * ?") 生产 k8s 部署了 4 副本,但是 apm 发现有一个副本是 4 点钟执行任务。并且任务执行时会有数据库更新,库里都更新成了 4 点。
进入容器内部,检查了 date 时区 ,代码版本,请求了接口的配置。4 个副本都一致。
没啥头绪,有大佬指点下吗
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 个副本重复执行任务。 |
2 shiny 1 天前 via iPhone @NelsonZhao 不要在这里直接贴 AI 的答案 |