-rw-rw-r-- 1 admin admin 183570494 Jan 9 16:00 a...

创建于:2026年1月9日

使用 GPT-5.2 回答 Chat01

问题

-rw-rw-r-- 1 admin admin 183570494 Jan 9 16:00 access.log
-rw-r--r-- 1 admin admin 1073741901 Sep 15 03:26 application-1.log
-rw-r--r-- 1 admin admin 1073741971 Sep 15 09:26 application-2.log
-rw-r--r-- 1 admin admin 1073741927 Sep 17 17:21 application-3.log
-rw-r--r-- 1 admin admin 1073741916 Nov 27 09:41 application-4.log
-rw-r--r-- 1 admin admin 584889184 Jan 9 16:02 application.log
drwxr-xr-x 2 admin admin 4096 May 6 2024 deeplog-offllogmanager
-rw-r--r-- 1 admin admin 1073741918 Sep 15 03:25 error-1.log
-rw-r--r-- 1 admin admin 1073741904 Sep 15 10:25 error-2.log

springboot 里面,access.log 和 application.log error.log 有什么区别呢? 分别写入的是什么日志?

回答

Spring Boot(通常是内嵌 Tomcat / 使用 Logback) 里,这三类日志关注点不同,可以理解为 访问层 / 应用层 / 异常层。结合你给出的文件名和大小,我按最常见配置给你解释(你这个看起来是典型的生产环境滚动日志)。


一、access.log(访问日志 / Web 访问层)

是谁写的

  • Web 容器写的(Tomcat / Undertow / Nginx 等)
  • 不是 Spring Boot 的 Logger

记录什么

  • 每一次 HTTP 请求
  • 类似 Nginx 的访问日志

典型内容

text
127.0.0.1 - - [09/Jan/2026:16:00:01 +0800] "GET /api/user/1 HTTP/1.1" 200 123 "-" "Mozilla/5.0"

一般包含

  • 客户端 IP
  • 请求时间
  • HTTP 方法 + URL
  • 返回状态码(200 / 500 / 404)
  • 响应大小
  • 耗时(如果配置了)
  • User-Agent

用途

  • 排查接口是否被调用
  • 统计 QPS / 流量
  • 分析慢接口、异常接口
  • 审计、回溯请求

特点

  • 请求多 → 文件增长快
  • 即使业务代码没写日志,也会有

二、application.log(应用日志 / 业务日志)

是谁写的

  • Spring Boot 应用本身
  • 通过 Logger(Logback / Log4j2)
java
log.info("create order, userId={}", userId); log.debug("param={}", param);

记录什么

  • 你自己写的日志
  • Spring / MyBatis / Hibernate 等框架日志
  • 各种 INFO / DEBUG / WARN

典型内容

text
2026-01-09 16:02:11.345 INFO [http-nio-8080-exec-3] c.xxx.OrderService - create order success, orderId=123

一般包含

  • 时间
  • 日志级别(INFO / DEBUG / WARN)
  • 线程名
  • 类名
  • 业务信息

用途

  • 看业务流程
  • 调试问题
  • 分析代码逻辑是否走到某一步

特点

  • 最常用
  • 内容最多、最杂
  • 和业务强相关

三、error.log(错误日志 / 异常日志)

有些项目叫 error.log,有些是 application-error.log,本质一样

是谁写的

  • Spring Boot Logger
  • 一般通过 日志级别或单独 appender 分流

记录什么

  • ERROR 级别日志
  • 未捕获异常
  • 堆栈信息(StackTrace)

典型内容

text
2026-01-09 16:02:15.789 ERROR [http-nio-8080-exec-5] c.xxx.OrderController - create order failed java.lang.NullPointerException at com.xxx.OrderService.create(OrderService.java:45) ...

用途

  • 排查线上问题
  • 定位代码 bug
  • 告警系统监控(ELK / Prometheus)

特点

  • 数量相对少
  • 价值最高
  • 经常单独采集、报警

四、三者对比总结

日志文件关注点谁写的主要内容常见用途
access.log请求Web 容器HTTP 请求流量、接口、性能
application.log业务应用业务日志逻辑排查
error.log异常应用ERROR + 堆栈故障定位

五、结合你目录里的现象解释一下

你这里看到:

  • application-1.log ~ application-4.log
  • error-1.log ~ error-2.log
  • 每个差不多 1GB(1073741824 bytes)

👉 说明

  • 配置了 按大小滚动
  • 超过 1GB 就切文件
  • application 和 error 是 分开滚动的

六、一个常见误区提醒 ⚠️

❌ access.log 里 一般看不到 Java 异常堆栈
❌ error.log 里 一般看不到每一次请求

很多新人第一反应是:

“接口 500 了,我去 application.log 里搜 URL”

正确姿势

  1. 先在 access.log 找到 请求时间 & 请求路径
  2. 再按时间去 error.log 看异常
  3. 最后结合 application.log 看业务上下文

如果你愿意,我可以:

  • 帮你 画一张请求 → 日志流转图
  • 或直接帮你 写一份 logback-spring.xml 标准生产配置
  • 或告诉你 怎么快速用 grep / awk / less 定位问题

你可以直接说你现在是 运维 / 开发 / 排障中 的哪种场景。

问题

2025-12-30 10:41:19.927 [ERROR] [http-nio-0.0.0.0-8087-exec-6] [ExceptionHandler:resolveException(32)] - Request method 'GET' not supported
org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' not supported
at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.handleNoMatch(RequestMappingInfoHandlerMapping.java:200) ~[spring-webmvc-5.1.3.RELEASE.jar!/:5.1.3.RELEASE]
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod(AbstractHandlerMethodMapping.java:422) ~[spring-webmvc-5.1.3.RELEASE.jar!/:5.1.3.RELEASE]
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:368) ~[spring-webmvc-5.1.3.RELEASE.jar!/:5.1.3.RELEASE]
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:65) ~[spring-webmvc-5.1.3.RELEASE.jar!/:5.1.3.RELEASE]
at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:401) ~[spring-webmvc-5.1.3.RELEASE.jar!/:5.1.3.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1231) ~[spring-webmvc-5.1.3.RELEASE.jar!/:5.1.3.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1014) ~[spring-webmvc-5.1.3.RELEASE.jar!/:5.1.3.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) ~[spring-webmvc-5.1.3.RELEASE.jar!/:5.1.3.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005) ~[spring-webmvc-5.1.3.RELEASE.jar!/:5.1.3.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897) ~[spring-webmvc-5.1.3.RELEASE.jar!/:5.1.3.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:687) ~[hadoop-jdufs-cdn-1.0.5.jar:?]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882) ~[spring-webmvc-5.1.3.RELEASE.jar!/:5.1.3.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:790) ~[hadoop-jdufs-cdn-1.0.5.jar:?]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.13.jar!/:9.0.13]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.13.jar!/:9.0.13]
at org.springframework.web.filter.AbstractRequestLoggingFilter.doFilterInternal(AbstractRequestLoggingFilter.java:262) ~[spring-web-5.1.3.RELEASE.jar!/:5.1.3.RELEASE]

@Override
public BaseResponse setTaskFailed(String targetName) {
BaseResponse response = new BaseResponse();
String monitorKey = taskRedisProperties.getMonitorKey();

text
try { String taskJson = cluster.hGet(monitorKey, targetName); if (taskJson == null) { response.getStatus().setCode(StatusResponse.ResponseCode.ClientError); response.getStatus().setMsg("Task not found: " + targetName); @RequestMapping(value = "setTaskFailed", method = RequestMethod.POST) BaseResponse setTaskFailed(@RequestParam("targetName") String targetName) { return vipUploadService.setTaskFailed(targetName); } return response; } logger.info("The content of task before setting task as failed: {}", taskJson); Properties properties = JSON.parseObject(taskJson, Properties.class); // 添加当前时间戳(精确到分钟)到reason字段 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); String timestamp = sdf.format(new Date()); properties.put("status", "failed"); properties.put("reason", "Manually marked as failed due to worker failure at " + timestamp); // 使用DisableCircularReferenceDetect避免循环引用问题,同时保留类型信息 String value = JSON.toJSONString(properties, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteClassName); // 这个特性可以保留Long类型信息 logger.info("The content of task after setting task as failed: {}", value); cluster.hSet(monitorKey, targetName, value); logger.info("Task {} has been manually set to failed status", targetName); response.getStatus().setMsg("Task has been successfully marked as failed"); } catch (Exception e) { logger.error("Failed to set task {} as failed: {}", targetName, e.getMessage(), e); response.getStatus().setCode(StatusResponse.ResponseCode.ServerError); response.getStatus().setMsg("Error setting task as failed: " + e.getMessage()); } return response; }

日志:2025-12-30 10:41:19.766 [INFO] [http-nio-0.0.0.0-8087-exec-5] [RequestLoggingFilterConfig:afterRequest(28)] - Afteruri=/vipUpload/setTaskFailed?targetName=domainStorage_183_31_62/amNsb3VkX2F5Z2ZzZUU=/storage.jd.com/20251230/2025123007/2025123007_part23.gz]

似乎 我这个是post 的方法,但是在相同的时间戳里面,我却看到了 get 分析一下 这个报错信息

回答

从你贴的两段日志来看,这个报错不是“你明明 POST 了却被 Spring 误判成 GET”,而是:** 同一个 URL 在同一时间附近,被至少两个请求打到了,其中有一个是 GET**,而你的接口只允许 POST,所以 Spring 抛了 HttpRequestMethodNotSupportedException: Request method 'GET' not supported

下面把证据和最常见原因拆开讲清楚。


1) 错误堆栈到底在说什么

报错核心是这一句:

  • Request method 'GET' not supported
  • 异常类型:org.springframework.web.HttpRequestMethodNotSupportedException

这类异常是在 Spring MVC 找到了路径(或接近匹配的路径),但 HTTP Method 不匹配 时抛出的。

而你的 Controller 显式限制了:

java
@RequestMapping(value = "setTaskFailed", method = RequestMethod.POST) BaseResponse setTaskFailed(...)

所以只要有人用 GET /vipUpload/setTaskFailed?... 访问,就会抛这个异常。


2) 你那条 “Afteruri= …” 日志并不能证明它是 POST

你给的访问日志是:

Afteruri=/vipUpload/setTaskFailed?targetName=...

注意:它只记录了 URI,没有记录 HTTP method(GET/POST)。

所以这条日志完全可能来自 GET 请求,也可能来自 POST 请求 —— 单凭它不能断定“这是 post 的”。

另外也很关键:你两条日志的线程不一样:

  • ERROR 在 http-nio-...-exec-6
  • INFO 在 http-nio-...-exec-5

不同线程 = 不同请求(Tomcat 一个请求通常由一个 worker 线程处理)。
所以“同一时间戳附近看到 GET 和 Afteruri”非常符合:并发/紧邻的两个请求


3) 为什么会出现 “同一个接口既有 POST 又有 GET”

最常见的几种来源(按概率从高到低):

A. 人工/脚本用浏览器直接打开了这个 URL(浏览器地址栏永远是 GET)

很多人为了“触发一下接口”,把: /vipUpload/setTaskFailed?targetName=... 直接粘贴到浏览器地址栏回车——这必然是 GET,然后就报你这个错。

B. 监控/探活/网关健康检查误扫到了业务接口

比如 Prometheus/黑盒探测、ELB/SLB 健康检查、某个网关路由探测,会用 GET 去探测 URL 是否可达。

C. 前端/调用方写错了 method(该 POST 写成 GET)

例如 axios/fetch 配置错了 method,或把参数拼 query string 后误用了 GET

D. 重定向导致 method 变化(少见但要留意

如果中间有 301/302,某些客户端会把原请求方法改成 GET(尤其历史行为)。
但你这里异常是 Spring 直接抛的“GET 不支持”,更像请求一开始就是 GET

E. CSRF / CORS 预检(OPTIONS) —— 但你这不是 OPTIONS

你这里明确是 GET,所以不是预检。


4) 你现在这段日志缺了一个关键信息:到底是谁发的 GET?

要快速锁定来源,建议你把 access log 或 request logging 打到包含 method + remoteAddr + status

你已经有 RequestLoggingFilterConfig 了,但它现在只打印了 uri。把它改成至少包含:

  • method(GET/POST)
  • remoteAddr(来源 IP)
  • status(200/405/…)
  • headers(可选:User-Agent、X-Forwarded-For)

这样你立刻能确认:那条 Afteruri 其实是不是 GET、GET 来自哪里。

最直接的操作(不改代码也能查)

  1. access.log 里按时间 2025-12-30 10:41:19 附近 grep setTaskFailed
  2. 看同一秒内是否有两条:
    • "GET /vipUpload/setTaskFailed?..."
    • "POST /vipUpload/setTaskFailed?..."
  3. 重点看:
    • client ip
    • status code(GET 通常是 405)
    • user-agent(浏览器 / curl / 探测器)

如果你能贴一行 access.log(脱敏 IP 也行),我可以帮你判断几乎 100% 是哪一类来源。


5) 这个异常对业务有没有“真实影响”?

  • 如果你的真实调用方是 POST,并且 POST 成功执行了,那这个 GET 报错只是噪音(有人扫了/误点了)。
  • 如果你的调用方本来应该是 POST,但实际发成 GET,那么业务就会失败(得到 405),需要改调用方。

6) 你可以怎么处理(按你的目标选)

如果你只想“别报 ERROR 这么吓人”

HttpRequestMethodNotSupportedException 这种客户端误用,很多团队会:

  • 记录成 WARN(甚至 INFO)
  • 返回更友好的 405 响应

例如全局异常里把它单独处理(不打 ERROR 堆栈),避免污染 error.log。

如果你想“立刻定位是谁发的 GET”

优先做两件事:

  1. 在 access.log 找到对应行(method、IP、UA)
  2. 在应用 request log 打印 method + remoteAddr + XFF

一句话结论

你看到的“POST 接口却报 GET not supported”,本质是:同一个 URL 有请求以 GET 方式打进来了;你那条 Afteruri=... 日志没有 method,而且线程不同,说明不是同一个请求

你把 access.log 里那一行 GET/POST 的原始记录贴出来(1~2 行就够,IP 可打码),我就能帮你判断是浏览器误触发、探活、还是调用方写错。

问题

2025-12-29 11:49:48.002 [INFO] [scheduling3] [LeaderWrapper:tryTakeOverLeader(94)] - tryTakeOverLeader key offline-upload-master-locker,redis msg I'm leader,and expire time update success!
2025-12-29 11:49:50.328 [ERROR] [http-nio-0.0.0.0-8087-exec-9] [ExceptionHandler:resolveException(32)] - domainStorage_183_31_62/c2hlbnpoZW5neWlkb25nMDM=/v96-sz-daily-b.douyinvod.com/20251228/2025122805/202512280520_part2.gz 任务不处于失败状态,当前状态为running
com.jdcloud.pid.taskmanager.exception.BizException: domainStorage_183_31_62/c2hlbnpoZW5neWlkb25nMDM=/v96-sz-daily-b.douyinvod.com/20251228/2025122805/202512280520_part2.gz任务不处于失败状态,当前状态为running
at

这个情况是,脚本会对 failed 状态的任务进行 调整,把他们设为running状态;然后在设置前的过程中,会判断 任务是否在失败。结果发现还在运行,似乎就把 这个redis 任务信息 直接给搞没了。分析一下

思考

Analyzing task status issue with Redis

It looks like the script is encountering an issue where it’s not properly handling a task's status. Possibly, the task is being marked as running even when it should be set to failed. There might be a race condition due to concurrent worker actions. Mitigation could include atomic operations or adding version control to prevent this kind of overlap in status handling.

Thought for 8s

回答

从你这条日志本身来看,Spring 报错的根因不是“HTTP 方法不对”,而是你们业务自己抛了 BizException

任务不处于失败状态,当前状态为running
com.jdcloud.pid.taskmanager.exception.BizException ...

也就是说:脚本(或接口)想“只对 failed 的任务做调整”,但读到 Redis 里该任务状态是 running,于是按你们的业务规则直接拒绝了这次操作。

你担心的点是:“拒绝了以后,Redis 里的任务信息却没了(被搞没了)”。这通常不是 BizException 本身造成的,而是典型的两类问题:** 并发竞态 + 非原子写法/清理逻辑**。


1) 这两行时间非常关键:Leader 切换 + 立刻有人来改任务

text
11:49:48 tryTakeOverLeader ... I'm leader ... 11:49:50 BizException ... 状态为 running

这意味着在 Leader 刚切换成功后 2 秒,就有请求/脚本来做“失败任务调整”。

在这种架构里,经常存在:

  • 老 Leader / worker 仍在跑(没完全停干净)
  • 新 Leader 开始扫描/接管/清理
  • 调整脚本同时在改同一批任务

同一个 task 的状态可能在 very short window 内被不同线程/不同实例反复改。

所以你看到“我以为它 failed,结果读到 running”很符合竞态。


2) “任务信息直接没了”的最常见原因(按概率从高到低)

A. 你们的调整脚本/接口有“无论成功失败都删除”的清理逻辑(最常见)

常见伪代码长这样:

java
try { task = hget(...) check failed // do update } catch (Exception e) { // 记录日志 } finally { hdel(monitorKey, targetName); // ❌ 为了“处理完就清掉”,但异常也会走到这里 }

或者:

java
if (!failed) { throw new BizException(...) } hdel(...) // ❌ 放在后面,但上层 catch 了 BizException 后仍会继续执行清理流程

结果:一旦发现不是 failed(running),抛异常,但后续“清理”仍执行 → 任务被删

你现在看到的现象“发现还在 running,就把任务信息搞没了”——这个模式非常吻合。

✅ 怎么验证
搜代码里对该 task 的 Redis 操作,重点找:

  • hDel(monitorKey, targetName)
  • del(monitorKey)(更危险,会删整张 hash)
  • expire(monitorKey, ...)(把整张 hash 设置了短 TTL,导致“像被删了一样”)

B. 非原子 read-modify-write,导致“最后写回的人把内容覆盖成空/旧值”

如果你们逻辑是:

  1. hGet 读出 JSON
  2. parse -> 修改
  3. hSet 写回

在并发下可能发生:

  • 线程/实例 A 读到 taskJson(旧)
  • 同时线程/实例 B 把该任务删了或更新成别的状态
  • A 继续写回,可能写回了缺字段的对象、甚至写回 "null" / ""(取决于代码),最终表现为“任务信息没了/不可解析”。

✅ 怎么验证
看日志里是否有类似:

  • “before setting …” / “after setting …” 对应的 JSON
  • 是否出现 taskJson == null 分支
  • 是否出现写入空字符串、null{}

C. 设置了 key 级别 TTL,导致整张 hash 被过期(看起来像“任务被清空”)

你们的任务通常是 monitorKey 下面的 hash field(targetName)。如果某段逻辑对 monitorKey 这个 key 做了 expire,并且 TTL 较短,那会导致:

  • 某个任务调整时顺手 expire(monitorKey, 60)
  • 60 秒后整张任务表都没了
  • 你回头看就像“脚本把任务信息删了”

3) 你这条 BizException 本身透露了一个更深的业务问题:你们对“状态期望”过于乐观

你说“脚本会对 failed 状态任务进行调整,把他们设为 running”,但实际上读到的是 running,说明至少存在一种情况:

  • 任务已经被 worker 自动重试/拉起,状态从 failed → running(你脚本慢了一拍)
  • 或者你脚本的“筛选 failed 列表”本身不是实时从 Redis 拿,而是缓存/文件/上一次扫描结果(过期了)

这就会导致:脚本带着“应该是 failed”的假设来改,结果现场已经变了。


4) 建议你立刻做的修复(不需要大改架构)

① 把“只允许从 failed → running”做成 Redis 原子条件更新(强烈建议)

用 Redis Lua / CAS(WATCH-MULTI-EXEC)实现:

  • 只有当当前 status == failed 时才更新
  • 否则不动 Redis(尤其不要删)

这能一把解决“并发下误删/误覆盖”。

② 把 BizException 这种“状态不匹配”降级处理,绝对不要触发清理

状态不匹配本质是正常并发现象(或调用方选错任务),建议:

  • 只返回 “当前状态=running,跳过”
  • 日志记 WARN/INFO
  • 不要进入任何 hDel / expire / 清理分支

③ 增加关键日志:每次对这个 targetName 做 Redis 写/删时都打出来

至少包含:

  • monitorKey
  • targetName
  • 操作类型:HSET / HDEL / DEL / EXPIRE
  • 写入前后的 status
  • 执行节点/实例标识(哪个机器/哪个 leader)

这样你能在日志里直接抓到“是谁删的”。


5) 你现在就能快速定位“是谁把它删了”的排查路径

  1. 在同一时间窗口(11:49:48 ~ 11:50:30)grep:

    • hdel
    • del
    • expire
    • clean
    • remove
    • monitorKey
    • 这个 targetName(完整那串域名路径)
  2. 看是否存在两类线程/实例:

    • scheduling*(定时扫描/leader 任务)
    • http-nio-*(接口触发)
  3. 如果你们有 access.log:查这秒有没有人调用“调整接口”(看来源 IP / UA)——能区分是脚本还是人工。


总结一下你这条日志能得出的最靠谱结论

  • 这次请求/脚本尝试调整任务时,Redis 中该任务不是 failed,而是 running,所以业务按规则抛了 BizException。
  • “任务信息没了”极大概率是你们后续的清理逻辑(finally/hdel/del/expire)在异常路径也执行了,或并发覆盖/TTL 造成的“消失”。

如果你把调整脚本/接口里涉及 Redis 的删除/过期相关代码段(尤其是 catch/finally 部分)再贴一小段出来,我可以直接帮你指出哪一行会导致“running 时被清掉”,以及怎么改成原子安全的写法。

问题

这个补传脚本是,似乎不涉及把redis 直接扔掉的逻辑

coding=UTF-8

import time

import requests
from apscheduler.schedulers.blocking import BlockingScheduler

def autoMakeUp():
url = "http://offline-logup-deeplog.jdcloud.com/vipUpload/getRunningTasks"
resp = requests.get(url)
jsonData = resp.json()
data = jsonData['data']
for item in data:

text
if item['status'] == 'failed': targetName = item['targetName'] reRun_url = "http://offline-logup-deeplog.jdcloud.com/vipUpload/reRunJob?targetName=" requests.get(reRun_url + targetName) print "reRunJob:" + targetName time.sleep(1) else: print "skip normal"

if name == 'main':

text
scheduler = BlockingScheduler() scheduler.add_job(autoMakeUp, 'cron', id='auto_makeup', hour='*') try: scheduler.start() except (KeyboardInterrupt, SystemExit): scheduler.shutdown() print ("Exit....")

回答

你这段补传脚本本身确实没有任何“删 Redis”的动作,它只做了两件事:

  1. GET /vipUpload/getRunningTasks 拉任务列表
  2. 对其中 status == failed 的任务,再 GET /vipUpload/reRunJob?targetName=... 触发重跑

所以如果你观察到“Redis 里的任务信息没了”,更大的概率是:

  • 后端接口在处理 reRunJob / 或者某个定时/worker 做了删除/迁移/过期(TTL)
  • 或者 getRunningTasks 返回的数据并不是“Redis 当前实时状态”(可能是缓存/聚合后的视图)
  • 再加上并发竞态:你看到它 failed,但你真正去改时它已经 running(于是抛了 BizException)

下面我把你现在现象里最关键的点拆出来讲。


1) 你脚本最大的“异常点”:用 GET 去做“有副作用”的重跑

你之前贴过一个接口是 setTaskFailed 只允许 POST。而你这个脚本对重跑是:

python
requests.get(reRun_url + targetName)

也就是说 reRunJob 这个接口很可能也是一个“应该用 POST”的接口,但你用 GET 调了。如果后端严格限制 method,就会出现你之前那种:

  • Request method 'GET' not supported(405)
  • 或者后端为了兼容 GET,但内部逻辑走了某个“清理/迁移”的分支(更危险)

✅ 你现在贴的 BizException(“不处于失败,当前 running”)非常像:
reRunJob(或它内部调用的服务)在执行前做了状态校验:只允许 failed → running,结果读到 running,就抛异常。


2) 为什么会“你以为它 failed,但后端读到 running”

这个在你当前脚本逻辑里很容易发生:你先拉一次列表,然后逐个处理,中间还有 sleep(1)

典型竞态场景:

  • 11:49:48 你拉到列表:任务 A 状态 failed
  • 11:49:49 worker/leader 切换、自动重试、或其他人手动操作:把任务 A 改成 running
  • 11:49:50 你脚本去调用 reRunJob:后端读 Redis,发现已经 running → 抛 BizException

所以“列表里 failed,但执行时 running”并不矛盾,反而很常见。


3) “任务信息没了”最可能发生在后端,不在脚本

你脚本不删 Redis,但后端可能在这些地方让你感觉“没了”

A. reRunJob 内部“领取任务/迁移队列”时,把 field 从一个 hash 移到另一个 hash

比如:

  • failedTasks(或 monitorKey)挪到 runningTasks
  • 你去原来的 key 看,当然“没了”
  • 但实际上在另一个 key 里

B. reRunJob 或 worker 在异常路径里做了 HDEL / DEL / 设置短 TTL

你之前的担忧“发现 running 就给搞没了”,最符合的是这种代码味道:

  • 校验失败抛异常,但 finally/catch 仍然清理
  • 或者统一“处理过就删除”,没区分成功/失败

脚本不会触发删除,但脚本触发了接口,接口里可能删。

C. monitorKey 级别设置了 EXPIRE,导致整个任务表过期

这会表现为:某个时间点之后你发现“全没了”,不是单个没。


4) 你这脚本还有一个明显 bug:targetName 没做 URL 编码

你 targetName 看起来包含很多特殊字符:/= 等(你之前日志里就是这种)。

你现在直接拼接:

python
requests.get(reRun_url + targetName)

如果 targetName 里有 &?% 之类,或者某些网关对 /= 处理不一致,会导致:

  • 参数被截断
  • 参数被错误解析
  • 后端拿到的 targetName 不是你想要的那个
  • 从而出现“找不到任务 / 误操作另一个任务 / 状态对不上”

✅ 正确做法是用 params= 让 requests 帮你编码:

python
requests.get(reRun_url, params={"targetName": targetName})

这条非常重要:很多“玄学丢任务/查不到/错任务”最后都是参数没编码。


5) 我建议你立刻做的三步定位(不用改后端也能做)

第一步:确认 reRunJob 到底是 GET 还是 POST

看后端 Controller 映射,或直接看 access.log 是否出现:

  • "GET /vipUpload/reRunJob?..." 以及返回码(200/405/500)。

如果 reRunJob 实际只支持 POST,你现在用 GET 调,后端要么 405,要么走了异常处理器。

第二步:在 access.log 里找“谁删/迁移”

把同一秒内的请求串起来看:

  • getRunningTasks(脚本拉列表)
  • reRunJob(脚本触发)
  • 以及是否还有别的接口/定时触发(leader/worker)

重点看来源 IP / UA:一眼就能分辨是脚本机器还是别的系统。

第三步:在后端日志里 grep 你怀疑“丢失”的 targetName

在它“消失”的时间窗,搜这几类关键词(随便一个命中就有方向):

  • hdel / del / expire
  • remove task / clean / delete
  • move / transfer / take(迁移/领取)
  • 你们 monitorKey 名字(如果日志里打过)

6) 直接给你一个更安全的脚本改法(最少改动)

  • 对 reRunJob 改用 params 编码
  • 加超时、打印返回码和响应内容(否则你根本不知道后端有没有成功/有没有 405)
  • (如果后端支持)优先用 POST
python
# coding=UTF-8 import time import requests from apscheduler.schedulers.blocking import BlockingScheduler BASE = "http://offline-logup-deeplog.jdcloud.com" def autoMakeUp(): url = BASE + "/vipUpload/getRunningTasks" resp = requests.get(url, timeout=10) resp.raise_for_status() data = resp.json().get("data", []) for item in data: if item.get("status") == "failed": targetName = item.get("targetName") rerun_url = BASE + "/vipUpload/reRunJob" r = requests.get(rerun_url, params={"targetName": targetName}, timeout=10) print("reRunJob status=%s target=%s resp=%s" % (r.status_code, targetName, r.text[:200])) time.sleep(1) else: print("skip normal") if __name__ == '__main__': scheduler = BlockingScheduler() scheduler.add_job(autoMakeUp, 'cron', id='auto_makeup', hour='*') try: scheduler.start() except (KeyboardInterrupt, SystemExit): scheduler.shutdown() print("Exit....")

结论

  • 你脚本不删 Redis,但它触发的后端接口很可能会做“迁移/清理/过期”,或者在异常路径里误清理。
  • “列表里 failed,执行时 running”是典型竞态,尤其你日志里还出现 leader 切换。
  • 你脚本现在 GET + 未编码参数,非常容易引入“状态对不上/找不到任务/误命中”的问题。

如果你能再贴两样东西(任意一个都行),我就能把“Redis 被清掉/被迁移”的责任点定位到具体模块:

  1. reRunJob 的 Controller 映射(method、内部调用哪个 service)
  2. access.log 中同一秒 reRunJob 那条记录(含 method/status/来源 IP)

分享这个问答