简单、可靠、高效的Go分布式任务队列

1,922 阅读5分钟

简单、可靠、高效的Go分布式任务队列

GoDocGo Report CardBuild StatusLicense: MITGitter chat

Asynq是一个Go库,用于排队任务并通过工作者异步处理这些任务。它以Redis为后盾,设计成可扩展且容易上手。

Asynq的工作原理的高级概述:

  • 客户端将任务放在队列中
  • 服务器从队列中提取任务,并为每个任务启动一个worker goroutine
  • 任务由多个工作者同时处理

任务队列被用作一种在多台机器上分配工作的机制。一个系统可以由多个工作者服务器和经纪商组成,为高可用性和横向扩展让路。

用例

Task Queue Diagram

特点

稳定性和兼容性

状态:该库目前正在进行大量的开发,频繁地对API进行修改。

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

快速入门

请确保你已经安装了Go(下载)。需要1.14 或更高的版本。

通过创建一个文件夹,然后在该文件夹内运行go mod init github.com/your/repo (了解更多)来初始化你的项目。然后用以下命令安装Asynq库 go get命令安装Asynq库。

go get -u github.com/hibiken/asynq

确保你在本地或从Docker容器中运行一个Redis服务器。需要4.0 或更高版本。

接下来,编写一个封装任务创建和任务处理的包。

package tasks

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "time"
    "github.com/hibiken/asynq"
)

// A list of task types.
const (
    TypeEmailDelivery   = "email:deliver"
    TypeImageResize     = "image:resize"
)

type EmailDeliveryPayload struct {
    UserID     int
    TemplateID string
}

type ImageResizePayload struct {
    SourceURL string
}

//----------------------------------------------
// Write a function NewXXXTask to create a task.
// A task consists of a type and a payload.
//----------------------------------------------

func NewEmailDeliveryTask(userID int, tmplID string) (*asynq.Task, error) {
    payload, err := json.Marshal(EmailDeliveryPayload{UserID: userID, TemplateID: tmplID})
    if err != nil {
        return nil, err
    }
    return asynq.NewTask(TypeEmailDelivery, payload), nil
}

func NewImageResizeTask(src string) (*asynq.Task, error) {
    payload, err := json.Marshal(ImageResizePayload{SourceURL: src})
    if err != nil {
        return nil, err
    }
    // task options can be passed to NewTask, which can be overridden at enqueue time.
    return asynq.NewTask(TypeImageResize, payload, asynq.MaxRetry(5), asynq.Timeout(20 * time.Minute)), nil
}

//---------------------------------------------------------------
// Write a function HandleXXXTask to handle the input task.
// Note that it satisfies the asynq.HandlerFunc interface.
//
// Handler doesn't need to be a function. You can define a type
// that satisfies asynq.Handler interface. See examples below.
//---------------------------------------------------------------

func HandleEmailDeliveryTask(ctx context.Context, t *asynq.Task) error {
    var p EmailDeliveryPayload
    if err := json.Unmarshal(t.Payload(), &p); err != nil {
        return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
    }
    log.Printf("Sending Email to User: user_id=%d, template_id=%s", p.UserID, p.TemplateID)
    // Email delivery code ...
    return nil
}

// ImageProcessor implements asynq.Handler interface.
type ImageProcessor struct {
    // ... fields for struct
}

func (processor *ImageProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
    var p ImageResizePayload
    if err := json.Unmarshal(t.Payload(), &p); err != nil {
        return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
    }
    log.Printf("Resizing image: src=%s", p.SourceURL)
    // Image resizing code ...
    return nil
}

func NewImageProcessor() *ImageProcessor {
	return &ImageProcessor{}
}

在你的应用程序代码中,导入上述包并使用 Client来把任务放在队列中。

package main

import (
    "log"
    "time"

    "github.com/hibiken/asynq"
    "your/app/package/tasks"
)

const redisAddr = "127.0.0.1:6379"

func main() {
    client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
    defer client.Close()

    // ------------------------------------------------------
    // Example 1: Enqueue task to be processed immediately.
    //            Use (*Client).Enqueue method.
    // ------------------------------------------------------

    task, err := tasks.NewEmailDeliveryTask(42, "some:template:id")
    if err != nil {
        log.Fatalf("could not create task: %v", err)
    }
    info, err := client.Enqueue(task)
    if err != nil {
        log.Fatalf("could not enqueue task: %v", err)
    }
    log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue)


    // ------------------------------------------------------------
    // Example 2: Schedule task to be processed in the future.
    //            Use ProcessIn or ProcessAt option.
    // ------------------------------------------------------------

    info, err = client.Enqueue(task, asynq.ProcessIn(24*time.Hour))
    if err != nil {
        log.Fatalf("could not schedule task: %v", err)
    }
    log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue)


    // ----------------------------------------------------------------------------
    // Example 3: Set other options to tune task processing behavior.
    //            Options include MaxRetry, Queue, Timeout, Deadline, Unique etc.
    // ----------------------------------------------------------------------------

    task, err = tasks.NewImageResizeTask("https://example.com/myassets/image.jpg")
    if err != nil {
        log.Fatalf("could not create task: %v", err)
    }
    info, err = client.Enqueue(task, asynq.MaxRetry(10), asynq.Timeout(3 * time.Minute))
    if err != nil {
        log.Fatalf("could not enqueue task: %v", err)
    }
    log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue)
}
Next, start a worker server to process these tasks in the background. To start the background workers, use Server and provide your Handler to process the tasks.

You can optionally use ServeMux to create a handler, just as you would with net/http Handler.

package main

import (
    "log"

    "github.com/hibiken/asynq"
    "your/app/package/tasks"
)

const redisAddr = "127.0.0.1:6379"

func main() {
    srv := asynq.NewServer(
        asynq.RedisClientOpt{Addr: redisAddr},
        asynq.Config{
            // Specify how many concurrent workers to use
            Concurrency: 10,
            // Optionally specify multiple queues with different priority.
            Queues: map[string]int{
                "critical": 6,
                "default":  3,
                "low":      1,
            },
            // See the godoc for other configuration options
        },
    )

    // mux maps a type to a handler
    mux := asynq.NewServeMux()
    mux.HandleFunc(tasks.TypeEmailDelivery, tasks.HandleEmailDeliveryTask)
    mux.Handle(tasks.TypeImageResize, tasks.NewImageProcessor())
    // ...register other handlers...

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

接下来,启动一个工人服务器来在后台处理这些任务。要启动后台工作者,使用 Server并提供你的 Handler来处理这些任务。

你可以选择使用 ServeMux来创建一个处理程序,就像你用 net/http处理程序。

package

有关该库的更详细介绍,请参见我们的入门指南。

要了解更多关于asynq 功能和API的信息,请看包godoc

网络用户界面

Asynqmon是一个基于Web的工具,用于监控和管理Asynq队列和任务。

下面是Web UI的几个屏幕截图。

队列视图

Web UI Queues View

任务视图

Web UI TasksView

度量衡视图Screen Shot 2021-12-19 at 4 37 19 PM

设置和自适应黑暗模式

Web UI Settings and adaptive dark mode

关于如何使用该工具的细节,请参考该工具的README

命令行工具

Asynq提供了一个命令行工具来检查队列和任务的状态。

要安装CLI工具,运行以下命令。

go install github.com/hibiken/asynq/tools/asynq

关于如何使用该工具的细节,请参考该工具的README

贡献

我们欢迎并感谢社区的任何贡献(GitHub问题/PR、Gitter频道上的反馈等)。

在贡献之前,请参考贡献指南