线程/goroutine 消耗分析
线程/goroutine 消耗分析
概述
程序使用 goroutine(Go 的轻量级线程)来并发执行任务。goroutine 由 Go 运行时管理,通常一个 goroutine 对应一个操作系统线程,但在 I/O 阻塞时会自动让出线程给其他 goroutine。
固定消耗
1. 主 goroutine
- 数量: 1
- 作用: 主程序流程,协调所有任务
- 生命周期: 程序运行期间
2. 监控 goroutine (MonitorTaskStatus)
- 数量: 1
- 作用: 实时监控数据库,输出任务状态变化到标准输出
- 生命周期: 从程序启动到所有任务完成(通过 context 控制)
- 资源消耗:
- 每秒查询一次本地数据库(使用
time.NewTicker(1 * time.Second)) - 每秒更新一次全局数据库(如果配置了全局数据库)
- 内存:维护
lastStatusmap(每个任务约 100-200 字节) - 输出格式:表格格式,包含
try task status retry taskid exitcode time列 - 不显示 Pending 状态的任务
- 每秒查询一次本地数据库(使用
Local 模式线程消耗
任务执行 goroutine
每个任务启动 1 个 goroutine,通过 gpool 控制并发数(-p 参数)。
生命周期
启动 → 查询数据库 → 更新状态为 Running → 启动子进程 → 等待子进程完成 → 更新状态 → 退出
阻塞点分析
数据库操作(短暂阻塞)
QueryRow: 查询任务信息(< 10ms)Exec: 更新状态(< 10ms)- 通过
write_pool(大小为1)串行化,避免并发冲突
进程启动(短暂阻塞)
cmd.Start(): 启动子进程(< 100ms)
进程等待(长时间阻塞,但会释放线程)
cmd.Wait(): 阻塞等待子进程完成- 这是主要的阻塞点,但 Go 运行时会在 I/O 阻塞时自动让出线程
- 实际线程消耗:取决于子进程运行时间,但 goroutine 会复用线程
资源消耗总结
- Goroutine 数量: 最多
-t个(并发控制) - 实际线程数: 通常远少于 goroutine 数(Go 运行时复用)
- 内存消耗: 每个 goroutine 约 2KB 栈空间
- CPU 消耗: 主要在子进程,主程序消耗很少
示例计算
假设 -t 10,有 100 个任务:
- 同时运行的 goroutine: 最多 10 个
- 实际线程数: 通常 2-4 个(Go 运行时根据 CPU 核心数调整)
- 总 goroutine 数: 100 个(但只有 10 个同时运行)
- 内存: 约 10 × 2KB = 20KB(活跃 goroutine)+ 监控 goroutine ≈ 25KB
QsubSge 模式线程消耗
任务提交和监控 goroutine
每个任务启动 1 个 goroutine,通过 gpool 控制并发数(-p 参数)。
生命周期
启动 → 查询数据库 → 更新状态为 Running → 创建 DRMAA 会话 → 提交任务 →
进入监控循环(每5秒检查) → 任务完成 → 更新状态 → 退出
阻塞点分析
数据库操作(短暂阻塞)
- 同 Local 模式,通过
write_pool串行化
- 同 Local 模式,通过
DRMAA 操作(短暂阻塞)
MakeSession(): 创建 DRMAA 会话(< 100ms)AllocateJobTemplate(): 分配任务模板(< 50ms)RunJob(): 提交任务到 SGE(< 500ms)
监控循环(长时间运行,但会释放线程)
time.Sleep(5 * time.Second): 每 5 秒检查一次任务状态session.JobPs(jobID): 通过 DRMAA 查询 SGE 任务状态(< 100ms)- 文件读取操作:检查
.e错误文件判断内存错误(< 10ms) - 检查
.sign文件判断任务成功(< 10ms) - 支持 context 取消,允许优雅关闭
- 关键: 在 Sleep 期间,goroutine 会释放线程,不占用 CPU
资源消耗总结
- Goroutine 数量: 最多
-t个(并发控制) - 实际线程数: 通常远少于 goroutine 数(Go 运行时复用)
- 内存消耗: 每个 goroutine 约 2KB 栈空间 + DRMAA 会话对象(约 1KB)
- CPU 消耗: 非常低(主要是每 5 秒的状态检查)
- 网络消耗: 每 5 秒一次 DRMAA 查询,网络开销很小
示例计算
假设 -t 10,有 100 个任务:
- 同时运行的 goroutine: 最多 10 个
- 实际线程数: 通常 2-4 个
- 总 goroutine 数: 100 个(但只有 10 个同时运行)
- 内存: 约 10 × 3KB = 30KB(活跃 goroutine)+ 监控 goroutine ≈ 35KB
- CPU: 每 5 秒 10 次状态查询,CPU 消耗 < 1%
对比分析
Local 模式 vs QsubSge 模式
| 项目 | Local 模式 | QsubSge 模式 |
|---|---|---|
| Goroutine 数量 | 最多 -t 个 | 最多 -t 个 |
| 实际线程数 | 2-4 个(Go 运行时管理) | 2-4 个(Go 运行时管理) |
| 主要阻塞点 | cmd.Wait() 等待子进程 | time.Sleep(5s) 监控循环 |
| 线程占用 | 等待期间会释放线程 | Sleep 期间会释放线程 |
| 内存消耗 | ~25KB(活跃) | ~35KB(活跃) |
| CPU 消耗 | 低(主要在子进程) | 极低(每 5 秒检查) |
| 资源竞争 | 子进程资源竞争 | 无(任务在 SGE 节点执行) |
关键差异
Local 模式:
- 子进程在本地执行,占用本地 CPU/内存
- 主程序线程消耗低,但子进程会消耗系统资源
- 适合:任务轻量,本地资源充足
QsubSge 模式:
- 任务提交到 SGE,在集群节点执行
- 主程序只负责提交和监控,资源消耗极低
- 监控循环每 5 秒检查一次,CPU 消耗可忽略
- 适合:任务重量,需要集群资源
优化建议
1. 并发数设置(-t 参数)
Local 模式:
- 建议设置为 CPU 核心数的 1-2 倍
- 如果任务主要是 I/O 密集型,可以设置更高
- 如果任务主要是 CPU 密集型,建议等于 CPU 核心数
QsubSge 模式:
- 可以设置较高(如 50-100),因为只是提交和监控
- 实际执行在 SGE 节点,不受本地资源限制
- 建议根据网络和 DRMAA 库性能调整
2. 监控间隔
当前 QsubSge 模式每 5 秒检查一次,这是合理的平衡:
- 太频繁:增加数据库和 DRMAA 查询压力
- 太慢:状态更新延迟
3. 数据库写入优化
使用 write_pool(大小为1)串行化数据库写入,避免并发冲突:
- 优点:数据一致性保证
- 缺点:可能成为瓶颈(但通常数据库操作很快,影响不大)
实际测试建议
可以通过以下方式监控线程消耗:
# 查看进程线程数
ps -eLf | grep annotask | wc -l
# 查看 goroutine 数量(需要添加调试代码)
# 或使用 pprof 工具
go tool pprof http://localhost:6060/debug/pprof/goroutine
总结
两种模式的线程消耗都很低:
- Goroutine 数量: 受
-p参数控制,通常 10-100 个 - 实际线程数: Go 运行时管理,通常 2-4 个
- 内存消耗: 几十 KB 到几百 KB
- CPU 消耗: 极低(Local 模式主要在子进程,QsubSge 模式主要在监控)
主要资源消耗在任务执行本身,而不是主程序的线程管理。