Skip to content

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

Description

@Conansgithub

版本

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)
	...
}

触发链

  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.go、registry/etcd/watcher.go、registry/nacos/watcher.go 需同步修改。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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