etcd 退出码完全参考:0/1/2 与信号退出 143/130 的源码级解析
etcd server 的退出码(exit code)是容器编排、systemd 与自动化脚本判断其运行状态的核心依据。本文基于仓库中的退出码参考文档 exit_codes.md,逐场景核对当前仓库源码中的实际 os.Exit 调用点,讲清 etcd 显式使用的 0(成功)、1(一般错误)、2(参数错误)三类退出码,以及 Linux/Unix 下由信号终止产生的 128+信号号(143/130)行为的完整来源,帮助你在运维排障与监控接入时准确解读每一次进程退出。
退出码总览
etcd server 显式使用三个退出码,信号终止时则遵循 Unix 通用惯例:
| 退出码 | 含义 | 典型触发场景 |
|---|---|---|
| 0 | 成功 | --help、--version、正常停止、PID 1 进程收到 SIGTERM/SIGINT |
| 1 | 一般错误 | 启动阶段任何致命失败:logger 创建失败、URL 配置非法、datadir 校验失败、监听失败等 |
| 2 | 参数错误 | 命令行 flag 解析失败(未知 flag、参数格式错误) |
| 143 | 非 PID 1 进程收到 SIGTERM(128+15) | Linux 下 kill 默认信号 |
| 130 | 非 PID 1 进程收到 SIGINT(128+2) | 终端 Ctrl-C |
需要特别注意的是:信号终止行为是 Linux 平台特有的。按参考文档说明,在其他平台上,etcd 若无错误退出通常返回 0,否则返回 1。这一差异的根源在 pkg/osutil/signal_linux.go 与 pkg/osutil/signal.go 的构建标签划分(//go:build linux 与 //go:build !linux)。
退出码 0:成功退出的四个场景
所有“成功”路径在源码中都对应一次显式的 os.Exit(0),共四类:
1. 帮助标志 --help
在 server/etcdmain/config.go 的 parse() 中,flag 解析器返回 flag.ErrHelp 时直接打印帮助并退出:
perr := cfg.cf.flagSet.Parse(arguments)
switch {
case perr == nil:
case errors.Is(perr, flag.ErrHelp):
fmt.Println(flagsline)
os.Exit(0)
default:
os.Exit(2)
}
注意这个 switch 同时定义了退出码 2 的路径:任何不是“成功”或“帮助”的解析错误都落入 default 分支,以 os.Exit(2) 结束(详见后文)。
2. 版本标志 --version
同样位于 server/etcdmain/config.go,--version 打印版本信息后退出 0:
if cfg.printVersion {
fmt.Printf("etcd Version: %s\n", version.Version)
fmt.Printf("Git SHA: %s\n", version.GitSHA)
fmt.Printf("Go Version: %s\n", runtime.Version())
fmt.Printf("Go OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
版本字符串来自 api/version/version.go 的 version.Version / version.GitSHA 变量。
3. 正常停止(graceful shutdown)
服务运行完毕后,server/etcdmain/etcd.go 的主循环退出时调用 osutil.Exit(0):
select {
case lerr := <-errc:
// fatal out on listener errors
lg.Fatal("listener failed", zap.Error(lerr))
case <-stopped:
}
osutil.Exit(0)
osutil.Exit 并非无条件退出——见 pkg/osutil/interrupt_unix.go:
// Exit relays to os.Exit if no interrupt handlers are running, blocks otherwise.
func Exit(code int) {
interruptExitMu.Lock()
os.Exit(code)
}
它先获取 interruptExitMu。如果此时一个信号处理器正在执行(该互斥量由信号处理 goroutine 持有),Exit(0) 会阻塞等待,直到优雅关闭序列完成,从而避免“退出码 0”与信号处理并发竞争。stopped 通道来自 startEtcd,它等待 e.Server.ReadyNotify()(成员加入集群)之后返回 e.Server.StopNotify(),即 etcd server 自身完成停止后才触发退出。
4. PID 1 进程收到信号的优雅退出
容器内以 PID 1 运行的 etcd 收到 SIGTERM/SIGINT 时直接以 0 退出,代码在 pkg/osutil/interrupt_unix.go:
pid := syscall.Getpid()
// exit directly if it is the "init" process, since the kernel will not help to kill pid 1.
if pid == 1 {
os.Exit(0)
}
源码注释解释了原因:PID 1 的默认信号处置与子进程不同,内核不会按常规替它终止,因此必须在完成 handler 后显式 os.Exit(0)。这就是容器化部署中 docker stop(发 SIGTERM)后 etcd 容器以 0 退出的原因,也是 systemd 单元或 Kubernetes 认为进程“正常停止”的依据。
信号终止:非 PID 1 进程为什么退出 143 或 130
对非 PID 1 进程,信号处理流程在 pkg/osutil/interrupt_unix.go 的 HandleInterrupts 中实现:
- 注册监听:
signal.Notify(notifier, syscall.SIGINT, syscall.SIGTERM)(interrupt_unix.go#L52-L53),捕获 Go 运行时层面的 SIGINT/SIGTERM。 - 执行注册的 handler:
startEtcd通过osutil.RegisterInterruptHandler(e.Close)注册了 server 的Close作为优雅关闭钩子(server/etcdmain/etcd.go#L185)。handler 按注册顺序执行,期间interruptExitMu被持有,阻塞一切并发的osutil.Exit。 - 复位信号并自我重发:
setDflSignal(sig.(syscall.Signal))
syscall.Kill(pid, sig.(syscall.Signal))
Linux 上的 dflSignal(pkg/osutil/signal_linux.go)通过 rt_sigaction 把该信号处置方式清零,恢复为 SIG_DFL:
func dflSignal(sig syscall.Signal) {
// clearing out the sigact sets the signal to SIG_DFL
var sigactBuf [32]uint64
ptr := unsafe.Pointer(&sigactBuf)
syscall.Syscall6(uintptr(syscall.SYS_RT_SIGACTION), uintptr(sig), uintptr(ptr), 0, 8, 0, 0)
}
随后 syscall.Kill(pid, sig) 向自身重发同一信号,此时进程按内核默认处置被终止,最终退出码由内核按 128 + 信号号 约定呈现给父进程:
| 信号 | 退出码 | 说明 |
|---|---|---|
| SIGINT(Ctrl-C) | 130(128 + 2) | 终端交互式中断 |
| SIGTERM | 143(128 + 15) | kill <pid> / 编排系统默认停止信号 |
这个设计的意义在于:优雅关闭(handler 执行 Close、刷盘)先行完成,退出码再由内核按信号惯例生成,父进程(shell、supervisord 等)既能看到“被信号终止”的语义,又不丢失 etcd 的干净落盘。而非 Linux 平台上,pkg/osutil/signal.go 中 dflSignal 是空实现(nop),无法复位信号处置方式,因此参考文档明确标注:其他平台 etcd 无错误退出返回 0,否则返回 1。
值得对照的是仓库自身的测试框架:tests/framework/e2e/etcd_process.go 的 Stop() 会调用进程封装的 Stop()(发 SIGTERM)再 Close(),并把 unexpected exit code 视为可容忍结果之一——因为信号终止产生的非 0 退出码属于预期行为,而非故障。E2E 用例 tests/e2e/graceful_shutdown_test.go 则验证了 SIGTERM 触发的 leader 停止在 1.5 秒内完成且 leadership 正常转移。
退出码 1:启动阶段的一般错误
etcd 约定“一切 server 错误以 1 退出”。参考文档列出的场景在当前源码中的对应位置如下(行号以当前仓库为准;参考文档中锚定旧 commit 的行号可能有漂移):
| 场景 | 源码位置 |
|---|---|
| 创建 logger 失败 | server/etcdmain/etcd.go#L60 |
| 校验 flags 失败 | server/etcdmain/etcd.go#L69 |
| Discovery token 已被使用 | server/etcdmain/etcd.go#L141 |
| Initial cluster 配置错误 | server/etcdmain/etcd.go#L155 |
| Discovery 失败(Fatal) | server/etcdmain/etcd.go#L157 |
| 监听失败(Fatal) | server/etcdmain/etcd.go#L172 |
| 无法列出数据目录 | server/etcdmain/etcd.go#L201 |
| datadir 非法(member 与 proxy 并存) | server/etcdmain/etcd.go#L221 |
| 不受支持的 CPU 架构 | server/etcdmain/etcd.go#L252 |
| SRV 端点发现失败 | server/etcdmain/util.go#L34 |
| 非法 listen-peer-urls | server/embed/config.go#L809-L812 |
| 非法 listen-client-urls | server/embed/config.go#L817-L821 |
| 非法 listen-client-http-urls | server/embed/config.go#L826-L830 |
| 非法 initial-advertise-peer-urls | server/embed/config.go#L835-L838 |
| 非法 advertise-client-urls | server/embed/config.go#L844-L847 |
| 非法 listen-metrics-urls | server/embed/config.go#L853-L856 |
其中几个值得深入看:
URL 类配置错误集中在 server/embed/config.go。六处均为相同模式:把逗号分隔的字符串交给 types.NewURLs 解析,失败即打印到 stderr 并 os.Exit(1):
if cfg.configJSON.ListenClientURLs != "" {
u, err := types.NewURLs(strings.Split(cfg.configJSON.ListenClientURLs, ","))
if err != nil {
fmt.Fprintf(os.Stderr, "unexpected error setting up listen-client-urls: %v\n", err)
os.Exit(1)
}
cfg.Config.ListenClientUrls = u
}
types.NewURLs(位于 client/pkg/types 包)会拒绝含空白、非 http/https 等不合法形式。这意味着把 --listen-client-urls 写成 127.0.0.1:2379(缺 scheme)这类错误,进程会以 1 退出并在 stderr 给出 unexpected error setting up listen-client-urls 提示,而不是静默回退默认值。
Discovery token 复用(server/etcdmain/etcd.go#L130-L142):bootstrap 时若 errors.DiscoveryError 表明 token 已被消费,日志会明确提示 “do not reuse discovery token; generate a new one to bootstrap a cluster”,然后 os.Exit(1)。这是用 v2 discovery 服务组集群时最常见的“换 token 重跑”场景。
datadir 校验(server/etcdmain/etcd.go#L195-L230):identifyDataDirOrDie 读取数据目录内容,目录不存在视为 dirEmpty(新实例);若 member 与 proxy 子目录同时存在则 Fatal(退出 1);目录既存在又无法读取权限时同样 Fatal。此外若目录被识别为 dirProxy,进程会直接 lg.Panic——v2 HTTP proxy 已在 3.6 起弃用。
架构检查(server/etcdmain/etcd.go#L232-L253):checkSupportArch 仅放行 amd64、arm64、ppc64le、s390x 四种 GOARCH;其余架构必须显式设置环境变量 ETCD_UNSUPPORTED_ARCH=<架构> 才可继续,否则以 1 退出。
监听失败(server/etcdmain/etcd.go#L170-L174):端口被占用等监听错误走 lg.Fatal("listener failed", ...),zap 的 Fatal 在记录后调用 os.Exit(1),因此所有 lg.Fatal/lg.Panic 类路径最终都收敛到退出码 1。
退出码 2:参数错误
参考文档将“flag 解析错误”单独归为退出码 2,源码即前文 parse() 的 default 分支(server/etcdmain/config.go#L116-L118):
default:
os.Exit(2)
触发条件包括:出现未定义的 flag、flag 值类型不匹配、布尔 flag 误加参数等。这与 cobra/pflag 生态的通用约定一致(如 shell 工具的 $? 检查),便于把“用户写错命令行”(2)与“etcd 运行环境/配置有问题”(1)在监控中区分开:前者应提示使用者检查命令,后者通常意味着端口、证书、datadir、集群发现等运维层面的故障。
与之相邻的还有一类“参数存在但不合法”的情况:flag 解析成功但 flagSet.Args() 非空时返回错误 "%q" is not a valid flag(server/etcdmain/config.go#L119-L121),该错误在 server/etcdmain/etcd.go#L64-L70 被判定为 “failed to verify flags” 后以 1 退出——即“多余的裸参数”归入一般错误而非参数错误。
实践指引:如何依据退出码判断 etcd 状态
结合上文,可以在运维脚本与编排配置中这样解读 $?:
- 0:进程正常结束。容器场景下
SIGTERM优雅关闭也属于此类(PID 1 路径),systemd/Kubernetes 视为正常停止,不会触发“异常重启”告警。 - 1:启动期致命失败。优先检查 stderr 与日志关键字:
failed to verify flags(配置校验)、discovery token was already used(token 复用)、failed to list data directory(datadir 权限/路径)、invalid datadir(member/proxy 并存)、listener failed(端口占用)、unexpected error setting up listen-*-urls(URL 格式)。 - 2:命令行写法错误,检查 flag 名称与参数。
- 143/130(仅 Linux):进程被 SIGTERM/SIGINT 终止。注意这是“被外部要求停止”,若由你主动
kill属预期;若非预期发生,应排查是谁发出了信号(编排系统、OOM 后的清理流程、killall等),而非 etcd 自身故障。
仓库的测试基础设施也依赖同一套语义:tests/framework/e2e/etcd_process.go 在 Wait()/IsRunning() 中读取 ep.proc.ExitCode() 并记录日志,多处把 unexpected exit code 作为信号终止的容忍结果处理;robustness 框架的 tests/robustness/failpoint/kill.go 同样遵循该判断模式。
平台差异与适用前提
- 143/130 的
128+信号号行为仅适用于 Linux。其他 Unix 平台dflSignal为空实现(pkg/osutil/signal.go),信号重发无法恢复内核默认处置,参考文档将其概括为“无错误退出 0,否则 1”。 - Windows 构建不适用 pkg/osutil/interrupt_unix.go(构建标签
//go:build !windows && !plan9)。 - 退出码语义由 server/main.go 入口经
etcdmain包驱动;本文所有行号均以当前仓库版本为准,与参考文档 Documentation/contributor-guide/exit_codes.md 锚定的旧 commit 相比,个别行号存在漂移,建议以本文给出的相对路径定位为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00