版本
v2.5.8(registry/consul、registry/etcd、registry/nacos 三处同一模式)
问题
registry watcher 的 notify() 在持有 w.rw.RLock() 期间执行阻塞式 channel 发送,当 channel 缓冲写满时会在持锁状态下永久阻塞,与 Stop() 需要的写锁形成死锁,导致 watcher / locator 关停 hang。
以 registry/consul/watcher.go 为例(etcd / nacos 完全一致):
func (w *watcher) notify(services []*registry.ServiceInstance) {
w.rw.RLock()
defer w.rw.RUnlock()
if w.state == stateRunning {
w.chWatch <- services // 持 RLock 时阻塞发送;chWatch 缓冲仅 16
}
}
func (w *watcher) Stop() error {
w.rw.Lock() // 需要写锁
defer w.rw.Unlock()
...
w.cancel()
close(w.chWatch)
...
}
触发链
- 服务实例频繁变更,
watcherMgr.broadcast() 调用频率高于消费者 Next() 排空频率;
chWatch(缓冲 16)写满后,notify() 阻塞在 w.chWatch <- services,此时仍持有 w.rw.RLock();
- 并发调用
Stop() 需要 w.rw.Lock()(写锁),因读锁被 notify() 持有而永久等待;
Stop() 无法执行到 w.cancel() / close(w.chWatch),消费者也无从被唤醒去排空 → 互锁,关停永久 hang。
根因
这把 RLock 是在 703368b "Optimizing registry" 中加入的,目的是防止 Stop() 的 close(w.chWatch) 与 notify() 的 send 竞争(send on closed channel panic)——初衷正确;但"持锁 + 阻塞发送"引入了上述死锁。问题本质与已修复的 #80(session 读写锁嵌套死锁)同类。
建议修复(最小、三处一致)
让 notify() 的发送永不阻塞,持锁时间趋近于零,Stop() 即可随时拿到写锁。由于每条消息都是完整的服务实例快照(wm.services()),"丢最旧、补最新"不破坏最终一致性:
func (w *watcher) notify(services []*registry.ServiceInstance) {
w.rw.RLock()
defer w.rw.RUnlock()
if w.state != stateRunning {
return
}
select {
case w.chWatch <- services:
default: // 缓冲已满:丢弃最旧一条,确保投递最新快照
select {
case <-w.chWatch:
default:
}
select {
case w.chWatch <- services:
default:
}
}
}
(另一种更彻底的方向:不再用 close(chWatch) + 锁来协调生命周期,改为仅靠 w.ctx 取消、消费端 Next() 的 select 已经监听 w.ctx.Done(),notify 发送时 select 同时监听 w.ctx.Done() 即可,但改动较大。具体取舍由维护者决定。)
registry/consul/watcher.go、registry/etcd/watcher.go、registry/nacos/watcher.go 需同步修改。
版本
v2.5.8(
registry/consul、registry/etcd、registry/nacos三处同一模式)问题
registry watcher 的
notify()在持有w.rw.RLock()期间执行阻塞式 channel 发送,当 channel 缓冲写满时会在持锁状态下永久阻塞,与Stop()需要的写锁形成死锁,导致 watcher / locator 关停 hang。以
registry/consul/watcher.go为例(etcd / nacos 完全一致):触发链
watcherMgr.broadcast()调用频率高于消费者Next()排空频率;chWatch(缓冲 16)写满后,notify()阻塞在w.chWatch <- services,此时仍持有w.rw.RLock();Stop()需要w.rw.Lock()(写锁),因读锁被notify()持有而永久等待;Stop()无法执行到w.cancel()/close(w.chWatch),消费者也无从被唤醒去排空 → 互锁,关停永久 hang。根因
这把
RLock是在703368b "Optimizing registry"中加入的,目的是防止Stop()的close(w.chWatch)与notify()的 send 竞争(send on closed channel panic)——初衷正确;但"持锁 + 阻塞发送"引入了上述死锁。问题本质与已修复的 #80(session 读写锁嵌套死锁)同类。建议修复(最小、三处一致)
让
notify()的发送永不阻塞,持锁时间趋近于零,Stop()即可随时拿到写锁。由于每条消息都是完整的服务实例快照(wm.services()),"丢最旧、补最新"不破坏最终一致性:(另一种更彻底的方向:不再用
close(chWatch)+ 锁来协调生命周期,改为仅靠w.ctx取消、消费端Next()的 select 已经监听w.ctx.Done(),notify发送时select同时监听w.ctx.Done()即可,但改动较大。具体取舍由维护者决定。)registry/consul/watcher.go、registry/etcd/watcher.go、registry/nacos/watcher.go需同步修改。