文章目录
一、Linux Cron介绍
1.引言
定时任务在日常的开发中使用的频率越来越高,比如电商系统每天进行的数据统计任务,再比如信用卡还款日提醒等等,都可能会使用定时任务来实现。所以如何实现定时任务是我们经常碰到的问题,采用哪种方案,可能影响到系统的架构设计。在系统的不断迭代的过程中,定时任务的实现方式也会一直会跟随着演进。本篇我们介绍定时任务的实现方案之一cron
。
2.cron是什么
cron
是linux
系统中,以后台进程模式周期性地执行命令或指定程序任务的服务软件。默认安装完linux
系统之后,cron
服务软件便会启动,服务对应的进程名字为crond
。cron
服务会定期(默认每分钟检查一次
)检查系统中是否有需要执行的任务工作计划。cron
工具对应的crontab
命令依赖于crond
服务。
3.cron的适用场景
它非常适合定期运行脚本、备份数据、清理临时文件等一系列重复性任务。
4.cron原理
crond
服务工作时会以每分钟为单位查看在/var/spool/cron
路径下以系统用户名命名的定时任务文件,确定是否有需要执行的任务计划。如果有就把任务调到内存中执行,使用crontab
命令编辑的文件最终都会以当前用户命名存在于/var/spool/cron
路径下。
5.cron的使用方法
cron
的使用分为查看定时任务和编辑定时任务两个操作。
5.1查看定时任务
在linux
命令行输入crontab -l
,如果没有配置定时任务则不会显示任何东西。
5.2编辑定时任务
在linux
命令行输入crontab -e
,进入编辑模式,每一行表示一个定时任务,格式是cron
表达式紧跟着一个命令,表示按照cron
表达式的方式执行命令。
cron
表达式共五列,从左到右依次表示分钟、小时、天、月、星期
。每一列都可以采用如下的方式指定。
*
,表示不限制,可以翻译为每
,比如表达式“0 0 * * * ”
表示每天的0点0分(星期不显示,月份不限制,天不限制,限制0点和0分)数字、逗号分隔的数字、连接符表示的范围
,比如表达式“0 1,3,5 2-7 3 *”
表示3
月份2
号到7
号的每一天的凌晨1点0分 、3点0分、 5点0分
分别执行/n
,表示每隔n
执行,比如表达式“/10 * * * * ”
,每隔10
分钟执行
每一列的含义和取值范围如下
示例:
* * * * *
:每分钟运行一次任务。0 1-5 * * *
:每天凌晨的1点到5点的的第0分钟分别运行一次任务。0 0 1 * *
:每月1号的00:00(午夜)运行一次任务。0 0 * * 1
:每周一的00:00运行一次任务。
6.注意事项
- 定时任务后面执行的命令不能有百分号,如果有必须要转义
- 定时任务尽量用脚本,即将命令放入脚本
- 执行定时任务尽量用全路径,避免脚本找不到造成执行失败
- 结尾加上 &>/dev/null或者>/dec/null 2>&1,避免产生输出到控制台
7.问题排查
如果任务没有按照预期执行,可以从以下几个方面排查一下
- 自己手工执行一下命令,看是否是命令存在问题
- 通过日志排查,通过
tail -f /var/log/cron
查看日志 - 权限问题,检查如下两个文件。第一个
/etc/cron.deny
文件中所列用户不允许使用crontab
命令 ;第二个/etc/cron.allow
该文件中所列用户允许使用crontab
命令,优先于/etc/cron.deny
8.优缺点
优点:
- 自动化:
cron
允许用户自动化任务,无需人工干预。 - 资源利用:在不需要运行时,
cron
任务不会使用系统资源。 - 可扩展性:
cron
允许用户创建复杂的执行计划,可以包含多个任务。
缺点:
- 错误配置:如果配置不当,
cron
任务可能不会运行或运行不正确。 - 安全性问题:如果
cron
任务包含敏感信息或对系统产生负面影响的命令,那么错误的cron
设置可能会导致安全问题。 - 调试困难:
cron
任务的故障排除可能比较困难,因为它们通常在后台运行。 - 时间精准度,
cron
表达式只能精确到分钟,如果需求需要精确到秒级则不适合。
二、Go实现简易Cron
在 Go
语言中实现自制的Cron
能力可以通过以下步骤:
1.理解 cron 的功能需求
cron
是一个定时任务调度器,它可以在指定的时间执行特定的任务。主要功能包括:
- 支持设置任务的执行时间规则,如每天特定时间、每周特定日期等。
- 能够管理多个任务,并按照各自的时间规则执行。
- 具有一定的容错性和稳定性,即使在系统出现故障或重启后,也能继续执行任务。
2.选择合适的数据结构
任务结构体
type Task struct {
schedule string
command func()
}
其中 schedule
表示任务的执行时间规则,command
是要执行的任务函数。
存储任务的容器
可以使用切片来存储多个任务。
var tasks []Task
3.解析时间规则
可以使用第三方库或者自己实现一个简单的时间规则解析器。
对于简单的时间规则,如每天特定时间,可以直接解析字符串。
func parseSchedule(schedule string) (hour int, minute int) {
_, err := fmt.Sscanf(schedule, "%d:%d", &hour, &minute)
if err!= nil {
panic(err)
}
return
}
4.启动定时任务调度
使用time
包中的Tick
函数来创建一个定时器,每隔一分钟检查一次是否有任务需要执行。
ticker := time.Tick(time.Minute)
for range ticker {
now := time.Now()
for _, task := range tasks {
hour, minute := parseSchedule(task.schedule)
if now.Hour() == hour && now.Minute() == minute {
task.command()
}
}
}
5.添加任务
提供一个函数来添加任务。
func AddTask(schedule string, command func()) {
tasks = append(tasks, Task{schedule, command})
}
6.以下是一个完整的示例代码:
package main
import (
"fmt"
"time"
)
type Task struct {
schedule string
command func()
}
var tasks []Task
func parseSchedule(schedule string) (hour int, minute int) {
_, err := fmt.Sscanf(schedule, "%d:%d", &hour, &minute)
if err!= nil {
panic(err)
}
return
}
func startCron() {
ticker := time.Tick(time.Minute)
for range ticker {
now := time.Now()
for _, task := range tasks {
hour, minute := parseSchedule(task.schedule)
if now.Hour() == hour && now.Minute() == minute {
task.command()
}
}
}
}
func AddTask(schedule string, command func()) {
tasks = append(tasks, Task{schedule, command})
}
func main() {
AddTask("10:30", func() {
fmt.Println("Task 1 executed at", time.Now())
})
startCron()
}
在这个示例中,我们实现了一个简单的 cron
功能,它可以在指定的时间执行任务。你可以根据实际需求扩展这个功能,比如支持更复杂的时间规则、添加任务管理功能等。
三、如何保证Go语言实现的定时任务的稳定性?
1.错误处理机制
在定时任务执行的函数中,需要有完善的错误处理。例如,如果任务是向数据库写入数据,可能会遇到数据库连接错误、数据格式错误等情况。
注:如下myTask是func()类型,符合【二】中介绍的Task定义,可以作为任务传入
示例:
func myTask() {
// 假设这是一个向数据库写入数据的任务
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/mydb")
if err!= nil {
// 记录错误日志
log.Println("数据库连接错误:", err)
return
}
defer db.Close()
// 执行数据库操作
_, err = db.Exec("INSERT INTO mytable (column1) VALUES (?)", "value")
if err!= nil {
log.Println("数据库写入错误:", err)
}
}
当出现错误时,通过记录错误日志,可以方便后续排查问题。同时,根据错误的严重程度,可以选择是重试任务还是直接跳过。
2.任务重试机制
对于一些可能由于网络波动、资源暂时不可用等原因导致失败的任务,可以设置重试策略。
定义重试次数和重试间隔:
func myTaskWithRetry() {
maxRetries := 3
retryInterval := time.Second * 5
for i := 0; i < maxRetries; i++ {
err := doActualTask()
if err == nil {
return
}
// 记录重试信息
log.Printf("任务执行失败,正在进行第 %d 次重试,错误信息: %v", i + 1, err)
time.Sleep(retryInterval)
}
log.Println("任务经过多次重试后仍然失败")
}
这样,在一定程度上可以提高任务成功执行的概率。
3.资源管理和释放
确保在任务执行过程中正确地管理和释放资源。如果任务涉及文件操作,要及时关闭文件;如果涉及网络连接,要正确地关闭连接。
以文件操作为例:
func fileWriteTask() {
file, err := os.Create("output.txt")
if err!= nil {
log.Println("文件创建错误:", err)
return
}
defer file.Close()
_, err = file.WriteString("这是写入文件的内容")
if err!= nil {
log.Println("文件写入错误:", err)
}
}
正确地使用defer关键字可以确保资源在任务结束后得到释放,避免资源泄漏。
4. 持久化任务状态和进度
如果任务执行时间较长或者可能被中断,可以将任务的状态和进度进行持久化。
例如,对于一个下载任务,可以将已下载的字节数、文件总字节数等信息存储到数据库或者本地文件中。
假设使用数据库来存储下载任务状态:
type DownloadTask struct {
ID int
FileURL string
Progress int
TotalSize int
}
// DownloadTask有个func()类型的方法可以作为定时任务
// 而此处的updateDownloadProgress函数则是使用DB记录对应任务的执行进度
// progress表示已经下载的字节数
func updateDownloadProgress(task *DownloadTask, progress int) {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/mydb")
if err!= nil {
log.Println("数据库连接错误:", err)
return
}
defer db.Close()
// 更新数据库中的下载进度
_, err = db.Exec("UPDATE download_tasks SET progress =? WHERE id =?", progress, task.ID)
if err!= nil {
log.Println("下载进度更新错误:", err)
}
}
这样,即使程序崩溃或者重启,也可以根据之前存储的进度继续执行任务。
5.监控和报警机制
建立监控系统来跟踪定时任务的执行情况。可以通过记录任务的开始时间、结束时间、执行结果等信息来实现。
例如,使用 Prometheus
和 Grafana
来监控定时任务。首先,在任务执行函数中添加指标记录:
import (
"github.com/prometheus/client_golang/prometheus"
)
var taskExecutionDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "task_execution_duration_seconds",
Help: "Duration of task execution in seconds",
},
[]string{"task_name"},
)
func init() {
prometheus.MustRegister(taskExecutionDuration)
}
func myTask() {
start := time.Now()
// 执行任务内容
end := time.Now()
duration := end.Sub(start).Seconds()
taskExecutionDuration.WithLabelValues("my_task").Observe(duration)
}
同时,设置报警规则,当任务执行时间过长、频繁失败等异常情况发生时,及时通知相关人员。可以通过发送邮件、短信或者使用即时通讯工具等方式进行报警。