vault backup: 2026-11-07 16:33:44
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Cloud Mail.mhtml - 白云飘飘的登录页面。
|
||||
@@ -0,0 +1,341 @@
|
||||
---
|
||||
page-title: "轻量的定时任务工具 Cronicle:前篇 - 苏洋博客"
|
||||
url: https://soulteary.com/2021/12/05/cronicle-a-lightweight-tool-for-timed-tasks-part-1.html
|
||||
web-message:
|
||||
web-badge-color: ""
|
||||
web-badge-message:
|
||||
date: "2024-04-09 09:23:11"
|
||||
---
|
||||
|
||||
> 本篇文章将介绍一款轻量的、自带简洁 Web UI,适用于中小团队以及个人的定时任务工具:Cronicle。
|
||||
>
|
||||
> 本文是关于 Cronicle 的第一篇文章,主要聊聊这个软件在容器封装下的常见问题,以及容器封装思路。
|
||||
>
|
||||
> ## 写在前面
|
||||
>
|
||||
> Cronicle 自 2016 年正式开始开源,到现在已经过去了五年多了。而我第一次注意到这款软件则是在 2018 年,当时我正在为我的 HomeLab 挑选合适的定时任务工具。在过去的几年里,可以看到软件一直在细节功能上优化,目前已经做的已经比较完善了,尤其是近两年,基本没有功能上特别大的改版和变更发生。
|
||||
>
|
||||
> 软件除了支持基础的定时任务之外,还包含了非常多有用的功能:
|
||||
>
|
||||
> - 支持多实例搭建分布式定时任务系统
|
||||
> - 具备故障自愈和服务自动迁移能力
|
||||
> - 支持服务发现、以及具备自动组网的能力
|
||||
> - 允许实时查看任务执行状况
|
||||
> - 具备基础的插件系统,支持使用任意语言和方式来扩展能力
|
||||
> - 可以针对不同时区创建定时任务
|
||||
> - 可以针对降级执行时间比较长的任务做排队处理
|
||||
> - 基础的任务性能图标和统计数据
|
||||
> - 具备开放的 API、支持应用 API 密钥
|
||||
> - 具备 Web Hook 通知能力
|
||||
>
|
||||
> 如果你只将它作为任务触发器使用,它的内存资源消耗将会非常小,在我重新封装的镜像中,运行超过20个小时的程序,面板展示内存使用仅 80MB 出头,而 `docker stats` 的结果,则连 50 MB 都不到。
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 轻量的资源消耗
|
||||
>
|
||||
> ```
|
||||
> CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
|
||||
> ec4d5ab18b68 docker_cronicle_1 5.56% 46.09MiB / 15.64GiB 0.29% 4.71MB / 20.6MB 0B / 0B 11
|
||||
> ```
|
||||
>
|
||||
> 相比较使用传统的方式运行任务,和多数软件一样,它自带了任务执行列表,默认最低精度是 10秒,足够多数场景下的使用。
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 历史任务执行列表
|
||||
>
|
||||
> 同时,也支持针对任务输出简单的统计摘要。
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 简单的统计摘要
|
||||
>
|
||||
> 为了能够更好的使用它,我们需要对它先进行容器化封装。
|
||||
>
|
||||
> 如果你已经迫不及待的想开始使用它,可以跳转至至下一个章节 “在容器中使用 Cronicle”。
|
||||
>
|
||||
> ## 使用容器封装 Cronicle
|
||||
>
|
||||
> 第一次使用容器封装这个软件,应该是在 2018 年[这个 PR](https://github.com/jhuckaby/Cronicle/pull/80) 前后。随后虽然也有不少网友对这个软件进行了封装,但是都有一些不完善的地方,比如:如果你将容器进行了迁移或重建,软件将会无法运行,除非你[手动进行修复](https://github.com/jhuckaby/Cronicle/issues/36);比如首次使用的时候,需要等待至少一分钟的时间才能够让软件自己组网成功,然后才可以[开始使用](https://github.com/jhuckaby/Cronicle/issues?q=60+seconds+)…
|
||||
>
|
||||
> 所以,这篇文章里,我们就先来解决这两个问题吧。
|
||||
>
|
||||
> ### 减少 Cornicle 启动等待时间
|
||||
>
|
||||
> 在 [jhuckaby/Cronicle/blob/master/lib/engine.js](https://github.com/jhuckaby/Cronicle/blob/master/lib/engine.js) 中,有一个名为 `startup` 的函数,大概有一百多行,其中记录了 Cronicle 启动后需要做的事情,造成我们需要等待 60 秒才能够使用软件的逻辑主要是这部分:
|
||||
>
|
||||
> ```
|
||||
> ...
|
||||
>
|
||||
> startup: function(callback) {
|
||||
> // start cronicle service
|
||||
> var self = this;
|
||||
> this.logDebug(3, "Cronicle engine starting up");
|
||||
>
|
||||
> // create a few extra dirs we'll need
|
||||
>
|
||||
> ...
|
||||
>
|
||||
> // archive logs daily at midnight
|
||||
> this.server.on('day', function () {
|
||||
> self.archiveLogs();
|
||||
> });
|
||||
>
|
||||
> // determine master server eligibility
|
||||
> this.checkMasterEligibility(function () {
|
||||
> // master mode (CLI option) -- force us to become master right away
|
||||
> if (self.server.config.get('master') && self.multi.eligible) self.goMaster();
|
||||
>
|
||||
> // reset the failover counter
|
||||
> self.multi.lastPingReceived = Tools.timeNow(true);
|
||||
>
|
||||
> // startup complete
|
||||
> callback();
|
||||
> });
|
||||
> }
|
||||
> ...
|
||||
> ```
|
||||
>
|
||||
> 因为软件默认运行环境假设是多机的分布式环境,所以有一个比较长时间的服务发现和注册的过程。
|
||||
>
|
||||
> 而在本篇文章中,我们主要以单机模式运行,所以我们可以对它进行一些细微的改造,让 `self.goMaster();` 这个注册当前运行实例的动作在 `this.checkMasterEligibility` 前执行就可以了。
|
||||
>
|
||||
> 考虑到 Cronicle 团队接收 PR 的时间比较漫长,为了快速实现这个功能,可以采取自制补丁,并在容器构建过程中进行“补丁应用”的方式来实现。
|
||||
>
|
||||
> 对程序进行适当调整之后,执行 `diff -u lib/engine.js /tmp/engine.js > engine.patch` 就可以轻松创建一个类似下面内容的程序补丁啦。
|
||||
>
|
||||
> ```
|
||||
> --- lib/engine.js 2021-06-17 19:03:36.000000000 +0000
|
||||
> +++ /tmp/engine.js 2021-12-04 09:50:13.000000000 +0000
|
||||
> @@ -152,7 +152,8 @@
|
||||
> this.server.on('day', function() {
|
||||
> self.archiveLogs();
|
||||
> } );
|
||||
> -
|
||||
> + // for docker env
|
||||
> + self.goMaster();
|
||||
> // determine master server eligibility
|
||||
> this.checkMasterEligibility( function() {
|
||||
> // master mode (CLI option) -- force us to become master right away
|
||||
> ```
|
||||
>
|
||||
> 而要应用补丁也很简单,只需要执行 `patch -p3 < engine.patch lib/engine.js` 即可。
|
||||
>
|
||||
> 我们先将补丁文件保存好,稍后再使用。
|
||||
>
|
||||
> ### Cronicle 在容器中运行的其他常见问题
|
||||
>
|
||||
> 想要正常的运行 Cronicle ,默认情况下需要执行三条命令:
|
||||
>
|
||||
> ```
|
||||
> /opt/cronicle/bin/build.js dist
|
||||
> /opt/cronicle/bin/control.sh setup
|
||||
> /opt/cronicle/bin/control.sh start
|
||||
> ```
|
||||
>
|
||||
> 前两条命令中包含了程序启动和运行过程中依赖的目录结构,以及包含了当前运行环境信息,并将其中一些信息以配置的形式进行了持久化保存。而如果我们重新创建容器环境,容器的网络、主机名都有可能产生变化,这也是为什么如果我们进行运行环境迁移,很容易遇到程序无法正常工作,需要重新部署配置程序的原因。
|
||||
>
|
||||
> 而第三条命令中,则是以 Daemon 的方式启动程序,因此以往有一些 Cronicle 的容器封装者会使用类似 `tini` 之类的程序,来完成容器封装。但其实,如果我们将容器直接以前台方式运行,就不需要这些额外的程序来做僵尸进程捕获和系统信号转发了。
|
||||
>
|
||||
> 当这三条命令执行完毕,软件运行所需要的目录、配置将自动初始化完毕,然后软件将运行在系统后台。
|
||||
>
|
||||
> 如果包含了程序的容器在运行过程中出现异常中断,软件运行时创建的 PID 文件并不会“销毁”,这同样会导致程序无法重新运行起来。
|
||||
>
|
||||
> 所以,为了避免和解决上面的问题,以及改进使用体验,我们需要额外的写一个小程序。
|
||||
>
|
||||
> ### 编写适合容器内使用的启动脚本
|
||||
>
|
||||
> 上面清楚的提到了容易发生的问题,以及问题的根源,所以编写一个用来解决这些问题的程序,也就很简单了:
|
||||
>
|
||||
> ```
|
||||
> #!/usr/bin/env node
|
||||
>
|
||||
> const { existsSync, unlinkSync } = require('fs');
|
||||
> const { dirname } = require('path');
|
||||
> const { hostname, networkInterfaces } = require('os');
|
||||
> const StandaloneStorage = require('pixl-server-storage/standalone');
|
||||
>
|
||||
> if (existsSync("./logs/cronicled.pid")) unlinkSync("./logs/cronicled.pid");
|
||||
>
|
||||
> process.chdir(dirname(__dirname));
|
||||
>
|
||||
> const config = require('../conf/config.json');
|
||||
>
|
||||
> const storage = new StandaloneStorage(config.Storage, function (err) {
|
||||
> if (err) throw err;
|
||||
>
|
||||
> const dockerHostName = (process.env['HOSTNAME'] || process.env['HOST'] || hostname()).toLowerCase();
|
||||
>
|
||||
> const networks = networkInterfaces();
|
||||
> const [ip] = Object.keys(networks).
|
||||
> filter(eth => networks[eth].
|
||||
> filter(addr => addr.internal === false && addr.family === "IPv4").length).
|
||||
> map(eth => networks[eth])[0];
|
||||
>
|
||||
> const data = {
|
||||
> "type": "list_page",
|
||||
> "items": [{ "hostname": dockerHostName, "ip": ip.address }]
|
||||
> };
|
||||
>
|
||||
> const key = "global/servers/0";
|
||||
> storage.put(key, data, function () {
|
||||
> storage.shutdown(function () {
|
||||
> console.log("Record successfully saved: " + key + "\n");
|
||||
> storage.get(key, function (_, data) {
|
||||
> if (storage.isBinaryKey(key)) {
|
||||
> console.log(data.toString() + "\n");
|
||||
> } else {
|
||||
> console.log(((typeof (data) == 'object') ? JSON.stringify(data, null, "\t") : data) + "\n")
|
||||
> }
|
||||
> storage.shutdown(function () {
|
||||
> console.log("Docker Env Fixed.");
|
||||
> require('../lib/main.js');
|
||||
> });
|
||||
> });
|
||||
> });
|
||||
> });
|
||||
> });
|
||||
> ```
|
||||
>
|
||||
> 上面不到五十行代码主要做了几件事情:
|
||||
>
|
||||
> - 检测是否有之前运行程序遗留下来的 PID 文件,如果有,则清理掉,避免影响程序启动。
|
||||
> - 将目前实际运行的容器环境中的 IP、主机名更新到程序配置中,避免程序不能正确启动。
|
||||
> - 以前台的方式运行程序,避免再经手其他程序,保证容器足够简单。
|
||||
>
|
||||
> ### 编写容器镜像文件
|
||||
>
|
||||
> 这里因为 Cronicle 实际运行会使用到 shell,所以不推荐使用之前 [《使用以语言为中心的容器基础镜像 distroless》](https://soulteary.com/2021/10/14/use-language-centric-container-base-image-distroless.html) 一文中提到的方式进行最小化镜像构建,仅使用普通的二阶段构建即可:
|
||||
>
|
||||
> ```
|
||||
> FROM node:16 AS Builder
|
||||
> ENV CRONICLE_VERSION=0.8.62
|
||||
> WORKDIR /opt/cronicle
|
||||
> COPY Cronicle-${CRONICLE_VERSION}.tar.gz /tmp/
|
||||
> RUN tar zxvf /tmp/Cronicle-${CRONICLE_VERSION}.tar.gz -C /tmp/ && \
|
||||
> mv /tmp/Cronicle-${CRONICLE_VERSION}/* . && \
|
||||
> rm -rf /tmp/* && \
|
||||
> npm install --registry=https://registry.npm.taobao.org
|
||||
> COPY ./patches /tmp/patches
|
||||
> RUN patch -p3 < /tmp/patches/engine.patch lib/engine.js
|
||||
>
|
||||
>
|
||||
> FROM node:16-alpine
|
||||
> COPY --from=builder /opt/cronicle/ /opt/cronicle/
|
||||
> WORKDIR /opt/cronicle
|
||||
>
|
||||
> ENV CRONICLE_foreground=1
|
||||
> ENV CRONICLE_echo=1
|
||||
> ENV CRONICLE_color=1
|
||||
> ENV debug_level=1
|
||||
>
|
||||
> ENV HOSTNAME=main-server
|
||||
>
|
||||
> RUN node bin/build.js dist && \
|
||||
> bin/control.sh setup
|
||||
> COPY docker-entrypoint.js ./bin/
|
||||
> CMD ["node", "bin/docker-entrypoint.js"]
|
||||
> ```
|
||||
>
|
||||
> 因为即使是普通的二阶段构建,和基础镜像切换,也能够将软件的镜像体积由 1G 降低到 150M 不到,更加适合分发和保存。
|
||||
>
|
||||
> ```
|
||||
> cronicle latest c0575a5b900b 22 hours ago 1.04GB
|
||||
> cronicle latest e31626eac385 3 seconds ago 146MB
|
||||
> ```
|
||||
>
|
||||
> ## 在容器中使用 Cronicle
|
||||
>
|
||||
> 想要让 Cronicle 快速运行起来,可以使用我预构建好的容器镜像,为了让这个镜像能够正常运行起来,我们需要两个编排文件,分别用于程序“初始化”和“正常运行”,先来编写正常运行的文件:
|
||||
>
|
||||
> ```
|
||||
> version: "3.6"
|
||||
>
|
||||
> services:
|
||||
>
|
||||
> cronicle:
|
||||
> image: soulteary/cronicle:0.8.62
|
||||
> restart: always
|
||||
> hostname: cronicle
|
||||
> ports:
|
||||
> - 3012:3012
|
||||
> volumes:
|
||||
> - /etc/localtime:/etc/localtime:ro
|
||||
> - /etc/timezone:/etc/timezone:ro
|
||||
> - ./data/data:/opt/cronicle/data
|
||||
> - ./data/logs:/opt/cronicle/logs
|
||||
> - ./data/plugins:/opt/cronicle/plugins
|
||||
> extra_hosts:
|
||||
> - "cronicle.lab.io:0.0.0.0"
|
||||
> environment:
|
||||
> - TZ=Asia/Shanghai
|
||||
> healthcheck:
|
||||
> test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider localhost:3012/api/app/ping || exit 1"]
|
||||
> interval: 5s
|
||||
> timeout: 1s
|
||||
> retries: 3
|
||||
> logging:
|
||||
> driver: "json-file"
|
||||
> options:
|
||||
> max-size: "10m"
|
||||
> ```
|
||||
>
|
||||
> 将上面的文件保存为 `docker-compose.yml` 后,继续来编写初始化运行的配置:
|
||||
>
|
||||
> ```
|
||||
> version: "3.6"
|
||||
>
|
||||
> services:
|
||||
>
|
||||
> cronicle:
|
||||
> image: soulteary/cronicle:0.8.62
|
||||
> hostname: cronicle
|
||||
> command: /opt/cronicle/bin/control.sh setup
|
||||
> volumes:
|
||||
> - /etc/localtime:/etc/localtime:ro
|
||||
> - /etc/timezone:/etc/timezone:ro
|
||||
> - ./data/data:/opt/cronicle/data
|
||||
> - ./data/logs:/opt/cronicle/logs
|
||||
> - ./data/plugins:/opt/cronicle/plugins
|
||||
> environment:
|
||||
> - TZ=Asia/Shanghai
|
||||
> ```
|
||||
>
|
||||
> 将上面的文件保存为 `docker-compose.init.yml`,然后执行 `docker-compose -f docker-compose.init.yml up`,不出意外,你将会得到类似下面的内容:
|
||||
>
|
||||
> ```
|
||||
> cronicle_1 |
|
||||
> cronicle_1 | Setup completed successfully!
|
||||
> cronicle_1 | This server (main) has been added as the single primary master server.
|
||||
> cronicle_1 | An administrator account has been created with username 'admin' and password 'admin'.
|
||||
> cronicle_1 | You should now be able to start the service by typing: '/opt/cronicle/bin/control.sh start'
|
||||
> cronicle_1 | Then, the web interface should be available at: http://main:3012/
|
||||
> cronicle_1 | Please allow for up to 60 seconds for the server to become master.
|
||||
> cronicle_1 |
|
||||
> docker-cronicle_cronicle_1 exited with code 0
|
||||
> ```
|
||||
>
|
||||
> 接着再使用 `docker-compose up -d` 启动服务即可,大概几秒钟后,使用 `docker-compose ps` 检查服务,就能够看到服务运行正常的结果了。
|
||||
>
|
||||
> ```
|
||||
> Name Command State Ports
|
||||
> ---------------------------------------------------------------------------------------------------
|
||||
> docker-cronicle_cronicle_1 docker-entrypoint.sh node ... Up (healthy) 0.0.0.0:3012->3012/tcp
|
||||
> ```
|
||||
>
|
||||
> 此时,我们在浏览器中打开 `localhost:3012` 就能够开始使用软件啦,软件的默认账号和密码都是 `admin`。
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 软件默认界面
|
||||
>
|
||||
> 因为软件功能界面非常直观,这里就不多针对软件的基础使用进行赘述啦。
|
||||
>
|
||||
> ## 最后
|
||||
>
|
||||
> 关于分布式使用、容器内灾备转移、插件编写,或许适合在下一篇关于 Cronicle 的文章中展开。文中相关代码我已经[上传至 GitHub](https://github.com/soulteary/docker-cronicle) ,有需要的小伙伴可以自取。
|
||||
>
|
||||
> 谨以此文献给刚刚创建的技术讨论群中的小伙伴,权作抛砖引玉。
|
||||
>
|
||||
> –EOF
|
||||
Reference in New Issue
Block a user