bugfix:add lock to protect task delete and task kill
概述
在容器运行时(如 containerd)中,对任务(Task)生命周期的管理需要处理多个并发操作。本次修复的 PR 旨在解决一个具体的竞态条件:当一个 Delete(删除)操作正在执行时,另一个并发的 Kill(终止)操作会因为底层的 tRPC 服务已关闭而失败。这种问题会直接影响系统的可靠性和错误处理的健壮性,是 SRE 在维护大规模容器编排平台时需要关注和防范的典型场景。通过引入细粒度的锁机制,该修复确保了任务状态操作的原子性,防止了因并发导致的意外故障。
核心概念
- 竞态条件 (Race Condition):指多个并发操作(如 goroutine)访问共享资源(如任务状态),且其最终结果取决于执行的相对时序。在本案中,
Delete和Kill同时操作同一个任务,就可能引发竞态。 - 锁 (Lock):用于协调多个执行线程对共享资源访问的同步机制。通过获取锁,一个操作可以确保在它执行期间,其他试图操作同一资源的操作必须等待。
- 任务生命周期 (Task Lifecycle):在 containerd 中,任务代表一个正在运行的容器化进程。其状态机通常包括创建、运行、停止、删除等阶段。
Delete和Kill是两个关键的生命周期转换操作。 - tRPC:containerd 中用于组件间通信的远程过程调用协议。
Delete操作会停止与任务关联的 shim 进程的 tRPC 服务,这会导致后续任何依赖该服务的调用(如Kill)失败。
实践
该修复通过在关键路径上添加锁,保护了共享状态的访问。具体而言:
-
问题场景:当
local.Delete被调用时,它会调用TaskManager.Delete,进而执行shimTask.delete。这个删除过程会关闭 shim 的 tRPC 服务。如果在这一过程中或之后,另一个请求试图通过local.Kill来终止同一个任务,Kill需要先通过getTaskFromContainer获取任务对象,而获取过程依赖的 tRPC 服务可能已经关闭,导致错误。 -
解决方案:在
getTaskFromContainer函数和local层的Delete操作中引入同一把锁(l.locker)。- 在
getTaskFromContainer中,使用l.locker.Lock(context.Background(), container.ID)获取锁。这确保了在获取任务对象期间,该任务 ID 不会被其他操作(如删除)移除。 - 在
local的Delete操作中(代码未完全展示,但根据 PR 描述),也应该获取同一把锁。这确保了删除操作和获取任务对象的操作不会重叠执行。
这样,
Kill请求通过getTaskFromContainer获取任务时,如果该任务正被删除,Kill会等待锁释放。如果锁释放后任务已被删除,则getTaskFromContainer会返回明确的NotFound错误,而不是因 tRPC 服务关闭而产生模糊错误。// 伪代码示意修复后的逻辑 func (l *local) Kill(...) { // ... 前置检查 ... // 调用被锁保护的 getTaskFromContainer t, err := l.getTaskFromContainer(ctx, container) if err != nil { return err // 可能是 "task not found" } // ... 后续终止逻辑 ... } func (l *local) getTaskFromContainer(...) { l.locker.Lock(context.Background(), container.ID) // 获取锁 defer l.locker.Unlock(container.ID) // ... 获取任务 ... } func (l *local) Delete(...) { l.locker.Lock(context.Background(), containerID) // 获取同一把锁 defer l.locker.Unlock(containerID) // ... 执行删除,包括停止 tRPC 服务 ... } - 在
SRE 覧点
与可靠性的关联
- 提升系统鲁棒性:此修复消除了一个可能导致操作失败的竞态条件,使得任务管理逻辑在高并发场景下更加可靠。SRE 应将此类并发缺陷视为潜在的故障模式。
- 错误处理明确化:修复后,当尝试操作一个已被删除的任务时,系统会返回一个明确的“未找到”错误,而非难以诊断的连接错误。这极大地改善了问题的可诊断性和事件响应效率。
最佳实践与常见反模式
- 最佳实践:对共享状态的访问,尤其是涉及生命周期转换(创建/删除)的操作,应仔细评估并发需求,必要时使用锁或其他同步原语。锁的粒度应尽量细(如本案中按容器 ID 加锁),以减少对性能的影响。
- 常见反模式:假设操作是串行的,忽略并发访问的可能性。在分布式或事件驱动的系统中,这往往是生产环境故障的根源。
相关指标
- 虽然此修复本身不直接定义 SLI/SLO,但它影响错误率的衡量。一个由竞态条件引起的
ttrpc closed错误,本不应计入用户侧的有效错误预算。修复此类问题有助于使 错误预算 (Error Budget) 的消耗更准确地反映真实的用户体验问题,而非系统内部实现缺陷。