
heroku 的 dyno 会定期重启或强制终止,无法保证长时 go 协程持续运行;正确方案是使用专用 worker dyno + 信号感知 + 可重入任务设计,而非依赖“永不中断”的环境。
heroku 的 dyno 会定期重启或强制终止,无法保证长时 go 协程持续运行;正确方案是使用专用 worker dyno + 信号感知 + 可重入任务设计,而非依赖“永不中断”的环境。
在 Heroku 上运行长达 7 分钟、不可中断的 Go 后台任务时,切勿直接在 web dyno 中启动 goroutine——这不仅违反 Heroku 架构规范(web dyno 应专注处理 HTTP 请求),更会导致任务被无预警终止(如 dyno 重启、扩展缩容、SIGKILL 强杀)。真正的解决方案是:将耗时任务解耦为独立的 worker dyno 进程,并使其具备容错与恢复能力。
✅ 正确架构:Worker Dyno + 消息队列 + 可重入逻辑
Heroku 原生支持多进程类型,你只需在 Procfile 中定义 worker 进程:
web: ./myapp-server worker: ./myapp-worker
然后部署一个专用于执行后台任务的 Go 程序(myapp-worker),它不响应 HTTP,只监听任务队列(推荐 Redis + github.com/hibiken/asynq 或 github.com/go-redis/redis/v9 手动实现):
// myapp-worker/main.go
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/hibiken/asynq"
)
func main() {
r := asynq.RedisClientOpt{
Addr: "your-redis-url:6379",
}
srv := asynq.NewServer(r, asynq.Config{
Concurrency: 5,
})
mux := asynq.NewServeMux()
mux.HandleFunc("process:video", handleVideoProcessing)
// 监听 SIGTERM(Heroku 关机信号)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM)
go func() {
<-sigChan
log.Println("Received SIGTERM — shutting down gracefully...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Shutdown error: %v", err)
}
os.Exit(0)
}()
log.Println("Worker started")
if err := srv.Run(mux); err != nil {
log.Fatalf("Failed to start worker: %v", err)
}
}
func handleVideoProcessing(ctx context.Context, t *asynq.Task) error {
// ✅ 关键:任务必须幂等或可断点续传
// 示例:从 Redis Hash 中读取进度,跳过已处理步骤
taskID := t.Payload()["task_id"].(string)
if isCompleted(taskID) {
return nil
}
// 执行分段逻辑(如:下载 → 转码 → 上传 → 标记完成)
if err := downloadPart(ctx, taskID); err != nil {
return err // 自动重试(asynq 默认 3 次)
}
if err := transcodePart(ctx, taskID); err != nil {
return err
}
markCompleted(taskID)
return nil
}⚠️ 重要原则(不可妥协)
- 绝不假设运行环境永不停机:即使迁移到 AWS EC2 或物理服务器,硬件故障、系统更新、网络中断仍会发生。
-
所有任务必须可重入(idempotent)或支持断点续传:通过 Redis 存储中间状态(如
HSET task:123 status "transcoding"),避免重复执行或数据不一致。 - 显式处理 SIGTERM:Heroku 在终止前仅给予最多 30 秒清理时间,务必在此期间保存进度、拒绝新任务、优雅退出。
-
禁用长连接阻塞:避免
time.Sleep(7 * time.Minute)类型硬等待,改用带超时的 I/O 操作和心跳检查。
? 补充建议:增强健壮性
- 使用
asynq的Retry-After和失败回调自动重试瞬时错误; - 对关键任务添加数据库事务或 Redis Lua 脚本保证原子性;
- 通过 Heroku Scheduler(定时触发)或外部服务(如 Cronitor)触发任务,而非由 Web 请求直接
go f()启动; - 日志统一推送至 Papertrail 或 Datadog,便于追踪任务生命周期。
归根结底,Go 的 goroutine 是并发原语,不是分布式任务抽象。在云平台中保障长任务可靠性,靠的是架构设计(worker 进程隔离)+ 协议约定(信号处理)+ 业务建模(幂等/状态持久化),而非试图“把 goroutine 跑到后台去”。

















