0

0

Go 程序中线程数异常增长的原因与排查指南

花韻仙語

花韻仙語

发布时间:2026-01-02 14:12:44

|

195人浏览过

|

来源于php中文网

原创

Go 程序中线程数异常增长的原因与排查指南

go 程序内存持续上涨且线程数达上百,通常并非因 goroutine 泛滥,而是底层阻塞系统调用(如日志写入、文件 i/o、cgo 调用等)触发 go 运行时创建新 os 线程,导致线程泄漏和内存累积。

在 Go 中,“大量 goroutine” ≠ “大量 OS 线程”。Go 运行时通过 M:N 调度模型(M 个 OS 线程调度 N 个 goroutine)实现高并发,正常情况下活跃线程数由 GOMAXPROCS 控制(默认为 CPU 核心数),远低于 goroutine 数量。你观察到 Threads: 177 且内存长期不释放,说明存在 非预期的 OS 线程驻留,根源在于阻塞式系统调用未及时返回,导致运行时无法复用线程。

? 关键机制:什么情况下 Go 会创建新线程?

当一个 goroutine 执行以下操作时,运行时会将其从当前 M(OS 线程)上解绑,并可能新建线程:

  • 阻塞型系统调用(如 read()/write() 到慢速设备、open()、stat() 等);
  • 使用 cgo 调用 C 函数(尤其含阻塞逻辑);
  • 调用 runtime.LockOSThread() 显式绑定线程(你的代码未使用,可排除);
  • 某些第三方库内部封装的阻塞 I/O(如日志库、数据库驱动、加密库等)。

⚠️ 注意:标准库的 net.Conn 操作(如 conn.Read()/Write())是非阻塞的——它们基于 epoll/kqueue/io_uring 实现异步 I/O,不会导致线程增长。因此 handleClient 及 Session.handleRecv 中的网络读写本身不是元凶。

? 你的代码中最可疑的线程来源:日志写入

你使用了自定义日志模块 sanguo/base/log,并启用了文件写入:

filew := log.NewFileWriter("log", true)
err := filew.StartLogger() // 启动日志协程(极可能含阻塞 I/O)

若该日志器采用同步写文件(如直接 os.File.Write() + fsync()),尤其在磁盘负载高或 NFS 挂载时,每次写入都可能触发阻塞系统调用。Go 运行时为保障其他 goroutine 不被卡住,会分配新线程执行该阻塞调用。若日志高频且写入缓慢,线程将持续累积,且因未显式关闭,这些线程不会自动回收。

其他潜在风险点:

  • tcpkeepalive.EnableKeepAlive() 底层调用 setsockopt(),虽为轻量系统调用,但若其内部有锁竞争或错误路径,也可能间接引发线程行为;
  • json.Marshal() 本身无阻塞,但若 SendDirectly 中 sess.conn.Write() 因 TCP 窗口满而阻塞(罕见),理论上也可能触发线程切换(不过 net.Conn 默认非阻塞,实际概率极低)。

✅ 排查与修复方案

1. 验证线程归属

运行时检查线程状态:

Civitai
Civitai

AI艺术分享平台!海量SD资源和开源模型。

下载
# 查看进程所有线程的栈信息(需安装 delve 或使用 go tool pprof)
go tool pprof -threads http://localhost:6060/debug/pprof/threadcreate
# 或直接查看线程堆栈(Linux)
sudo cat /proc/$(pidof your_program)/stack | grep -A 5 -B 5 "sys"

重点关注中是否频繁出现 write, fsync, openat, epoll_wait(正常)或 futex, nanosleep(可疑阻塞)。

2. 替换/优化日志组件

  • 首选:改用异步日志库
    zap(带缓冲队列)或 logrus + hook 异步写入。
  • 次选:强制日志异步化
    将 filew.StartLogger() 改为启动 goroutine + channel 缓冲:
    logChan := make(chan string, 1000)
    go func() {
        for msg := range logChan {
            // 同步写文件,但由单一线程承担
            os.WriteFile("log.txt", []byte(msg+"\n"), 0644)
        }
    }()
    // 日志调用改为:logChan <- fmt.Sprintf("[DEBUG] %s", msg)

3. 设置线程上限(临时缓解)

通过环境变量限制最大 OS 线程数(防失控):

export GODEBUG="schedtrace=1000"  # 每秒打印调度器状态(调试用)
export GOMAXPROCS=4               # 严格限制 P 数(影响并发吞吐,慎用)
# 注:Go 无直接 GOMAXTHREADS,但可通过 runtime.LockOSThread() + 池管理模拟

4. 补充资源清理

确保连接关闭时释放所有资源:

func (sess *Session) Close() {
    sess.lock.Lock()
    if sess.ok {
        sess.ok = false
        close(sess.closeNotiChan)
        // ⚠️ 补充:关闭 recvChan 避免 goroutine 泄漏
        close(sess.recvChan)
        sess.conn.Close()
    }
    sess.lock.Unlock()
}

并在 handleDispatch 的 for 循环中处理 recvChan 关闭:

case msg, ok := <-sess.recvChan:
    if !ok { return } // chan closed
    log.Debug("msg", msg)
    sess.SendDirectly("helloworld", 1)

? 总结

  • Go 线程暴涨 ≠ goroutine 写错,而是阻塞系统调用未收敛所致;
  • 你的案例中,同步文件日志是最可能的罪魁祸首
  • 修复核心:将阻塞 I/O 移至专用 goroutine + 缓冲队列,或切换成熟异步日志库
  • 始终通过 pprof 和 /proc/PID/stack 验证线程行为,而非仅依赖猜测。

遵循以上方案,线程数将稳定在 GOMAXPROCS 附近,内存占用回归合理水平。

相关专题

更多
json数据格式
json数据格式

JSON是一种轻量级的数据交换格式。本专题为大家带来json数据格式相关文章,帮助大家解决问题。

403

2023.08.07

json是什么
json是什么

JSON是一种轻量级的数据交换格式,具有简洁、易读、跨平台和语言的特点,JSON数据是通过键值对的方式进行组织,其中键是字符串,值可以是字符串、数值、布尔值、数组、对象或者null,在Web开发、数据交换和配置文件等方面得到广泛应用。本专题为大家提供json相关的文章、下载、课程内容,供大家免费下载体验。

528

2023.08.23

jquery怎么操作json
jquery怎么操作json

操作的方法有:1、“$.parseJSON(jsonString)”2、“$.getJSON(url, data, success)”;3、“$.each(obj, callback)”;4、“$.ajax()”。更多jquery怎么操作json的详细内容,可以访问本专题下面的文章。

307

2023.10.13

go语言处理json数据方法
go语言处理json数据方法

本专题整合了go语言中处理json数据方法,阅读专题下面的文章了解更多详细内容。

74

2025.09.10

session失效的原因
session失效的原因

session失效的原因有会话超时、会话数量限制、会话完整性检查、服务器重启、浏览器或设备问题等等。详细介绍:1、会话超时:服务器为Session设置了一个默认的超时时间,当用户在一段时间内没有与服务器交互时,Session将自动失效;2、会话数量限制:服务器为每个用户的Session数量设置了一个限制,当用户创建的Session数量超过这个限制时,最新的会覆盖最早的等等。

302

2023.10.17

session失效解决方法
session失效解决方法

session失效通常是由于 session 的生存时间过期或者服务器关闭导致的。其解决办法:1、延长session的生存时间;2、使用持久化存储;3、使用cookie;4、异步更新session;5、使用会话管理中间件。

706

2023.10.18

cookie与session的区别
cookie与session的区别

本专题整合了cookie与session的区别和使用方法等相关内容,阅读专题下面的文章了解更详细的内容。

88

2025.08.19

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

371

2023.07.18

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

74

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 6.4万人学习

Git 教程
Git 教程

共21课时 | 2.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号