线程/goroutine 消耗分析

线程/goroutine 消耗分析

概述

程序使用 goroutine(Go 的轻量级线程)来并发执行任务。goroutine 由 Go 运行时管理,通常一个 goroutine 对应一个操作系统线程,但在 I/O 阻塞时会自动让出线程给其他 goroutine。

固定消耗

1. 主 goroutine

  • 数量: 1
  • 作用: 主程序流程,协调所有任务
  • 生命周期: 程序运行期间

2. 监控 goroutine (MonitorTaskStatus)

  • 数量: 1
  • 作用: 实时监控数据库,输出任务状态变化到标准输出
  • 生命周期: 从程序启动到所有任务完成(通过 context 控制)
  • 资源消耗:
    • 每秒查询一次本地数据库(使用 time.NewTicker(1 * time.Second)
    • 每秒更新一次全局数据库(如果配置了全局数据库)
    • 内存:维护 lastStatus map(每个任务约 100-200 字节)
    • 输出格式:表格格式,包含 try task status retry taskid exitcode time
    • 不显示 Pending 状态的任务

Local 模式线程消耗

任务执行 goroutine

每个任务启动 1 个 goroutine,通过 gpool 控制并发数(-p 参数)。

生命周期

启动 → 查询数据库 → 更新状态为 Running → 启动子进程 → 等待子进程完成 → 更新状态 → 退出

阻塞点分析

  1. 数据库操作(短暂阻塞)

    • QueryRow: 查询任务信息(< 10ms)
    • Exec: 更新状态(< 10ms)
    • 通过 write_pool(大小为1)串行化,避免并发冲突
  2. 进程启动(短暂阻塞)

    • cmd.Start(): 启动子进程(< 100ms)
  3. 进程等待(长时间阻塞,但会释放线程)

    • 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秒检查) → 任务完成 → 更新状态 → 退出

阻塞点分析

  1. 数据库操作(短暂阻塞)

    • 同 Local 模式,通过 write_pool 串行化
  2. DRMAA 操作(短暂阻塞)

    • MakeSession(): 创建 DRMAA 会话(< 100ms)
    • AllocateJobTemplate(): 分配任务模板(< 50ms)
    • RunJob(): 提交任务到 SGE(< 500ms)
  3. 监控循环(长时间运行,但会释放线程)

    • 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 节点执行)

关键差异

  1. Local 模式:

    • 子进程在本地执行,占用本地 CPU/内存
    • 主程序线程消耗低,但子进程会消耗系统资源
    • 适合:任务轻量,本地资源充足
  2. 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 模式主要在监控)

主要资源消耗在任务执行本身,而不是主程序的线程管理。

下一步

Last Updated 12/15/2025, 8:44:01 AM