做视频点播系统做了三四年,最头疼的往往不是视频转码算法,也不是CDN调度,而是那些“看起来很简单、出问题就全链路崩”的分布式协调问题。比如上线高峰期两台转码机同时消费了同一个任务,比如播放器配置改了十分钟还没拉到新值,比如分片上传遇到节点宕机后不知道哪些片该重传。我是在第二次重构点播系统时彻底接入 Etcd-SDK 的,之后这些问题的定位速度明显上了一个台阶。这篇文章就把我在视频点播项目里用 Etcd-SDK 的经验完整梳理一遍,从选型思路、核心API、落地场景到线上踩坑,适合正在做点播、直播录制、媒体处理这类系统的后端开发或架构同学参考。
1. 为什么视频点播系统会需要Etcd-SDK
很多人一听到 Etcd 就想到 K8s 服务发现,觉得“我又不是做容器平台的,用不上”。这是对 Etcd 最大的误解。视频点播系统本质上是一个“媒资生产 + 分发”的分布式系统,里面有大量需要跨节点达成一致的状态,而这些状态用数据库硬撑会非常痛苦,用 Redis 又缺少一些关键保障,这时候 Etcd 的价值就出来了。
1.1 点播系统里的三类典型分布式问题
先把我项目里真切遇到的三个问题摆出来,大家对照一下自己的系统。
第一类是任务互斥。点播上传完成后要触发转码,转码任务可能是多档位(标清、高清、超清),如果用户传的是一小时的长视频,转码耗时很长。为了提速,我最初用 MySQL 的行锁实现“同一视频同一时间只允许一个转码任务执行”,结果单库扛不住高并发写入,又引入消息队列削峰,可消费者一多就有重复消费。两个转码节点同时拿到同一个任务ID,结果重复转码、重复计费、重复回调。
第二类是元数据一致性。视频的转码状态、分片上传进度、播放器配置、封面图生成状态,这些数据分散在多个服务和多张表里。业务上要求“用户上传完成后能立刻看到‘处理中’,转码完成后立刻变成‘可播放’”。在微服务架构下,如果没有一个可靠的“状态中枢”,就会出现用户看到的状态和自己的操作对不上。
第三类是节点协调与热更新。点播系统有转码节点、截图节点、审核节点、分发节点,这些节点要动态扩缩容。新增一个转码节点后,需要把节点注册到某个地方,让调度模块知道它的处理能力;同时转码模板、水印参数、播放鉴权开关这类配置,如果只能改完代码重新发布,运维成本极高。
MySQL 能解决部分问题,但高并发下锁竞争严重,而且难以提供 TTL 自动过期这类能力;Redis 能解决锁和缓存,但它的分布式锁在极端场景下(GC 停顿、网络分区)有失效风险,而且没有原生的 Watch 机制做事件订阅,配置变更需要轮询,延迟和负载都不理想。最终我选了 Etcd,因为它在一致性、TTL 租约、Watch 监听这三个维度上正好覆盖了上面的问题。
1.2 SDK在技术栈中的定位
引入 Etcd-SDK,并不是要拿它替代数据库,而是把它定位成“分布式协调基础设施层”。在我的架构里,Etcd 承担三类职责:服务注册与发现(转码节点、截图节点的状态上报)、分布式锁与选主(任务调度互斥)、配置中心 + 状态机存储(播放配置、任务状态、分片进度)。
层和层之间的调用关系大概这样:客户端上传视频后,接入层把“待转码任务”写入 Etcd 的某个前缀目录;转码节点启动时通过 Etcd 的 Lease 注册自身节点信息,并且用 Watch 监听任务前缀;转码完成后,节点更新任务状态并触发回调服务。整个过程里,Etcd-SDK 就是上层业务和 Etcd 集群之间的桥,所有 API 调用、连接管理、异常重试都在 SDK 层完成。
所以与其说“ Etcd-SDK 有什么用法”,不如先想清楚:你的点播系统里,哪些状态需要跨节点达成一致?哪些节点需要知道彼此存在?哪些配置需要热更新? 这些问题想清楚了,SDK 用起来就有了目标。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Etcd-SDK 的核心接口与调用逻辑
Etcd 官方仓库提供的 client 库就是我们说的 Etcd-SDK。不同语言的 SDK 虽然包名不同,但核心抽象完全一致:KV、Lease、Watch、Txn 四个对象。下面我用 Java 的 jetcd 和 Go 的 clientv3 双语言对照来讲,视频点播后端最常见的两种技术栈都能覆盖。
2.1 连接初始化与客户端复用
这一步看起来简单,但坑最多。很多人为了省事,每次操作都新建一个 client,这在低并发验证时看不出问题,一旦点播任务量上来,gRPC 连接数会爆增,Etcd 集群的连接数很快被打满。
Java 侧的正确姿势是使用单例客户端:
java复制// jetcd 客户端构建,注意使用单例,避免反复创建连接
Client etcdClient = Client.builder()
.endpoints("http://10.0.0.11:2379", "http://10.0.0.12:2379", "http://10.0.0.13:2379")
.user(ByteSequence.from("root", StandardCharsets.UTF_8))
.password(ByteSequence.from("yourpass", StandardCharsets.UTF_8))
.build();
Go 侧对应的是:
go复制cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"10.0.0.11:2379", "10.0
