Skip to content

Conversation

@azio7
Copy link
Contributor

@azio7 azio7 commented Sep 27, 2024

我也是go半吊子,用 pprof 看了一下

goroutine profile: total 4369
2169 @ 0x47054e 0x405eed 0x405b57 0x6daaaf 0x478341
#	0x6daaae	main.ioReflector+0x26e	/home/azio7/project/SNIProxy/main.go:271

2169 @ 0x47054e 0x405eed 0x405b57 0x6dabff 0x6daac2 0x478341
#	0x6dabfe	main.onDisconnect+0x3e	/home/azio7/project/SNIProxy/main.go:279
#	0x6daac1	main.ioReflector+0x281	/home/azio7/project/SNIProxy/main.go:272

可能是存在goroutine泄漏,代码里是向conChk发送两次1,但只接收一次

删除与defer重复的260 261行
dst.Close()
conChk <- 1

仅解决一半的泄漏问题

一顿copliot后:

为了确保没有 goroutine 泄漏,我们需要确保在任何情况下都能正确地关闭所有 goroutine。我们可以通过使用 context 来管理 goroutine 的生命周期,并确保在主 goroutine 退出时,所有子 goroutine 都能被正确取消。

重构后的代码

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

conChk := make(chan struct{})
go ioReflector(ctx, backend, conn, false, conChk, raddr, dst)
go ioReflector(ctx, conn, backend, true, conChk, raddr, dst)

// 等待任意一个 ioReflector 完成
<-conChk
// 取消上下文,通知另一个 ioReflector 退出
cancel()

// ioReflector 函数接收一个 io.WriteCloser 类型的写入对象 dst、一个 io.Reader 类型的读取对象 src、一个 bool 类型的 isToClient、一个 chan struct{} 类型的 conChk,以及两个字符串类型的 raddr 和 dsts
// 该函数使用 io.Copy 函数将 src 中读取到的数据流复制到 dst 中,然后将转发的字节数写入日志
// 最后,该函数关闭 dst 连接,并向 conChk 通道发送一个信号以表示连接已关闭。
func ioReflector(ctx context.Context, dst io.WriteCloser, src io.Reader, isToClient bool, conChk chan struct{}, raddr string, dsts string) {
	// 将 IO 流反映到另一个
	defer onDisconnect(dst, conChk)

	done := make(chan struct{})
	go func() {
		written, _ := io.Copy(dst, src)
		if isToClient {
			serviceLogger(fmt.Sprintf("[%v] -> [%v] %d bytes", dsts, raddr, written), 33, true)
		} else {
			serviceLogger(fmt.Sprintf("[%v] -> [%v] %d bytes", raddr, dsts, written), 33, true)
		}
		close(done)
	}()

	select {
	case <-ctx.Done():
		// 上下文取消,退出
	case <-done:
		// 复制完成,退出
	}
}

// onDisconnect 函数接收一个 io.WriteCloser 类型的写入对象 dst 和一个 chan struct{} 类型的 conChk
// 该函数在 dst 连接关闭时被调用,并向 conChk 通道发送一个信号以表示连接已关闭
func onDisconnect(dst io.WriteCloser, conChk chan struct{}) {
	// 关闭时 -> 强制断开另一对连接
	dst.Close()
	select {
	case conChk <- struct{}{}:
	default:
	}
}

解释

  1. 使用 context.Context:在主函数中创建一个 context.Con*text,并传递给 ioReflector 函数。*
  2. 等待任意一个 ioReflector 完成:使用 <-conChk 等待任意一个 ioReflector 完成,然后调用 cancel() 取消上下文,通知另一个 ioReflector 退出。
  3. 修改 ioReflector 函数:在 ioReflector 函数中使用 select 语句等待 context 取消或 io.Copy 完成。
  4. 修改 onDisconnect 函数:确保在关闭连接时不会阻塞 conChk 通道。

通过这种方式,可以确保在主 goroutine 退出时,所有子 goroutine 都能被正确取消,避免资源泄漏。

测试copliot的代码,结果在chan处堵塞的goroutine与io.Copy处的goroutine保持一致(算修好了吧)

go.mod go.sum 处的修改是go mod tidy自动改的

@XIU2 XIU2 mentioned this pull request Sep 27, 2024
@XIU2
Copy link
Owner

XIU2 commented Sep 27, 2024

我简单测试了下,没发现啥问题,不过我还是将你的代码编译后,发给 #17 让其帮忙测试下,看在高负载下是否还会存在内存泄漏问题,如果他测试没问题的话,我再合并好了。

@hieixu
Copy link

hieixu commented Sep 27, 2024

确定解决了我这边的内存泄漏问题
跑了10分钟,打了2万次请求,内存基本保持着10M左右

@XIU2 XIU2 merged commit 5bfe206 into XIU2:main Sep 27, 2024
@XIU2
Copy link
Owner

XIU2 commented Sep 27, 2024

已合并 PR 并 正式发布 v1.0.4 版本:
https://github.com/XIU2/SNIProxy/releases/tag/v1.0.4


感谢帮忙修复哈~

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants