如何在 Docker 中运行 Cron?
- W_Z_C
- 共 2302 字,阅读约 6 分钟
前几天刚刚在新的平台部署了博客系统,目前使用还比较顺利,不过因为使用了 cloudflare 服务,国内访问速度有点感人。使用的过程中突然想到数据备份的问题,因为博客系统目前核心数据是 Markdown 文档,这些会实时上传到 github,并不太担心,除此之外博客的点赞数据以及其它的动态数据却是个问题,万一哪天平台的云盘坏掉了,我这个免费撸羊毛的用户可能都没地方说理去,再加上我准备接下来添加评论之类的功能,可能对数据库的依赖逐级递增,所以数据库备份这个问题还是要提上进程的。
我想到了一个最简单的方案,就是直接将数据库上传到 github,以后数据大了,可以上传到 Google 云盘之类的地方,问题不大。理论上上传到这些地方应该一个简单的脚本就搞定了,因此我脑海中的第一个想法就是创建一个定时任务,每隔 6 个小时就上传一次。 目前 fly.io 并没有提供类似的服务,所以需要专门创建一个镜象来执行此类任务。
Ofelia 是一个不错的选择,但是我怀疑部署的时候 fly.io 平台是否支持某些参数,我的要求很简单,理论上直接使用 cron 定时运行脚本即可,因此我的想法是直接封装一个专门运行 cron 的镜象,然后部署到云主机即可,但是没想到 cron 在 Docker 中运行会遇到很多问题,查了很多资料,下面是我的一些总结,供大家参考。
cron 是什么?
工具型软件 cron 是一款类 Unix 的操作系统下的基于时间的任务管理系统。用户们可以通过 cron 在固定时间、日期、间隔下,运行定期任务(可以是命令和脚本)。cron常用于运维和管理,但也可用于其他地方,如:定期下载文件和邮件。cron 该词来源于希腊语 chronos(χρόνος),原意是时间。
通常,任务时间表(crontab)文件储存的指令被 crond 守护进程激活,守护进程在后台运行,并每一分钟检查是否有定期的作业需要执行。这类作业一般称为 cron jobs。
上面是维基百科中关于 Cron 的描述,本质上就是支持定时运行某些命令的程序。cron 的 API 接口以及被标准化了,但是该标准有很多不同的实现(dcron、cronie、fcron 以及 vixie-cron 等)。除此之外,有些功能 cron 需要和 anachron 配合使用,例如非 7 x 24 开机的机器,如果关机状态下 cron 是无法运行的,即使等机器开机之后在此期间需要执行的任务请求也会被忽略。而 anachron 程序可以弥补这个潜在的问题。
总而言之,cron 的实现有很多种,它们的功能差异化较大,有些 cron 已经内置了 anachron 的功能,而有些并没有。因此对于不同的版本,cron 的使用方式可能略有不同,特别是在 Docker 环境下。
在不同版本的 Linux 中,Cron 的安装方式也越有差异:
- Debian/Ununtu:
apt-get update && apt-get install -y cron && cron
。 - Centos:
yum install -y cronie && crond -V
。 - Alpine:已经预装,可以使用
which crond
查看。
配置文件
crontab 是 cron table 的简称,是 cron 程序的配置文件。cron 支持在多用户环境下运行,配置文件可能存在几个地方,不同的 Linux 版本可能存在一定的差异:
-
/var/spool/cron/crontabs
目录下对应不同用户的配置文件,该文件一般是由 crontab 命令创建,不建议手动修改。 -
/etc/cron.d
目录下是为了更精确的控制,在编辑的时候需要注意权限。 -
/etc/crontab
是系统级的配置文件。
对于 Docker 容器来说,多用户的作用不大,只需要专注全局的配置文件:/etc/crontab
即可。
下面的配置命令可以每分钟运行一次 date 命令:
# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed
* * * * * root date
前台执行
现在我们已经有了一个 cron 配置文件,正常情况下,cron 被作为守护进程来启动,一般由 service 服务来管理这些后台进程。但是在 Docker 内部,默认只能启动一个程序(其实可以运行 systemd,是另一种解决方案),所以 cron 应该在前台运行。大多数的 cron 程序都支持这一点。
- Debian/Ubuntu:
cron -f
。 - Alpine:
cron -f
。 - Centos:
crond -n
。
环境变量
前面已经提到过,cron 被设计支持多用户,所以 cron 守护进程无法对运行环境做出统一的行为。cron 为此强制每个任务从自定义环境中运行(通常是 /etc/environment
文件)。所以如果你在 cron 运行脚本,可能会导致无法读取到某些环境变量的问题。除此之外,cron 默认的 shell 是 sh,而非 bash。
当在 Docker 中运行 cron 时,环境变量就会非常重要,因为 Docker 最常用的配置方式就是使用环境变量,因此必须确保容器内的环境变量可以传入到 cron 的子进程中。较为合适的方式是使用 Docker 中的 entrypoint 参数,在前台 cron 程序运行之前导入需要的环境变量。
下面是 /entrypoint.sh
的大致代码:
#!/bin/sh
env >> /etc/environment
# start cron in the foreground (replacing the current process)
exec "cron -f"
Centos 系统中 cronie 并不是从
/etc/environment
中读取环境变量的。你需要在脚本运行之前手动获取,例如:* * * * * root . /etc/environment; date
如果 crontab 配置中存在多个任务,你可以改变默认的 shell,并加载指定的环境变量:
SHELL=/bin/bash BASH_ENV=/etc/environment * * * * * root echo "${CUSTOM_ENV_VAR}"
STDOUT/STDERR
如果你运行第二小节中的例子,可能会发现终端上没有显示 date 命令的输出信息,这是因为即使 cron 运行在前台,其子进程(任务进程)的输出也被重定向到日志文件(一般在 /var/log/cron
中)。这个在主机运行的时候没有什么问题,但是在 Docker 中运行显然不是最佳方案,因为你无法在 Docker 日志中直接查看 cron 的输出。
解决这个问题的方法有很多,stackoverflow 上有一个 方案,直接在 Dockerfile 中输出 cron 日志:
# Run the command on container startup
CMD cron && tail -f /var/log/cron.log
我更偏向于文章中另一种,直接将日志重定向:
# >/proc/1/fd/1 redirects STDOUT from the `date` command to PID1's STDOUT
# 2>/proc/1/fd/2 redirects STDERR from the `date` command to PID1's STDERR
* * * * * root date >/proc/1/fd/1 2>/proc/1/fd/2
因为 Docker 内部 cron 进程号为 1,因此你可以在 Docker 日志中看到所有 cron 进程的输出信息。
Alpine 中无需这样做,只需要直接运行:
crond -f -l 2
。
Kill
在测试的时候,你可能会发现运行 cron 的容器很难被关掉,普通的容器直接使用 Ctrl + C
或 docker stop
就可以关停,但是 cron 容器你可能必须使用 docker kill
才可以。
这是因为 cron 在前台运行的时候好像并不能及时正确的处理 SIGINT 信号。目前还没有特别好的简易方案,先忍忍吧!
Centos 中可以正常功能,cronie 程序可以很好的处理前台的 SIGTERM 信号。
代码
下面是最后整合的代码:
FROM ubuntu
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install -y cron \
# Remove package lists for smaller image sizes
&& rm -rf /var/lib/apt/lists/* \
&& which cron \
&& rm -rf /etc/cron.*/*
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
# https://manpages.ubuntu.com/manpages/trusty/man8/cron.8.html
# -f | Stay in foreground mode, don't daemonize.
# -L loglevel | Tell cron what to log about jobs (errors are logged regardless of this value) as the sum of the following values:
CMD ["cron","-f", "-L", "2"]
下面是镜像编译和运行命令:
docker build -t cron .
docker run --rm --name cron -e CUSTOM_ENV_VAR=foobar -v `pwd`/crontab:/etc/crontab cron
你可以看到下面的输出:
foobar
Wed Jun 15 07:46:01 UTC 2022
总结
条件允许的情况下,还是用 Ofelia 吧!🤣