Asynq 高性能分布式任务队列实践

作者: adm 分类: go 发布时间: 2022-05-06

Asynq 是一个 Go 库,用于对任务进行排队并与工作人员异步处理它们。它由Redis提供支持,旨在可扩展且易于上手。开发自谷歌员工。

Asynq 工作原理的高级概述:

客户端将任务放入队列
服务器从队列中拉出任务并为每个任务启动一个工作 goroutine
多个工作人员同时处理任务
任务队列用作跨多台机器分配工作的机制。一个系统可以由多个工作服务器和代理组成,让位于高可用性和水平扩展。

稳定性和兼容性
状态:该库目前正在进行大量开发,API 更改频繁且中断。

☝️ 重要提示:当前主要版本为零 ( v0.x.x),以适应快速开发和快速迭代,同时获得用户的早期反馈(感谢 API 的反馈!)。公共 API 可能会在发布前没有主要版本更新的情况下发生变化v1.0.0。

快速开始
首先,确保你在本地运行Redis服务器。

$ redis-server

安装Asynq库

go get -u github.com/hibiken/asynq
go get -u github.com/hibiken/asynq/tools/asynqmon

创建项目

mkdir ziji && cd ziji
go mod init ziji
mkdir tasks
touch tasks/beta.go  tasks/worker.go  tasks/task.go


Redis 连接选项
Asynq 使用 Redis 作为消息代理, beta.go和worker.go都需要连接到 Redis 进行写入和读取。
我们将使用 RedisClientOpt 指定如何连接到本地 Redis 实例。

beta.go

package tasks

import (
   "github.com/hibiken/asynq"
   "log"
   "pigs/common"
   "pigs/models/cmdb"
)

func TaskBeta() {
   c := common.CONFIG.Redis
   // 周期性任务
   scheduler := asynq.NewScheduler(
      asynq.RedisClientOpt{
         Addr:     c.Host,
         Username: c.UserName,
         Password: c.PassWord,
         DB:       c.DB,
      }, nil)

   var account cmdb.CloudPlatform
   common.DB.Table("cloud_platform").Where("enable != ? and type = ?", 0, "aliyun").Find(&account)
   syncResource := NewAliCloudTask(&account)
   // 每隔5分钟同步一次
   entryID, err := scheduler.Register("*/5 * * * *", syncResource)

   if err != nil {
      log.Fatal(err)
   }
   log.Printf("registered an entry: %q\n", entryID)

   if err := scheduler.Run(); err != nil {
      log.Fatal(err)
   }
}

NewScheduler
将运行调度程序,用于定期处理任务。调度器定期对任务排队,然后由集群中可用的工作服务器执行。

时区
默认情况下,定期任务计划使用UTC时区,更改默认时区可以使用SchedulerOpts参数

scheduler.Register` 接受三个参数,cron时间、任务、队列名`asynq.Queue("cloud")
// Example of using America/Los_Angeles timezone instead of the default UTC timezone.
loc, err := time.LoadLocation("America/Los_Angeles")
if err != nil {
    panic(err)
}
scheduler := asynq.NewScheduler(
    redisConnOpt, 
    &asynq.SchedulerOpts{
        Location: loc,
    },
)

task.go

package tasks

import (
   "context"
   "encoding/json"
   "github.com/hibiken/asynq"
   "log"
   "pigs/inner/cloud/cloudsync"
   "pigs/inner/cloud/cloudvendor"
   "pigs/models/cmdb"
)

const (
   SyncAliYunCloud  = "cmdb:aliyun"
   SyncTencentCloud = "cmdb:tencent"
)

// NewAliCloudTask 同步阿里云资产同步任务
func NewAliCloudTask(conf *cmdb.CloudPlatform) *asynq.Task {
   payload, err := json.Marshal(conf)
   if err != nil {
      panic(err)
   }
   return asynq.NewTask(SyncAliYunCloud, payload)
}

func HandleAliCloudTask(ctx context.Context, t *asynq.Task) error {

   var a cmdb.CloudPlatform
   if err := json.Unmarshal(t.Payload(), &a); err != nil {
      return err
   }

   _, err := cloudvendor.GetVendorClient(&a)
   if err != nil {
      log.Fatalf("AccountVerify GetVendorClient failed,%v", err)
      return err
   }

   cloudsync.SyncAliYunHost(&a)

   log.Printf("Aliyun Cloud assets are successfully synchronized...")
   return nil
}

Tasks 任务
在 asynq 中,工作单元被封装为 Task 类型。
其中有两个字段:“类型” 和 “有效载荷”。

// Task represents a task to be performed.
type Task struct {
    // Type indicates the type of a task to be performed.
    Type string

    // Payload holds data needed to perform the task.
    Payload Payload
}

Type 是一个简单的字符串值,指示给定任务的类型。
Payload 保存执行任务所需的数据,您可以将其视为 map[string]interface{}。需要注意的重要一件事是有效负载值必须是可序列化的。

worker.go

package tasks

import (
   "context"
   "pigs/common"
   "time"

   "log"
   "os"
   "os/signal"

   "github.com/hibiken/asynq"
   "golang.org/x/sys/unix"
)

// loggingMiddleware 记录任务日志中间件
func loggingMiddleware(h asynq.Handler) asynq.Handler {
   return asynq.HandlerFunc(func(ctx context.Context, t *asynq.Task) error {
      start := time.Now()
      log.Printf("Start processing %q", t.Type())
      err := h.ProcessTask(ctx, t)
      if err != nil {
         return err
      }
      log.Printf("Finished processing %q: Elapsed Time = %v", t.Type(), time.Since(start))
      return nil
   })
}

func TaskWorker() {
   c := common.CONFIG.Redis
   srv := asynq.NewServer(
      asynq.RedisClientOpt{
         Addr:     c.Host,
         Username: c.UserName,
         Password: c.PassWord,
         DB:       c.DB,
      },
      asynq.Config{Concurrency: 20},
   )

   mux := asynq.NewServeMux()
   mux.Use(loggingMiddleware)
   // 任务执行时的handle
   mux.HandleFunc(SyncAliYunCloud, HandleAliCloudTask)

   // start server
   if err := srv.Start(mux); err != nil {
      log.Fatalf("could not start server: %v", err)
   }

   // Wait for termination signal.
   sigs := make(chan os.Signal, 1)
   signal.Notify(sigs, unix.SIGTERM, unix.SIGINT, unix.SIGTSTP)
   for {
      s := <-sigs
      if s == unix.SIGTSTP {
         srv.Shutdown()
         continue
      }
      break
   }

   // Stop worker server.
   srv.Stop()
}

示例
建立任务使用NewTask方法,并为任务传递类型和有效负载。 可以通过Client.Schedule传入任务和需要处理的时间来计划任务

func main() {
  client := asynq.NewClient(redis)
  
  // 创建任务,声明任务类型,有效负载
  t1 := asynq.NewTask("send_register_email", map[string]interface{}{"userName": "zhangsan"})
  t2 := asynq.NewTask("send_forget_email", map[string]interface{}{"userName": "zhangsan"})

  // 立即处理任务
  err := client.Enqueue(t1, time.Now())
  if err != nil {
    log.Fatal(err)
  }

  // 2小时后处理任务, 延迟任务
  err := client.Enqueue(t2, asynq.ProcessIn(time.Now().Add(2 * time.Hour)))
  if err != nil {
    log.Fatal(err)
  }
}

asynq.Client 支持三种调度任务的方法:Enqueue,EnqueueIn 和 EnqueueAt。
使用 client.Enqueue 将任务立即加入队列。
使用 client.EnqueueIn 或 client.EnqueueAt 来安排将来要处理的任务。 EnqueueAt支持 2021-11-11 15:10:00 时间格式定时执行任务

// 定时任务
package tasks

import (
   "fmt"
   "github.com/hibiken/asynq"
   "log"
   "pigs/common"
   "pigs/models/cmdb"
   "time"
)

type EmailTaskPayloadTest struct {
	UserID  int64
	Msg		string
}

func TaskBeta() {
   client := asynq.NewClient(
      asynq.RedisClientOpt{
         Addr:     ":6379",
         Password: "",
      })
   
   payload, err := json.Marshal(EmailTaskPayloadTest{
      UserID: 100,
      Msg:    "test",
   })
   if err != nil {
      log.Fatal(err)
   }
   t1 := asynq.NewTask("task:oneTask", payload)
   
   // 定时执行任务时间
   setDate := "2021-11-11 15:10:00"
   dateFormats := "2006-01-02 15:04:05"

   // 获取时区
   loc, _ := time.LoadLocation("Local")

   // 指定日期 转 当地 日期对象 类型为 time.Time
   timeObj, err := time.ParseInLocation(dateFormats, setDate, loc)
   if err != nil {
      fmt.Println("parse time failed err :", err)
      return
   }

   info, err := client.Enqueue(t1, asynq.ProcessAt(timeObj), asynq.Queue("test"))
   if err != nil {
      log.Fatal(err)
   }
   
   log.Printf(" [*] Successfully enqueued task: %+v", info)

启动任务

go run beta.go
go run worker.go

asynq: pid=1274467 2021/11/17 04:27:34.960443 INFO: Starting processing

2021/11/17 12:27:34 /home/risk/code/pigs/tasks/beat.go:25
2021/11/17 12:27:34 registered an entry: “fe209263-1561-4b94-8b72-98b29bab6efe”
asynq: pid=1274467 2021/11/17 04:27:34.962365 INFO: Scheduler starting
asynq: pid=1274467 2021/11/17 04:27:34.962371 INFO: Scheduler timezone is set to UTC
asynq: pid=1274467 2021/11/17 04:27:34.962379 INFO: Send signal TERM or INT to stop the scheduler

2021/11/17 12:45:13 Aliyun Cloud assets are successfully synchronized…
2021/11/17 12:45:13 Finished processing “cmdb:aliyun”: Elapsed Time = 12.471785932s

Web UI
Asynqmon是一个基于web的工具,用于监视和管理Asynq队列和任务。有关详细信息,请参阅工具的README。


如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!