v2node v0.4.1:DelUsers 执行时 LinkManager.CloseAll 自死锁,进而阻塞 XUDPManager,导致 goroutine 与堆内存持续增长
环境信息
- 程序:
/usr/local/v2node/v2node server
- v2node 模块版本:
github.com/wyx2685/v2node v0.4.1
- v2node 提交(来自 Go build info):
927701c30de32a7ab2a6091792e4697b2c9619e2
- 构建时间(来自 Go build info):
2026-06-01T14:08:26Z
- 二进制中依赖的 xray-core:
github.com/xtls/xray-core v1.260425.0 => github.com/wyx2685/xray-core 13357f3bc706dbae675223a42016a57ed27c3510
- 运行环境:Linux amd64,Go build info 显示
go1.26.1
现象描述
程序运行数天后,v2node 内存占用增长到约 1.7 GiB RSS,goroutine 数量增长到约 6 万个。
最新一次 pprof 采集数据:
timestamp=2026-07-02T21:12:50+08:00
pid=1996821
rss_kb=1801764
goroutines=60952
HeapAlloc=1601754560
HeapObjects=5600817
Stack=267845632
2026-07-02 当天自动采集的增长趋势:
12:00 rss=1658036KB heap=1176779504 goroutines=45863 xudp_lock_wait=43042
15:35 rss=1705000KB heap=1359599896 goroutines=53563 xudp_lock_wait=49824
19:39 rss=1811028KB heap=1530883504 goroutines=59431 xudp_lock_wait=56831
21:12 rss=1801764KB heap=1601754560 goroutines=60952 xudp_lock_wait=59631
从数据来看,这是真实的堆内存和 goroutine 持续增长,而不仅仅是 Go 运行时未将 RSS 归还给操作系统的现象。
关键 goroutine 证据
有 4 个 goroutine 卡在同一条 v2node 死锁路径上,示例如下:
goroutine 11583089 [sync.Mutex.Lock, 4584 minutes]:
sync.(*RWMutex).Lock
github.com/wyx2685/v2node/core/app/dispatcher.(*LinkManager).RemoveWriter
core/app/dispatcher/linkmanager.go:36
github.com/wyx2685/v2node/core/app/dispatcher.(*ManagedWriter).Close
core/app/dispatcher/linkmanager.go:20
github.com/wyx2685/v2node/core/app/dispatcher.(*LinkManager).CloseAll
core/app/dispatcher/linkmanager.go:45
github.com/wyx2685/v2node/core.(*V2Core).DelUsers
core/user.go:68
github.com/wyx2685/v2node/node.(*Controller).nodeInfoMonitor
node/task.go:116
这基本可以确定问题路径是:CloseAll() 持有 LinkManager.mu 锁的同时调用 common.Close(w),而该调用会进入 ManagedWriter.Close(),后者又会调用 RemoveWriter(),试图再次获取同一把 LinkManager.mu 锁。由于 Go 的 sync.RWMutex 不可重入,该 goroutine 因此自锁死。
对应源码(二进制所对应的提交):
ManagedWriter.Close() 在关闭 writer 之前会调用 w.manager.RemoveWriter(w):
|
func (w *ManagedWriter) Close() error { |
|
w.manager.RemoveWriter(w) |
|
return common.Close(w.writer) |
RemoveWriter() 内部执行 m.mu.Lock():
|
func (m *LinkManager) RemoveWriter(writer *ManagedWriter) { |
|
m.mu.Lock() |
|
defer m.mu.Unlock() |
|
delete(m.links, writer) |
CloseAll() 在遍历并调用 common.Close(w) 期间持有 m.mu.Lock():
|
func (m *LinkManager) CloseAll() { |
|
m.mu.Lock() |
|
defer m.mu.Unlock() |
|
for w, r := range m.links { |
|
common.Close(w) |
|
common.Interrupt(r) |
DelUsers() 会调用 lm.CloseAll():
|
tc.Delete(user) |
|
} |
|
if v, ok := vc.dispatcher.LinkManagers.Load(user); ok { |
|
lm := v.(*dispatcher.LinkManager) |
|
lm.CloseAll() |
|
vc.dispatcher.LinkManagers.Delete(user) |
对 XUDPManager 的连锁影响
有一个 xray-core 的 XUDP 清理 goroutine 也被同样卡住:
goroutine 24 [sync.Mutex.Lock, 3042 minutes]:
github.com/wyx2685/v2node/core/app/dispatcher.(*LinkManager).RemoveWriter
github.com/wyx2685/v2node/core/app/dispatcher.(*ManagedWriter).Close
github.com/xtls/xray-core/app/dispatcher.(*SizeStatWriter).Close
github.com/xtls/xray-core/common/mux.(*XUDP).Interrupt
common/mux/session.go:227
github.com/xtls/xray-core/common/mux.init.0.func1
common/mux/session.go:244
在所使用的 xray-core fork 中,XUDP 清理循环在持有全局 XUDPManager.Lock() 的情况下调用了 x.Interrupt():
由于 x.Interrupt() 会走到上面 v2node 的 ManagedWriter.Close() 死锁路径,这个清理 goroutine 将永远无法释放全局的 XUDPManager.Lock()。
此后,几乎所有 mux/XUDP worker 都会堆积在这里:
github.com/xtls/xray-core/common/mux.(*ServerWorker).handleStatusNew
common/mux/server.go:200
最新一次采集中的统计:
common/mux/server.go:200 lock wait: 59631
created by mux.NewServerWorker: 59651
相关 xray-core 代码:
这就解释了内存持续增长的原因:大量被阻塞的 mux worker 会持续占用其 goroutine 栈以及已分配的缓冲区。heap pprof 显示,主要的内存滞留集中在 xray-core/common/buf、bytespool、common/mux、proxy/freedom 以及 VLESS 入站相关路径。
预期行为
删除带有活跃连接的用户时,应当能够正常关闭/中断相关连接,而不应导致 LinkManager 死锁;某一个连接关闭卡住,也不应该永久阻塞全局的 XUDP manager。
v2node v0.4.1:
DelUsers执行时LinkManager.CloseAll自死锁,进而阻塞XUDPManager,导致 goroutine 与堆内存持续增长环境信息
/usr/local/v2node/v2node servergithub.com/wyx2685/v2node v0.4.1927701c30de32a7ab2a6091792e4697b2c9619e22026-06-01T14:08:26Zgithub.com/xtls/xray-core v1.260425.0 => github.com/wyx2685/xray-core 13357f3bc706dbae675223a42016a57ed27c3510go1.26.1现象描述
程序运行数天后,v2node 内存占用增长到约 1.7 GiB RSS,goroutine 数量增长到约 6 万个。
最新一次 pprof 采集数据:
2026-07-02 当天自动采集的增长趋势:
从数据来看,这是真实的堆内存和 goroutine 持续增长,而不仅仅是 Go 运行时未将 RSS 归还给操作系统的现象。
关键 goroutine 证据
有 4 个 goroutine 卡在同一条 v2node 死锁路径上,示例如下:
这基本可以确定问题路径是:
CloseAll()持有LinkManager.mu锁的同时调用common.Close(w),而该调用会进入ManagedWriter.Close(),后者又会调用RemoveWriter(),试图再次获取同一把LinkManager.mu锁。由于 Go 的sync.RWMutex不可重入,该 goroutine 因此自锁死。对应源码(二进制所对应的提交):
ManagedWriter.Close()在关闭 writer 之前会调用w.manager.RemoveWriter(w):v2node/core/app/dispatcher/linkmanager.go
Lines 19 to 21 in 927701c
RemoveWriter()内部执行m.mu.Lock():v2node/core/app/dispatcher/linkmanager.go
Lines 35 to 38 in 927701c
CloseAll()在遍历并调用common.Close(w)期间持有m.mu.Lock():v2node/core/app/dispatcher/linkmanager.go
Lines 41 to 46 in 927701c
DelUsers()会调用lm.CloseAll():v2node/core/user.go
Lines 64 to 69 in 927701c
对 XUDPManager 的连锁影响
有一个 xray-core 的 XUDP 清理 goroutine 也被同样卡住:
在所使用的 xray-core fork 中,XUDP 清理循环在持有全局
XUDPManager.Lock()的情况下调用了x.Interrupt():XUDP.Interrupt()会关闭x.Mux.output:https://github.com/wyx2685/xray-core/blob/13357f3bc706dbae675223a42016a57ed27c3510/common/mux/session.go#L225-L228
XUDPManager,再调用x.Interrupt():https://github.com/wyx2685/xray-core/blob/13357f3bc706dbae675223a42016a57ed27c3510/common/mux/session.go#L241-L245
由于
x.Interrupt()会走到上面 v2node 的ManagedWriter.Close()死锁路径,这个清理 goroutine 将永远无法释放全局的XUDPManager.Lock()。此后,几乎所有 mux/XUDP worker 都会堆积在这里:
最新一次采集中的统计:
相关 xray-core 代码:
handleStatusNew()读取一个数据包后,会在第 200 行尝试获取XUDPManager.Lock():https://github.com/wyx2685/xray-core/blob/13357f3bc706dbae675223a42016a57ed27c3510/common/mux/server.go#L195-L200
这就解释了内存持续增长的原因:大量被阻塞的 mux worker 会持续占用其 goroutine 栈以及已分配的缓冲区。heap pprof 显示,主要的内存滞留集中在
xray-core/common/buf、bytespool、common/mux、proxy/freedom以及 VLESS 入站相关路径。预期行为
删除带有活跃连接的用户时,应当能够正常关闭/中断相关连接,而不应导致
LinkManager死锁;某一个连接关闭卡住,也不应该永久阻塞全局的 XUDP manager。