go开发定时任务并告警

445 阅读7分钟

介绍

在开发go语言项目时,对一些容易变化的资源进行一系列的定时监控。设置一系列的前置条件,当条件满足则触发告警请求。

使用场景:

  1. log日志存储文件中的日志信息过多,当到达500M时,进行报警操作。
  2. redis缓存键值对过多,进行报警操作。

对定时任务的使用,是根据其业务逻辑来制定的。

逻辑图:

image.png

第三方依赖包

github.com/robfig/cron…

go get github.com/robfig/cron/v3@v3.0.0

robfig/cron/v3是一个基于go语言的第三方库,用于在应用程序中实现定时任务的调度和管理。允许按照预定的时间间隔或时间点自动执行指定的任务。可以自定义定时任务并设置其执行规则。比如,每天的特定时间、每周的某一天。该库还可以支持定时任务的并发执行。

/*创建了一个cron的调度器*/
var Client = cron.New()

func Test01() {
   //每隔1秒,执行func(){}
   _, err := Client.AddFunc("@every 1s", func() {
      fmt.Println("execing", time.Now().Format("2006-01-02 15:04:05"))
   })
   if err != nil {
      fmt.Println(err)
   }
   //调度器开始
   Client.Start()
   //睡眠5分钟
   time.Sleep(5 * time.Minute)
   //调度器结束
   Client.Stop()
}

execing 2023-07-08 09:58:12
execing 2023-07-08 09:58:13
execing 2023-07-08 09:58:14
execing 2023-07-08 09:58:15
execing 2023-07-08 09:58:16
execing 2023-07-08 09:58:17
execing 2023-07-08 09:58:18

在上述代码中,创建了一个调度器client,在其方法AddFunc("间隔时间",执行函数)实现了定时任务的执行。这只是一个简单的demo。接下来分析其源码:

var Client = cron.New()

func New(opts ...Option) *Cron {
   c := &Cron{
      entries:   nil,
      chain:     NewChain(),
      add:       make(chan *Entry),
      stop:      make(chan struct{}),
      snapshot:  make(chan chan []Entry),
      remove:    make(chan EntryID),
      running:   false,
      runningMu: sync.Mutex{},
      logger:    DefaultLogger,
      location:  time.Local,
      parser:    standardParser,
   }
   for _, opt := range opts {
      opt(c)
   }
   return c
}

解释:在创建调度器时,可以传入参数,自定义调度器。可以对调度器做以下配置:
1.设置时间解析器
2.设置日志记录器
3.设置时区
4.设置并发执行数

一般来说,没有人想去自定义定时任务的调度器。因为默认的调度器就很完美了,几乎可以按满足需求。项目中使用调度器也就做个定时任务而已。如果有自定义配置调度器的需要,请查阅文档。

Client.AddFunc("@every 1s", func() {
      fmt.Println("execing", time.Now().Format("2006-01-02 15:04:05"))
})
   
func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) {
   return c.AddJob(spec, FuncJob(cmd))
}

可以看出client.AddFunc()接收两个参数,一个时间表达式、一个执行任务的函数。关于cron包的时间表达式我放在下一主题讲解。在AddFunc()源码中,其底层也是实现了AddJob()的方法。对AddFunc()只需要了解如下信息:

  • AddFunc()可以被同一个client多次调用,执行多个定时任务
  • 在client.start()时,才开始进行任务
  • AddFunc()内的任务是并发的,是不会相互影响的。即使在上一个任务还未执行完毕,下一个任务在时间到了后也会按照预期的时间策略触发
c.Start()

// Start the cron scheduler in its own goroutine, or no-op if already started.
func (c *Cron) Start() {
   c.runningMu.Lock()
   defer c.runningMu.Unlock()
   if c.running {
      return
   }
   c.running = true
   go c.run()
}
  • 当你想要执行已添加到Cron对象的若干个定时任务时,即可调用start()
  • 在调用start()方法后,cron对象会按照预设的时间策略来自动触发定时任务
  • Cron对象会启动一个后台的调度器协程own goroutine,其负责计算和判断任务触发的时间,并执行任务
  • 在调用 c.Start() 方法之前,必须先向 Cron 对象中添加任务,否则调度器无法执行任何任务。
  • 一旦调用了 Start() 方法,任务调度器将不会停止,直到显式地调用了 c.Stop() 方法或程序结束。
  • 如果在任务调度期间有新的任务添加进来,调度器会即时生效,无需重新启动。
  • 如果任务执行时间过长而导致错过了下一次预定的触发时间,调度器会等待任务执行完毕后立即开始下一次触发。
 Client.Stop()
 解释:停止调度器运行

cron包的时间表达式

在cron包的时间策略两个方面:一是在指定时间点触发一次任务,二是每隔多少时间触发一次任务。

这是特定时间点的常用写法:一个 cron 表达式是一个由 5 个空格分隔的字符串,每一部分从左到右分别表示 分, 时, 天,月, 星期,每个部分由数字和一些特殊字符表示一个约定的时间项,在 robfig/cron 中,每一部分允许的特殊字符为*。

Field name   | Mandatory? | Allowed values  | Allowed special characters
----------   | ---------- | --------------  | --------------------------
Minutes      | Yes        | 0-59            | * / , -
Hours        | Yes        | 0-23            | * / , -
Day of month | Yes        | 1-31            | * / , - ?
Month        | Yes        | 1-12 or JAN-DEC | * / , -
Day of week  | Yes        | 0-6 or SUN-SAT  | * / , - ?
"30 * * * *" 表示在*:*:*:*:30分钟执行一次任务,即只要分钟数达到30时,即执行。说白了就是每隔30
"31 5 8 6 *" 表示在每一年的685310秒时,执行一次任务

相信大家已经在心里默默的说了句MMP,是的,这就是个反人类的设计。看着很不舒服。接下来说的是时间间隔的方式,也是最常用的时间策略。

@every 10m 表示每隔10分钟执行一次任务
@every 1s 表示每隔1秒执行一次任务

这个时间间隔,是以当前时间为基准的。

对比上述两种时间策略。总的来说各有千秋吧。如果定时任务执行的时间期限长,比如,一年只执行一次。那可以选择第一种时间策略。如果是每天都要测一次,那就用第二种。

实战

在官方文档里,还有很多的函数讲解。但是一般开发也用不到很多。我这里就以实战来讲解了。

package timer

import (
   "sync"

   "github.com/robfig/cron/v3"
)
//定义了一个接口,下列是实现这些接口的方法
type Timer interface {
   AddTaskByFunc(taskName string, spec string, task func(), option ...cron.Option) (cron.EntryID, error)
   AddTaskByJob(taskName string, spec string, job interface{ Run() }, option ...cron.Option) (cron.EntryID, error)
   FindCron(taskName string) (*cron.Cron, bool)
   StartTask(taskName string)
   StopTask(taskName string)
   Remove(taskName string, id int)
   Clear(taskName string)
   Close()
}

// timer 定时任务管理【taskLis内部存入调度器和任务名称】
type timer struct {
   taskList map[string]*cron.Cron
   sync.Mutex
}

// AddTaskByFunc 通过函数的方法添加任务,自定义封装AddFunc() 加锁 有start() 激活状态
func (t *timer) AddTaskByFunc(taskName string, spec string, task func(), option ...cron.Option) (cron.EntryID, error) {
   t.Lock()
   defer t.Unlock()
   if _, ok := t.taskList[taskName]; !ok {
      t.taskList[taskName] = cron.New(option...)
   }
   id, err := t.taskList[taskName].AddFunc(spec, task)
   t.taskList[taskName].Start()
   return id, err
}

// AddTaskByJob 通过接口的方法添加任务,与上述的AddTaskByFunc()是一样的,都实现了AddJob()
func (t *timer) AddTaskByJob(taskName string, spec string, job interface{ Run() }, option ...cron.Option) (cron.EntryID, error) {
   t.Lock()
   defer t.Unlock()
   if _, ok := t.taskList[taskName]; !ok {
      t.taskList[taskName] = cron.New(option...)
   }
   id, err := t.taskList[taskName].AddJob(spec, job)
   t.taskList[taskName].Start()
   return id, err
}

// FindCron 获取对应taskName的cron 可能会为空
func (t *timer) FindCron(taskName string) (*cron.Cron, bool) {
   t.Lock()
   defer t.Unlock()
   v, ok := t.taskList[taskName]
   return v, ok
}

// StartTask 开始任务
func (t *timer) StartTask(taskName string) {
   t.Lock()
   defer t.Unlock()
   if v, ok := t.taskList[taskName]; ok {
      v.Start()
   }
}

// StopTask 停止任务
func (t *timer) StopTask(taskName string) {
   t.Lock()
   defer t.Unlock()
   if v, ok := t.taskList[taskName]; ok {
      v.Stop()
   }
}

// Remove 从taskName 删除指定任务
func (t *timer) Remove(taskName string, id int) {
   t.Lock()
   defer t.Unlock()
   if v, ok := t.taskList[taskName]; ok {
      v.Remove(cron.EntryID(id))
   }
}

// Clear 清除任务
func (t *timer) Clear(taskName string) {
   t.Lock()
   defer t.Unlock()
   if v, ok := t.taskList[taskName]; ok {
      v.Stop()
      delete(t.taskList, taskName)
   }
}

// Close 释放资源
func (t *timer) Close() {
   t.Lock()
   defer t.Unlock()
   for _, v := range t.taskList {
      v.Stop()
   }
}

func NewTimerTask() Timer {
   return &timer{taskList: make(map[string]*cron.Cron)}
}

上述代码是封装了cron的原始函数,也就是加了一个锁。

func hander() {
   // 获取Timer接口
   t := timer.NewTimerTask()
   // 开始设置定时任务,默认为激活状态
   _, err := t.AddTaskByFunc("taskName", "@every 10m", func() {
       //此处可以设计业务逻辑并报警
   }
 }

报警

常见的报警有很多种:发送电子邮件、微信通知、QQ通知、打电话、企业微信机器人等

我最喜欢用的即使企业微信机器人,因为简单。

type Message struct {
   MsgType string `json:"msgtype"`
   Text    struct {
      Content string `json:"content"`
   } `json:"text"`
}

func Test09() {
   webhookURL := "你自己的企业微信机器人的webhookURL"

   message := Message{
      MsgType: "text",
      Text: struct {
         Content string `json:"content"`
      }{
         Content: "text",
      },
   }

   data, err := json.Marshal(message)
   if err != nil {
      fmt.Println("JSON marshal error:", err)
      return
   }

   req, err := http.NewRequest("POST", webhookURL, bytes.NewBuffer(data))
   if err != nil {
      fmt.Println("HTTP request error:", err)
      return
   }
   req.Header.Set("Content-Type", "application/json")

   client := &http.Client{}
   resp, err := client.Do(req)
   if err != nil {
      fmt.Println("HTTP request error:", err)
      return
   }
   defer resp.Body.Close()

   if resp.StatusCode == http.StatusOK {
      fmt.Println("HTTP request success")
   } else {
      fmt.Println("HTTP request failed")
   }
}

总结

没得总结,嘿嘿···