Skip to content

registry watcher 死锁:notify() 持 RLock 期间阻塞发送 chWatch,与 Stop() 写锁互锁(consul/etcd/nacos) #86

Description

@Conansgithub

版本

v2.5.8(registry/consulregistry/etcdregistry/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)
	...
}

触发链

  1. 服务实例频繁变更,watcherMgr.broadcast() 调用频率高于消费者 Next() 排空频率;
  2. chWatch(缓冲 16)写满后,notify() 阻塞在 w.chWatch <- services,此时仍持有 w.rw.RLock();
  3. 并发调用 Stop() 需要 w.rw.Lock()(写锁),因读锁被 notify() 持有而永久等待;
  4. 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.goregistry/etcd/watcher.goregistry/nacos/watcher.go 需同步修改。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions