每次发版滚动更新都会冒出一波请求失败,我以为做了优雅停机,排查才发现应用根本没收到关闭信号,我对着 Dockerfile 用 shell 形式启动让 PID 1 吞掉 SIGTERM 这个坑排查大半天的复盘

一个让我对容器里进程怎么活怎么死彻底搞明白的 DevOps 坑,隐蔽在我明明以为做了优雅停机(应用里写了关闭钩子),发版时却总有一小波请求失败,而那个优雅停机逻辑像从没被触发过——收到的关闭信号在到达它之前就被吞掉了。每次滚动更新监控就冒出一小波 5xx,用户偶尔反馈操作失败。Dockerfile 用了 CMD java -jar app.jar(shell form)。深究才明白:容器里 PID 1 进程负责接收外界发来的 SIGTERM;shell form 会被实际执行成 /bin/sh -c java...,PID 1 是 /bin/sh、java 是它的子进程;而 sh 收到 SIGTERM 既不处理也不转发给子进程,信号被吞掉,java 根本收不到 SIGTERM、优雅停机钩子从未触发,K8s 等 30 秒宽限期后发 SIGKILL 强杀,正在处理的请求被粗暴中断。根因两层:信号没传到应用(shell form 让 sh 当 PID 1 不转发),应用本身也要真正实现优雅停机。这篇从故障现场、PID 1 与信号传递真相、正解(exec form 数组写法让 java 当 PID 1、tini 转发信号并回收僵尸、脚本里 exec 启动、应用实现优雅停机钩子停新请求+等存量+释放资源、K8s 配 preStop 摘流量 readiness grace period)、容器化其他坑(JVM 不感知 cgroup 被 OOMKilled、健康检查反复重启、时区、日志、配置硬编码、跑 root)、shell form vs exec form 对照表、优雅停机完整链条、写启动命令决策图与铁律,到补充怎么验证优雅停机真生效(看 PID 1、手动发 SIGTERM 看日志、压测中滚动更新)。核心领悟:工具的两种等价写法背后可能截然不同要搞清区别和适用场景;优雅停机是环环相扣的链条任何一环断了就失效要端到端看整条链路排查;容器不只是打包它重新定义了程序运行的环境契约(PID 1 身份、cgroup 资源、信号驱动的生命周期),拥抱任何新运行环境的前提是理解并遵循它的规则别用旧环境直觉想当然;对只在特定时刻才生效的保障机制不能配了就以为有效要主动模拟那个时刻去实证验证。

每次发版滚动更新都会冒出一波请求失败,我以为做了优雅停机,排查才发现应用根本没收到关闭信号,我对着 Dockerfile 用 shell 形式启动让 PID 1 吞掉 SIGTERM 这个坑排查了大半天的复盘

这是一个让我对"容器里进程是怎么活、怎么死的"彻底搞明白的 DevOps 坑。它的隐蔽之处在于:我明明以为自己做了"优雅停机"(应用里写了关闭钩子),发版时却总有一小波请求失败;而我写的那个优雅停机逻辑,像从来没被触发过一样——因为它收到的关闭信号,在到达它之前,就被"吞掉"了。

事情起于一个发版现象。每次我的服务做滚动更新(K8s 逐个替换旧 Pod),监控上就会冒出一小波 5xx 错误,用户偶尔反馈"操作失败"。我以为优雅停机能解决——我在应用里注册了关闭钩子,收到 SIGTERM 就先停止接新请求、处理完手头的再退出。可现象依旧。我看了看 Dockerfile 的启动方式:

# 有问题的 Dockerfile 启动方式
FROM eclipse-temurin:17
WORKDIR /app
COPY app.jar .

# ★★★ 问题: 用 shell 形式(shell form)启动 ★★★
CMD java -jar app.jar
# 或等价的: CMD ["sh", "-c", "java -jar app.jar"]
# 看起来没问题, 但这会让 /bin/sh 成为容器的 PID 1, 而不是 java!

这个 CMD java -jar app.jar 看起来再正常不过,服务也能跑起来。可正是这种"shell 形式"的写法,让我那个精心写的优雅停机逻辑形同虚设:K8s 要关闭旧 Pod 时,会给容器的 1 号进程(PID 1)发送 SIGTERM 信号;可因为我用了 shell 形式启动,PID 1 是 /bin/sh,真正的 java 进程是 sh 的子进程;而 sh 不会把 SIGTERM 转发给子进程——于是 java 根本收不到 SIGTERM,优雅停机逻辑从未被触发;K8s 等了 30 秒(默认宽限期)见进程还不退,就发 SIGKILL 强制杀死,正在处理的请求被粗暴中断我盯着这个链条,意识到问题的本质:我的关闭信号,在到达应用之前,就被 PID 1 的那个 shell 给"截胡"并"无视"了。

第一件事:看清真相——容器 PID 1 负责接信号,shell form 让 shell 当 PID 1 吞掉了信号

我去深入理解了容器里的进程模型、PID 1 的特殊职责,以及 Docker 的 shell form 和 exec form 的区别,才终于解开这个"信号神秘消失"之谜——容器里,PID 1 进程负责接收外界发来的信号(如 SIGTERM);而我用 shell form 启动,让 /bin/sh 成了 PID 1,真正的应用是它的子进程;sh 收到 SIGTERM 既不处理、也不转发给子进程,信号就这样被"吞掉"了

容器 PID 1 与信号传递的真相

# 1. 容器里 PID 1 的特殊职责:
#    - 容器启动的那个进程, 是容器里的 1 号进程(PID 1)。
#    - 当外界(Docker/K8s)要停止容器时, 会向【PID 1】发送 SIGTERM 信号,
#      意思是"请优雅地关闭"; 等一段时间(宽限期)还不退, 再发 SIGKILL 强杀。
#    - 所以: 谁是 PID 1, 谁就负责接收和处理这个 SIGTERM!

# 2. Docker 的两种 CMD/ENTRYPOINT 写法:
#    a) shell form(我用的):  CMD java -jar app.jar
#       → Docker 实际执行: /bin/sh -c "java -jar app.jar"
#       → 此时 PID 1 是 【/bin/sh】, java 是 sh 的【子进程】(不是PID 1)!
#    b) exec form(数组写法): CMD ["java", "-jar", "app.jar"]
#       → Docker 直接执行 java, 不经过 shell
#       → 此时 PID 1 就是 【java】本身!

# 3. 为什么 shell form 会吞掉信号:
#    - PID 1 是 sh, K8s 把 SIGTERM 发给 sh
#    - 而 /bin/sh 默认【不会把它收到的信号转发给子进程】!(它就那么干等着)
#    - → 真正的 java(子进程)根本收不到 SIGTERM → 优雅停机钩子不触发
#    - → 30秒宽限期到, K8s 发 SIGKILL → 整个进程组被强杀 → 请求被粗暴中断

# 4. 另外: PID 1 还有"回收僵尸子进程"等特殊职责,
#    普通程序当 PID 1 可能处理不好这些, 这也是要用 tini 等 init 的原因之一。

# 5. 所以根因有两层:
#    - 信号没传到应用: 因为 shell form 让 sh 当了 PID 1 且不转发信号
#    - (即使传到了)应用要真正实现优雅停机逻辑: 收到SIGTERM后停接新请求、处理完再退

# 核心: 容器PID 1负责接收SIGTERM; shell form(CMD java...)让/bin/sh当PID 1, 而sh不转发信号给
#   子进程, 导致应用收不到SIGTERM、优雅停机失效、最终被SIGKILL强杀中断请求。

真相大白,我恍然大悟。原来容器里,PID 1 进程负责接收外界(Docker/K8s)发来的停止信号(SIGTERM);谁是 PID 1,谁就负责处理这个信号。而 Docker 的 CMD 有两种写法:shell form(我用的 CMD java -jar app.jar)会被 Docker 实际执行成 /bin/sh -c "java -jar app.jar",此时 PID 1 是 /bin/sh、java 是它的子进程;而 exec form(数组写法 CMD ["java","-jar","app.jar"])则直接执行 java、java 本身就是 PID 1关键在于:/bin/sh 默认不会把它收到的信号转发给子进程!所以 K8s 把 SIGTERM 发给 sh,sh 干等着、java 根本收不到,优雅停机钩子从未触发;30 秒宽限期到,K8s 发 SIGKILL 强杀整个进程组,请求被粗暴中断。(此外 PID 1 还有"回收僵尸子进程"等特殊职责,这也是要用 tini 等 init 的原因。)所以根因有两层:信号没传到应用(shell form 让 sh 当 PID 1 且不转发),以及应用本身要真正实现优雅停机逻辑(收到 SIGTERM 后停接新请求、处理完再退)。

第二件事:正解——用 exec form 让应用当 PID 1,或用 init,并配合优雅停机

搞懂了原理,正解就清晰了:用 exec form 让应用直接成为 PID 1 收到信号(或用 tini 这类 init 转发信号),应用里实现优雅停机逻辑,再配合 K8s 的 preStop 和就绪探针

# ====== 正解一(基础): 用 exec form, 让 java 直接当 PID 1 ======
FROM eclipse-temurin:17
WORKDIR /app
COPY app.jar .
# ★ exec form(数组写法): java 直接成为 PID 1, 能收到 SIGTERM
CMD ["java", "-jar", "app.jar"]
# 不要写成 CMD java -jar app.jar(shell form, sh会当PID 1吞信号)

# ====== 正解二: 用 tini 等 init 进程(更稳妥, 还能回收僵尸进程) ======
# Docker run 加 --init, 或 Dockerfile 里:
# ENTRYPOINT ["/tini", "--"]
# CMD ["java", "-jar", "app.jar"]
# → tini 当 PID 1, 它会正确地把信号转发给子进程、并回收僵尸进程

# ====== 正解三: 如果必须用脚本启动, 用 exec 替换进程 ======
# start.sh 里:
#   #!/bin/sh
#   exec java -jar app.jar    # ★ exec: 用java【替换】掉shell进程, java就成了PID 1
# → exec 让 java 顶替 shell 的 PID 1 位置, 直接收信号(没有exec的话sh还是PID 1)
// ====== 正解四: 应用里真正实现优雅停机(收到SIGTERM要做事) ======
public class App {
    public static void main(String[] args) {
        Server server = startServer();
        // 注册关闭钩子: 收到 SIGTERM 时执行
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("收到SIGTERM, 开始优雅停机...");
            server.stopAcceptingNewRequests();   // 1. 先停止接收新请求
            server.awaitInflightRequests(25);    // 2. 等待正在处理的请求完成(限时)
            server.close();                      // 3. 释放资源后退出
            System.out.println("优雅停机完成");
        }));
        server.serve();
    }
}
// Spring Boot: server.shutdown=graceful + spring.lifecycle.timeout-per-shutdown-phase
// → 框架已内置优雅停机, 配置开启即可

// ====== 正解五: K8s 配合 preStop + readiness, 让流量先摘除 ======
// preStop: sleep几秒, 让Pod先从Service端点摘除(不再有新流量进来), 再开始关闭
// readinessProbe: 关闭中标记为not ready, K8s不再往它转发流量
// terminationGracePeriodSeconds: 设得比优雅停机耗时长一点(如 grace 30s > 停机 25s)

// 核心: exec form/tini/exec脚本 让应用直接当PID 1收到SIGTERM; 应用实现优雅停机(停新请求+
//   等存量完成+释放资源); K8s配preStop摘流量、readiness、合理grace period。

修复的核心,是"让应用收到信号 + 应用真正优雅停机 + K8s 先摘流量"正解一(基础):用 exec form——CMD ["java","-jar","app.jar"] 数组写法,java 直接当 PID 1 能收到 SIGTERM,别写 shell form正解二:用 tini 等 init——ENTRYPOINT ["/tini","--"] 或 docker run --init,tini 当 PID 1 正确转发信号、还回收僵尸进程正解三:必须用脚本就 exec java ...——exec 用 java 替换掉 shell 进程、java 成为 PID 1正解四:应用真正实现优雅停机——注册关闭钩子,收到 SIGTERM 后停止接新请求 → 等正在处理的请求完成(限时)→ 释放资源退出;Spring Boot 配 server.shutdown=graceful 即可正解五:K8s 配 preStop + readiness——preStop sleep 让 Pod 先从 Service 摘除流量、readiness 标记 not ready、grace period 设得比停机耗时长归根结底:exec form/tini/exec 脚本让应用直接当 PID 1 收到 SIGTERM;应用实现优雅停机;K8s 配 preStop 摘流量、readiness、合理 grace period。

第三件事:容器化部署的其他常见坑

排查后我把容器化部署相关的其他常见坑也系统梳理了一遍。

容器化部署的其他常见坑

# 1. shell form 吞信号(本文): 优雅停机失效。→ exec form / tini / exec。

# 2. JVM不感知cgroup内存限制: 老JVM按宿主机内存算堆, 超容器limit被OOMKilled
#    → 新JVM默认 -XX:+UseContainerSupport; 或显式 -Xmx / -XX:MaxRAMPercentage。

# 3. 健康检查配置不当: liveness探针太敏感, 启动慢的应用被反复重启
#    → 用 startupProbe 给启动留时间; liveness/readiness 分清职责。

# 4. 容器时区: 默认UTC, 日志时间和本地差8小时
#    → 挂载 /etc/localtime 或设 TZ 环境变量。

# 5. 日志写到容器内文件: 容器没了日志就没了, 还可能写满磁盘
#    → 日志输出到stdout/stderr, 由日志系统收集。

# 6. 没设资源 requests/limits: 容器抢占资源或被驱逐。→ 合理设置。

# 7. 配置硬编码进镜像: 改配置要重新构建。→ 配置用环境变量/ConfigMap注入。

# 8. 镜像跑root: 安全风险。→ 非root用户运行(见镜像构建那篇)。

# 共同根源: 容器不只是"把程序打个包", 它改变了程序运行的【环境和生命周期】
#   (PID 1、cgroup资源限制、短暂易销毁、声明式编排); 用"传统部署"的直觉容易踩坑。

# 核心: 容器化要理解它带来的新环境(PID 1信号、cgroup限制、无状态短暂、探针生命周期);
#   信号/资源/健康检查/时区/日志/配置都要按容器的方式来; 别用裸机部署的老习惯套容器。

排查让我把容器化的其他坑也梳理清了。一、shell form 吞信号(本文)。二、JVM 不感知 cgroup 内存限制(按宿主机算堆超 limit 被 OOMKilled,新 JVM 用 UseContainerSupport)。三、健康检查配置不当(liveness 太敏感反复重启,用 startupProbe)。四、容器时区(默认 UTC,设 TZ)。五、日志写容器内文件(应输出 stdout 由日志系统收集)。六、没设 requests/limits七、配置硬编码进镜像(用环境变量/ConfigMap)。八、镜像跑 root它们的共同根源是:容器不只是"把程序打个包",它改变了程序运行的环境和生命周期(PID 1、cgroup 资源限制、短暂易销毁、声明式编排);用"传统部署"的直觉容易踩坑核心是:容器化要理解它带来的新环境;信号/资源/健康检查/时区/日志/配置都要按容器的方式来,别用裸机部署的老习惯套容器下面这张图,是这次优雅停机失效的成因与解法:

第四件事:shell form vs exec form 对照表

这次踩坑后,我把 Docker 的 shell form 和 exec form 的区别整理成一张表,写 Dockerfile 时对照。

维度 shell form (CMD java ...) exec form (CMD ["java",...])
实际执行 /bin/sh -c "..." 直接执行程序
PID 1 是谁 /bin/sh(应用是子进程) 应用本身
能否收到 SIGTERM ✗ sh收到但不转发 ✓ 应用直接收到
优雅停机 ✗ 失效(信号没到应用) ✓ 可生效
支持shell特性(变量/管道) ✓ 支持 $VAR、&& 等 ✗ 不解析(需显式sh -c)
推荐度 仅在需shell特性时 ✓ 默认推荐(尤其长驻服务)

这张表把两种写法的区别钉死了。核心是:exec form 让应用直接成为 PID 1、能正确收到信号(优雅停机生效),是长驻服务的默认推荐;shell form 会插一个 /bin/sh 当 PID 1(吞信号),只在确实需要 shell 特性(环境变量展开、管道、&&)时才用,且要注意信号问题它给我的最大启发是:一个工具提供的"两种看起来差不多的写法",背后可能有着截然不同的底层行为和后果;CMD java ...CMD ["java",...] 看起来只是"写法风格的差异",实则一个让 shell 当 PID 1、一个让应用当 PID 1,导致了"优雅停机能不能生效"这种天壤之别这其实是一个普遍的教训:对工具/框架提供的"多种等价写法",别想当然地以为它们"只是风格不同、效果一样";要去搞清楚它们底层到底有什么区别、各自适用什么场景、有什么不同的后果;很多坑,就藏在我们"以为只是写法不同、随便选一种"的那些地方这让我养成一个习惯:当文档提到某个东西有"两种形式/两种模式/两种写法"时,我会特意去了解"它们的区别和各自的适用场景到底是什么",而不是随手挑一个看着顺眼的认真对待"等价写法"背后的真实差异——是避开这类"选错形式"的坑的关键。

第五件事:优雅停机背后的完整链条

这次也让我把"一次优雅的滚动更新"背后的完整链条理清了。

阶段 发生什么 要做对什么
1. K8s 决定关旧Pod 开始终止流程
2. 摘除流量 从Service端点移除 readiness标记notready, preStop sleep
3. 发SIGTERM 给PID 1发信号 exec form/tini让应用收到
4. 应用优雅关闭 停新请求+处理存量 应用实现关闭钩子
5. 等宽限期 等应用退出 grace period > 停机耗时
6. 超时则SIGKILL 强杀 正常不该走到这步

这张表把"优雅停机"这件事的完整链条铺开了。核心是:"优雅停机"不是某一个孤立的配置,而是一条环环相扣的链条——从"先摘流量(readiness/preStop)"、到"信号要能传到应用(exec form)"、到"应用要真正实现优雅关闭逻辑"、到"宽限期要给够时间";这条链上任何一环没做对,整个优雅停机就会失效(我就是栽在了"信号传不到应用"这一环)它给我的深刻启发是:很多看似简单的"功能/特性"(优雅停机、优雅启动、健康检查),实际是多个组件、多个环节协同配合才能实现的;它的正确工作,依赖于链条上每一环都正确,而不是某一处配置对了就行;排查这类问题时,要有"端到端、看整条链路"的视角,逐环检查"到底是哪一环断了",而不是只盯着自己最熟悉的那一环(比如我一开始只查应用代码,没想到是 Dockerfile 的启动方式断了信号链)用"端到端看整条链路"的视角去理解和排查这类"多环节协同"的功能——是这个优雅停机坑教给我的、关于"系统性排查"的重要方法。

第六件事:写 Dockerfile 启动命令时,我现在的判断习惯

现在每当我写容器的启动命令、或处理容器生命周期,我都会按这张图先想清楚:

这张图的精髓,是"长驻服务必须用 exec form 让应用收到 SIGTERM,并实现优雅停机、配 K8s 摘流量"长驻服务必须让应用能收到 SIGTERM:用 exec form 数组写法、需要 shell 特性就用脚本里 exec 启动或 tini;应用里实现优雅停机钩子,K8s 里配 readiness+preStop 摘流量、grace period 设够长这套习惯,让我写容器启动命令时,从"随手 CMD java -jar"变成了"先想这服务关闭时信号能不能传到、能不能优雅停"——核心始终是:容器 PID 1 接信号,exec form 让应用当 PID 1 收到 SIGTERM,配优雅停机和摘流量实现平滑滚动更新。

我立下的几条规矩

这场"优雅停机失效"的事故,换来了我做容器化部署时,刻进骨子里的几条铁律:

  1. 长驻服务的 CMD 用 exec form。数组写法,让应用直接当 PID 1。
  2. shell form 让 sh 当 PID 1 吞信号。CMD java... 是优雅停机的隐形杀手。
  3. 容器 PID 1 负责接收 SIGTERM。谁是 PID 1 谁收信号。
  4. 必须用脚本就 exec 启动。exec java... 让 java 替换 shell 成 PID 1。
  5. 应用要真正实现优雅停机。停新请求+处理存量+释放资源。
  6. K8s 配 preStop + readiness 先摘流量。grace period 比停机耗时长。
  7. 理解容器改变了进程的环境和生命周期。别用裸机习惯套容器。

补充:怎么验证你的容器优雅停机真的生效了

这个坑最隐蔽的一点,是它"看起来好像没问题"——服务能起、能跑、滚动更新也"完成"了,只是悄悄丢了一小撮请求。所以踩坑后,我特意总结了几招"主动验证优雅停机是否真生效"的方法,免得它再悄悄潜伏。

第一招,看进程是不是 PID 1。进到运行中的容器里执行 ps -efps aux,确认你的应用进程的 PID 是不是 1。如果 PID 1 是 /bin/sh 而你的应用是个更大的 PID,那信号链就断了,优雅停机一定有问题。

第二招,手动发信号测试。对运行中的容器执行 docker stop(它会先发 SIGTERM、再等、再 SIGKILL),或直接 docker kill -s TERM <容器>,然后观察:应用日志里有没有打印出你的"开始优雅停机..."那行?如果没有,说明信号根本没传到应用。这是最直接的验证——让信号真的发一次,看应用有没有反应

第三招,压测中滚动更新。用压测工具持续打流量,同时触发一次滚动更新/重启,观察这期间有没有请求失败(5xx、连接重置)。真正生效的优雅停机,应该能做到滚动更新期间零失败(或极少失败);如果有一波明显的失败,就说明优雅停机或流量摘除有问题。

这几招验证方法,给我的启发超越了优雅停机本身:对于那些"只在特定时刻(如关闭、故障、高峰)才起作用、平时看不出来"的机制,绝不能"配了就以为它生效了",而必须主动地、用模拟那个特定时刻的方式去验证它;优雅停机要靠"真的发个 SIGTERM、真的滚动更新一次"来验证,就像备份要靠"真的恢复一次"来验证、容灾要靠"真的切一次"来验证、限流要靠"真的打超量流量"来验证一样这本质上是一种"不轻信、要实证"的工程严谨:"我配置了它"和"它真的有效"之间,隔着一道必须用实际验证去跨越的鸿沟;尤其是那些"关键时刻才生效"的保障机制,你不能等到那个关键时刻(真出事、真发版)才发现它其实没用——而要在平时,就主动制造那个时刻去检验它主动模拟"关键时刻"去验证那些"平时看不出来的保障机制"——这是这个优雅停机坑,教给我的、关于"如何确保保障真的有保障"的务实方法。

写在最后

回头看,这场由"Dockerfile 一种启动写法"引发的、优雅停机失效的事故,真正教给我的,远不止"用 exec form"这一个技巧。它让我对"容器到底改变了什么",以及"抽象之下的运行环境",有了一次深刻的认识。我栽跟头,根源是我对容器的理解,停留在"容器就是把程序和它的依赖打个包,让它能到处一致地运行"这个表面层次。这个理解没错,但太肤浅了——它让我以为"容器里运行的程序,和我直接在机器上运行的程序,没什么本质区别"。于是我把"裸机上运行程序"的全部直觉,原封不动地搬进了容器,完全没有意识到:容器其实为程序创造了一个不同的运行环境——在这里,我的程序可能是"PID 1"(承担着接收信号、回收子进程的特殊职责),它的资源被 cgroup 限制着,它的生命被编排系统以"发信号、给宽限期、再强杀"的特定方式管理着。我用裸机的直觉写启动命令,自然就忽略了"谁是 PID 1、信号怎么传"这个在容器环境里至关重要、在裸机上却几乎不用操心的问题。这让我领悟到一个深刻的认知:"容器化"不只是一种"打包和分发"的技术,它更重新定义了程序运行的'环境契约'——程序在容器里的身份(PID 1)、资源边界(cgroup)、生命周期(信号驱动的启停)、与外界的交互方式,都和裸机不同;要在容器里把程序跑好、跑对,就必须理解并适应这套新的'环境契约',而不能想当然地沿用旧环境的假设这其实是拥抱任何新的"运行时/平台"(容器、Serverless、各种云托管环境)的共同前提:每一种新的运行环境,都有它自己的"规则和契约"(程序如何被启动、被调度、被限制、被终止、如何与平台交互);享受新平台带来的便利(一致性、弹性、自动化)的前提,是理解并遵循它的这套规则;否则,你的程序虽然"能在上面跑起来",却会在那些"新环境与旧直觉冲突"的地方(比如这次的信号处理),以意想不到的方式出问题理解并适应每一种新运行环境独特的"环境契约"、而非用旧环境的直觉想当然——这,是我用一次优雅停机失效的事故,换来的、关于 DevOps、关于容器、也关于如何拥抱任何新平台的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次写 Dockerfile 的 CMD 时,自然地用上数组形式、并想一下"这服务关的时候,信号传得到它吗?",那我对着那一波波发版失败排查的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

下游一个接口只是变慢了一点,我的整个服务却跟着全部瘫痪、所有请求都卡死,我对着调用下游时没设超时导致请求堆积线程耗尽的级联雪崩这个坑排查大半天的复盘

2026-6-2 11:20:15

技术教程

我训练的欺诈检测模型准确率高达 99%,我正得意,一看才发现它把所有交易都判成了正常、一笔欺诈都没抓到,我对着类别极度不平衡时 accuracy 完全失真这个坑排查了大半天的复盘

2026-6-2 11:32:16

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索