首页
/ 使用 systemd 部署 etcd 多节点集群:从 unit 服务文件到开机自启与日志运维的完整实战

使用 systemd 部署 etcd 多节点集群:从 unit 服务文件到开机自启与日志运维的完整实战

2026-09-07 14:36:14作者:明树来

本指南以仓库 contrib/systemd/etcd3-multinode/README.md 为核心,讲解如何在三台机器上以 systemd 服务形式部署一个三节点 etcd 集群,覆盖数据目录准备、systemd unit 服务文件编写、开机自启、日志查看与停服全流程。读完本文,你将能够独立完成一套"由 systemd 托管、随开机自动拉起、可通过 journalctl 统一查日志"的多机 etcd 部署,并理解 Type=notify 与 etcd 就绪通知机制背后的源码实现。

部署方案总览

在生产环境中,将 etcd 交给 systemd 托管的好处在于:etcd 进程崩溃后会被自动拉起、开机时可随系统自动启动、标准输出与日志统一进入 journald 便于检索。本方案的拓扑如下:

  • 三台独立主机,分别命名 my-etcd-1my-etcd-2my-etcd-3
  • 每台主机各放置一个对应的 systemd unit 文件(my-etcd-1.service / my-etcd-2.service / my-etcd-3.service);
  • 客户端端口使用 etcd 约定的 2379,节点间 peer(Raft)通信端口使用 2380;
  • 三份 unit 文件中的 --initial-cluster 都完整列出三个成员,保证首次引导时彼此发现。

默认端口与仓库源码一致:在 server/embed/config.go 中定义了 DefaultListenPeerURLs = "http://localhost:2380"DefaultListenClientURLs = "http://localhost:2379";本文由于要组成多机集群,将这些 URL 显式替换为各主机实际的 IP。

第一步:准备数据目录

etcd 需要在宿主机上有一个数据目录用于存放 WAL 日志、快照与元数据(对应命令行参数 --data-dir)。文档给出如下准备命令:

sudo mkdir -p /var/lib/etcd
sudo chown -R root:$(whoami) /var/lib/etcd
sudo chmod -R a+rw /var/lib/etcd

命令含义拆解:

  • mkdir -p /var/lib/etcd:在宿主机创建数据目录 /var/lib/etcd-p 保证目录已存在时不报错;
  • chown -R root:$(whoami):把目录属主设为 root、属组设为当前登录用户($(whoami) 会展开为执行者用户名),使系统用户组可写入;
  • chmod -R a+rw:授予所有用户读写权限,确保以不同身份启动的 etcd 进程都能写入。

提示:本文示例直接以 root 身份运行 etcd。更严格的生产做法是为 etcd 创建专用系统用户(例如 contrib/systemd/etcd.service 中使用的 User=etcd)并收紧目录权限,可结合自身安全基线调整。

数据目录对应关系可以在 etcd.conf.yml.sample(配置项 data-dir:)以及 server/embed/config.go 的 flag 注册 fs.StringVar(&cfg.Dir, "data-dir", cfg.Dir, "Path to the data directory.") 中确认:--data-dir 是 etcd server 的标准启动参数,三台机器需各自准备。

理解 unit 服务文件中的关键 systemd 指令

在动手写文件前,先拆解文档中 service 文件反复出现的几行配置,它们决定了 etcd 被 systemd 托管的行为:

指令 取值 作用
Type=notify 告诉 systemd:进程在完成初始化后通过 sd_notify 主动上报 READY=1,在此之前 unit 一直处于 "activating" 状态,防止上层依赖过早启动
Restart=always 进程异常退出后总是由 systemd 重启(正常 stop 除外)
RestartSec=5s 5s 重启前等待的秒数,避免故障时疯狂重启消耗资源
LimitNOFILE=40000 40000 调高进程文件描述符上限。etcd 需要为大量并发客户端连接与 WAL 文件保持句柄,默认软限制通常不够用
TimeoutStartSec=0 0 关闭 systemd 的启动超时判定。集群引导时需要等待选主与成员间握手,可能耗时较长,置 0 表示不设超时
Conflicts=etcd.service / Conflicts=etcd2.service 与旧式 etcd/etcd2 服务互斥,避免同一主机上多套 etcd 服务冲突
WantedBy=multi-user.target [Install] 段的安装目标,执行 systemctl enable 后即随多用户运行级别开机自启

Type=notify 的源码依据:etcd 何时上报就绪

Type=notify 能够成立的前提是 etcd 进程真的会在合适时机发送就绪通知。这一点在仓库源码中有明确实现:etcd 通过 github.com/coreos/go-systemd/v22daemon 包(依赖声明见 server/go.mod)调用 sd_notify

就绪通知入口 server/etcdmain/main.go

func notifySystemd(lg *zap.Logger) {
	lg.Info("notifying init daemon")
	_, err := daemon.SdNotify(false, daemon.SdNotifyReady)
	if err != nil {
		lg.Error("failed to notify systemd for readiness", zap.Error(err))
		return
	}
	lg.Info("successfully notified init daemon")
}

而调用时机在 server/etcdmain/etcd.go 中给出明确注释:

"At this point, the initialization of etcd is done. The listeners are listening on the TCP ports and ready for accepting connections. The etcd instance should be joined with the cluster and ready to serve incoming connections."

也就是说:只有当 etcd 的监听端口已就绪、实例已加入集群、可以对外服务时,才会调用 notifySystemd 发送 READY=1。因此 unit 文件里写 Type=notify 是安全且准确的——systemd 不会在 etcd 真正就绪前把服务标记为 active。(同理,gatewaygrpc-proxy 子命令在启动完成后也会调用 notifySystemd,见 server/etcdmain/gateway.goserver/etcdmain/grpc_proxy.go。)

编写三个节点的 systemd 服务文件

在三台机器上分别执行以下操作(注意把 ${IP_1}${IP_2}${IP_3} 替换为三台机器各自真实的 IP)。每份文件先写入 /tmp,再 mv/etc/systemd/system/

节点 my-etcd-1 的服务文件(在主机 1 上执行)

cat > /tmp/my-etcd-1.service <<EOF
[Unit]
Description=etcd
Conflicts=etcd.service
Conflicts=etcd2.service

[Service]
Type=notify
Restart=always
RestartSec=5s
LimitNOFILE=40000
TimeoutStartSec=0

ExecStart=etcd --name my-etcd-1 \
    --data-dir /var/lib/etcd \
    --listen-client-urls http://${IP_1}:2379 \
    --advertise-client-urls http://${IP_1}:2379 \
    --listen-peer-urls http://${IP_1}:2380 \
    --initial-advertise-peer-urls http://${IP_1}:2380 \
    --initial-cluster my-etcd-1=http://${IP_1}:2380,my-etcd-2=http://${IP_2}:2380,my-etcd-3=http://${IP_3}:2380 \
    --initial-cluster-token my-etcd-token \
    --initial-cluster-state new

[Install]
WantedBy=multi-user.target
EOF
sudo mv /tmp/my-etcd-1.service /etc/systemd/system/my-etcd-1.service

节点 my-etcd-2 的服务文件(在主机 2 上执行)

cat > /tmp/my-etcd-2.service <<EOF
[Unit]
Description=etcd
Conflicts=etcd.service
Conflicts=etcd2.service

[Service]
Type=notify
Restart=always
RestartSec=5s
LimitNOFILE=40000
TimeoutStartSec=0

ExecStart=etcd --name my-etcd-2 \
    --data-dir /var/lib/etcd \
    --listen-client-urls http://${IP_2}:2379 \
    --advertise-client-urls http://${IP_2}:2379 \
    --listen-peer-urls http://${IP_2}:2380 \
    --initial-advertise-peer-urls http://${IP_2}:2380 \
    --initial-cluster my-etcd-1=http://${IP_1}:2380,my-etcd-2=http://${IP_2}:2380,my-etcd-3=http://${IP_3}:2380 \
    --initial-cluster-token my-etcd-token \
    --initial-cluster-state new

[Install]
WantedBy=multi-user.target
EOF
sudo mv /tmp/my-etcd-2.service /etc/systemd/system/my-etcd-2.service

节点 my-etcd-3 的服务文件(在主机 3 上执行)

cat > /tmp/my-etcd-3.service <<EOF
[Unit]
Description=etcd
Conflicts=etcd.service
Conflicts=etcd2.service

[Service]
Type=notify
Restart=always
RestartSec=5s
LimitNOFILE=40000
TimeoutStartSec=0

ExecStart=etcd --name my-etcd-3 \
    --data-dir /var/lib/etcd \
    --listen-client-urls http://${IP_3}:2379 \
    --advertise-client-urls http://${IP_3}:2379 \
    --listen-peer-urls http://${IP_3}:2380 \
    --initial-advertise-peer-urls http://${IP_3}:2380 \
    --initial-cluster my-etcd-1=http://${IP_1}:2380,my-etcd-2=http://${IP_2}:2380,my-etcd-3=http://${IP_3}:2380 \
    --initial-cluster-token my-etcd-token \
    --initial-cluster-state new

[Install]
WantedBy=multi-user.target
EOF
sudo mv /tmp/my-etcd-3.service /etc/systemd/system/my-etcd-3.service

ExecStart 启动参数逐项说明

参数 说明 备注
--name 本成员在集群内唯一的人读名字,my-etcd-1/2/3 --initial-cluster 中的成员名一一对应;重启后必须保持不变
--data-dir 数据目录,对应第一步创建的 /var/lib/etcd 首次引导时目录必须为空
--listen-client-urls 本节点监听客户端 gRPC 流量的地址 2379 是 etcd 客户端端口约定(默认值见 server/embed/config.go
--advertise-client-urls 向集群其他成员与客户端通告的本节点客户端地址 须为其他机器可达的地址
--listen-peer-urls 本节点监听 Raft peer 通信的地址 2380 是 etcd peer 端口约定(默认值见 server/embed/config.go
--initial-advertise-peer-urls 向集群其他成员通告的本节点 peer 地址 集群成员间互相拨号的目标
--initial-cluster 首次引导时的完整集群成员表,形如 name1=url1,name2=url2,... 三份文件中的内容必须完全一致;flag 注册见 server/embed/config.go
--initial-cluster-token 集群引导令牌,用于区分同网段内可能存在的多个 etcd 集群,防止串扰 三节点必须一致
--initial-cluster-state 取值 new 表示以空数据目录创建新集群;existing 表示加入既有集群 flag 说明见 server/embed/config.go

关键实践点:--initial-cluster--initial-cluster-token--initial-cluster-state new 这三组"引导参数"只在首次以空数据目录创建集群时起作用。三个成员一旦完成引导、把集群成员信息写入本地 WAL,之后即使服务被重启(例如 systemctl restart),etcd 也会以本地已持久化的集群信息为准继续工作。因此三台机器的 --name 与端口必须保持稳定,否则成员身份会与数据目录中的记录不一致。

命令行参数的两种写法:flag 与 ETCD_* 环境变量

在 systemd unit 中除了把参数写进 ExecStart,还可以使用 Environment= 注入 ETCD_* 环境变量,二者等价。仓库中的单节点示例 contrib/systemd/etcd.service 就是后者的代表,例如:

[Service]
User=etcd
Type=notify
Environment=ETCD_DATA_DIR=/var/lib/etcd
Environment=ETCD_NAME=%m
ExecStart=/usr/bin/etcd
Restart=always
RestartSec=10s
LimitNOFILE=40000

其中 ETCD_DATA_DIR 对应 --data-dirETCD_NAME 对应 --name%m 是 systemd 展开为机器 ID 的占位符)。多机集群若想统一管理,也可以把 --initial-cluster 等参数改写为 ETCD_INITIAL_CLUSTERETCD_INITIAL_CLUSTER_TOKENETCD_INITIAL_CLUSTER_STATE 环境变量形式。需要强调的是:无论哪种写法,Type=notify 都不可或缺——etcd 正是依赖它向上报 READY=1,systemd 才能准确判断启动何时完成。

启动服务并配置开机自启

unit 文件就位后,需要先让 systemd 重新加载配置(daemon-reload),再 enable(创建开机自启的符号链接,防止系统重启后 etcd 丢失)与 start。三台机器分别执行:

主机 1:

sudo systemctl daemon-reload
sudo systemctl enable my-etcd-1.service
sudo systemctl start my-etcd-1.service

主机 2:

sudo systemctl daemon-reload
sudo systemctl enable my-etcd-2.service
sudo systemctl start my-etcd-2.service

主机 3:

sudo systemctl daemon-reload
sudo systemctl enable my-etcd-3.service
sudo systemctl start my-etcd-3.service

集群引导提示:三个成员需要相互发现并通过 Raft 选出 leader,因此"全部就位"通常需要等三台都执行完 start。若某台机器启动较早,其日志中可能出现等待其他成员建连的提示,这是正常的引导过程。由于 etcd 的三节点集群只要有两台(多数派)存活即可对外服务,实际维护中应避免同时重启超过一台节点。

查看运行状态与日志

systemd 会把 etcd 的日志收集到 journald(这也是为何 etcd.conf.yml.samplelog-outputs 的注释写明:只有显式指定 stdout/stderr 才会"skip journald logging even when running under systemd")。因此无需重定向日志文件,直接用 systemctl statusjournalctl 即可完成排障。

主机 1:

sudo systemctl status my-etcd-1.service -l --no-pager
sudo journalctl -u my-etcd-1.service -l --no-pager|less
sudo journalctl -f -u my-etcd-1.service

主机 2:

sudo systemctl status my-etcd-2.service -l --no-pager
sudo journalctl -u my-etcd-2.service -l --no-pager|less
sudo journalctl -f -u my-etcd-2.service

主机 3:

sudo systemctl status my-etcd-3.service -l --no-pager
sudo journalctl -u my-etcd-3.service -l --no-pager|less
sudo journalctl -f -u my-etcd-3.service

各命令的用途:

  • systemctl status <svc> -l --no-pager:查看 unit 当前状态(active/failed)、最近日志与进程信息,-l 不折叠长行,--no-pager 避免进入分页器,便于脚本与 CI 场景直接输出;
  • journalctl -u <svc>:按 unit 过滤并查看 journald 中的完整历史日志,管道给 less 便于翻页检索;
  • journalctl -f -u <svc>:以 follow 模式实时跟踪日志,适合观察集群引导过程与 leader 切换。

排障时可重点在日志中确认三件事:节点是否成功通知 init daemon(notifying init daemon / successfully notified init daemon,对应 server/etcdmain/main.go 的日志)、集群成员是否互相建连、以及是否选出了 leader。

停止并禁用服务

若需要下线某节点(例如整机维护、替换成员),执行 stop 停止进程,再执行 disable 取消开机自启:

主机 1:

sudo systemctl stop my-etcd-1.service
sudo systemctl disable my-etcd-1.service

主机 2:

sudo systemctl stop my-etcd-2.service
sudo systemctl disable my-etcd-2.service

主机 3:

sudo systemctl stop my-etcd-3.service
sudo systemctl disable my-etcd-3.service

运维提醒:对于运行中的三节点集群,disable 一台后若长期不恢复,剩余两台仍可维持多数派继续服务;但如果再停掉一台,集群将因失去多数派而进入只读/不可用状态(Raft 需要至少 (n+1)/2 即 2 台存活)。因此停服操作应遵循"先确认对端,再逐台操作"的原则,集群成员变更建议参考官方 etcdctl member 相关工具做正式成员移除,而不是只停系统服务。

延伸:将单机示例改造为多机集群的对照

仓库同时提供了单机版 systemd 示例 contrib/systemd/etcd.service,其核心差异值得对照理解:

  1. 成员命名:单机用 Environment=ETCD_NAME=%m(机器 ID),多机用固定的 my-etcd-N 并写入 --initial-cluster
  2. 集群参数:单机没有 --initial-cluster / --initial-cluster-state(默认按单成员引导),多机必须显式声明全部成员;
  3. 运行用户:示例使用专用 User=etcd,多机 README 直接以 root 运行——两者对数据目录权限要求不同,迁移到专用用户时需相应收紧 /var/lib/etcd 的属主与权限;
  4. Type=notify 与重启策略:两者一致(Type=notifyRestart=alwaysLimitNOFILE=40000),可见这些是与进程模型强相关的固定项。

生产化建议与安全提示

  • 务必启用 TLS:本文与 README 示例为便于演示全部使用 http:// 明文。生产环境建议为客户端(2379)与 peer(2380)分别配置证书,相关参数(cert-filekey-filetrusted-ca-fileclient-cert-authauto-tls 等)可在 etcd.conf.yml.sampleclient-transport-securitypeer-transport-security 段中查看完整清单;
  • 限制监听范围--listen-client-urls / --listen-peer-urls 应只监听集群内网卡或受信网络,配合防火墙仅放行必要端口(客户端 2379、peer 2380),不要直接暴露到公网;
  • 独立账号与目录权限:参考 contrib/systemd/etcd.service 使用专用 etcd 用户与 User=etcd 指令运行,避免 etcd 以 root 身份常驻;
  • 文件描述符与资源限制:保留 LimitNOFILE=40000,若预期连接数很大可结合监控继续上调;
  • 持久化与备份/var/lib/etcd 是 etcd 的全部状态所在,集群由三节点互为冗余,但若数据需要长期留存,仍应依据官方 snapshot 机制做定期快照备份(对应 etcdutl/snapshot 工具,见 etcdutl/snapshot/v3_snapshot.go)。

至此,从数据目录、三份 unit 服务文件,到 enable/start、journald 查日志、stop/disable 的全套 systemd 多机 etcd 集群运维闭环已经完整落地,可直接作为三节点生产部署的基线模板使用。

登录后查看全文
热门项目推荐
相关项目推荐