Cronicle 日常运维手册
运维篇:Cronicle 日常运维手册,覆盖六步巡检闭环、备份恢复、失败告警 Webhook 与邮件通知实测、扩容升级流程,以及附原始报错的故障排查手册(ENOEXEC/超时/双主冲突/改密失效)。
Cronicle 日常运维手册
目标:面向 Cronicle 管理员,建立「日常巡检 → 备份恢复 → 监控告警 → 故障排查」的运维闭环:六步巡检每步给命令与实测回显,失败告警给出 Webhook 最小实现并实测送达,故障排查手册收录带原始报错的实测排障实录。已在 Ubuntu 22.04 服务器、Cronicle 0.9.131 实测环境逐项验证。
一、环境信息
- 操作系统:Ubuntu 22.04(x86_64)
- Cronicle 版本:0.9.131,安装目录
/opt/cronicle - 服务管理:
/opt/cronicle/bin/control.sh(start/stop/restart/status/setup) - 关键端口:3012(Web UI 与 API)、3013(HTTPS,默认关闭)、3014(UDP 节点发现)
- 关键目录:
/opt/cronicle/data(存储引擎数据)、/opt/cronicle/logs(组件与作业日志)、/opt/cronicle/conf(配置)、/opt/cronicle/queue(队列暂存) - 告警链路实测形态:全局 universal_web_hook → 内网 HTTP 接收端;SMTP → 内网邮件中继
- 部署形态说明:本手册同样适用于以《Cronicle Docker Compose 部署指南》形态部署的实例(路径语义一致,服务管理命令换成对应部署形态的服务管理方式即可)
二、日常巡检
日常巡检的目标是「一分钟内确认服务活着、作业正常、没有异常日志、磁盘不涨爆」。以下六步构成闭环,每步给命令与实测回显。
2.1 服务进程状态
用官方管理脚本确认主服务进程在位:
/opt/cronicle/bin/control.sh status
实测输出:
status: Cronicle Server running (pid 7)
若输出 status: Cronicle Server not running (no pid file)(实测同样可复现:服务停止后该命令返回此提示),说明服务未运行,需排查启动日志后 control.sh start。
2.2 端口探活(3012)
确认 Web/API 端口本机可达:
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3012/
实测输出:
200
返回 200 即 Web 服务在位;连接被拒(Connection refused)说明进程死了或端口被改。
2.3 Web 可用性与登录链路
端口 200 只代表静态服务活着,还需确认登录与调度状态 API 可用(等同于「能登录、调度器在跑」):
SID=$(curl -s -X POST http://127.0.0.1:3012/api/user/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"q1w2e3r4"}' \
| python3 -c "import json,sys; print(json.load(sys.stdin)['session_id'])")
curl -s http://127.0.0.1:3012/api/app/get_master_state/v1 -H "X-Session-ID: $SID" | head -c 200
实测输出(节选):
{"code":0,"state":{"enabled":true,"cursors":{"emveu1c8y01":1792389660,"emveua39j02":1792389660},"stats":{"jobs_completed":6,"jobs_failed":4,...
code:0 且 state.enabled 为 true:登录链路正常、调度器处于启用状态;stats 里还能直接读到累计完成/失败作业数。
2.4 Job 失败历史检查
检查最近作业历史中的失败记录(code 非 0 的行即为失败作业):
curl -s -X POST http://127.0.0.1:3012/api/app/get_history/v1 \
-H "X-Session-ID: $SID" -H 'Content-Type: application/json' -d '{}' \
| python3 -c "
import json,sys
d=json.load(sys.stdin)
for r in d.get('rows',[]):
print(r.get('id'), '|', r.get('event_title'), '| code:', r.get('code'), '| elapsed:', r.get('elapsed'))
"
实测输出:
jmveucxa401 | 测试-超时被杀 | code: 1 | elapsed: 20.533999919891357
jmveua9mf03 | 测试-超时被杀 | code: 1 | elapsed: 20.509999990463257
jmveu3ypg04 | 测试-Shell回显 | code: 0 | elapsed: 0.053999900817871094
jmveu3dji03 | 测试-Shell回显 | code: 1 | elapsed: 1.0490000247955322
jmveu1ggh02 | 测试-Shell回显 | code: 1 | elapsed: 1.0509998798370361
巡检口径:code: 0 为成功;非 0 逐条看 description 定因(本例三条失败分别是脚本无 shebang 的 ENOEXEC 与超时 SIGTERM,见第七节排障实录)。此查询可直接纳入外部监控脚本,出现新失败即触发告警。
2.5 日志异常检查
Cronicle 的组件日志按组件分文件落在 /opt/cronicle/logs/(Cronicle.log、Error.log、WebServer.log、Storage.log 等),错误集中在 Error.log:
tail -2 /opt/cronicle/logs/Error.log | cut -c1-160
实测输出:
[1792389277.013][2026-10-19 13:54:37][cronicle-server][7][Error][error][job][Job failed: jmveua9mf03][{"category":"general","plugin":"shellplug","target":"croni
[1792389401.01][2026-10-19 13:56:41][cronicle-server][7][Error][error][job][Job failed: jmveucxa401][{"category":"general","plugin":"shellplug","target":"cronic
巡检口径:Error.log 为空或只有已知 job failed 记录即正常;出现 [Error][error] 且 component 不是 job 的记录(如 Storage/WebServer 组件错误)需要立即跟进。主日志 Cronicle.log 可用 grep -iE 'error|crash' | tail 快扫。
2.6 磁盘水位检查
Cronicle 的磁盘消耗主体是作业日志与历史数据(data 目录 + logs 目录):
df -h /opt/cronicle | tail -1
du -sh /opt/cronicle/data /opt/cronicle/logs 2>/dev/null
实测输出:
overlay 786G 265G 486G 36% /
856K /opt/cronicle/data
196K /opt/cronicle/logs
巡检口径:文件系统使用率超过 80% 进入告警线;data 目录增长速率异常(如日均增长数百 MB)通常是高频大输出作业,优先调小事件级 log_max_size 或全局 job_log_max_size,并按《Cronicle 0.9.131 配置参数详解》调小 job_data_expire_days。
2.7 巡检脚本化建议
将以上六步串成一个可 cron 化的巡检脚本骨架(判定阈值按需调整,失败即非零退出以对接监控):
#!/bin/sh
# cronicle_daily_check.sh —— 任一步失败即 exit 1,可被监控拉起告警
/opt/cronicle/bin/control.sh status || exit 1
[ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3012/)" = "200" ] || exit 1
tail -20 /opt/cronicle/logs/Error.log | grep -v '\[Error\]\[error\]\[job\]' && exit 1
USED=$(df -P /opt/cronicle | tail -1 | awk '{print $5}' | tr -d '%')
[ "$USED" -lt 80 ] || exit 1
echo "cronicle check ok"
💡 说明:脚本对 Error.log 的过滤逻辑是「允许出现 job 类失败记录(作业失败已有独立告警链路),但出现其他组件错误立即判异常」——这正是 2.5 节巡检口径的脚本化表达。
三、备份与恢复
3.1 备份什么
Cronicle 全部状态在两个目录:conf/(config.json 是集群灵魂,丢了等于重新配置)与 data/(调度表、用户、分类、插件定义、作业历史全在这里)。logs/ 可不备份。
3.2 备份方式一:官方导出命令(逻辑备份)
官方 control.sh 自带 export 子命令,把存储数据导出为文本格式:
/opt/cronicle/bin/control.sh export > /opt/backup/cronicle-export-$(date +%Y%m%d).json
实测输出(导出文件头部与体量):
# Cronicle Data Export v1.0
# Hostname: cronicle-server
# Date/Time: Mon Oct 19 2026 17:08:19 GMT+0800 (China Standard Time)
# Format: KEY - JSON
users/admin - {"username":"admin","password":"$2b$10$Hw6m7TOhvOvDfTx15ZFhge6SVnazeuE598GExPu2Z6PngGfrimdQW","full_name":"Administrator","email":"admin@cronicle.com","active":1,"modified":1792388807,"created":1792388708,"salt":"96ede79759c89642fcda3979a8
导出文件实测 4905 字节(小规模实例),格式为「KEY - JSON」逐行键值,可读可 diff。对应的恢复子命令为 control.sh import(导入该格式文件)。
⚠️ 注意:导出文件包含密码哈希与 secret_key 相关数据,备份文件权限应收紧(
chmod 600)并存储到受限目录。
3.3 备份方式二:目录级冷备(物理备份)
停服务或接受轻微不一致时,直接打包 data + conf:
tar -czf /opt/backup/cronicle-backup-$(date +%Y%m%d).tar.gz data conf
实测输出(包大小与内容清单节选):
-rw-r--r-- 1 root root 20119 10月 19 17:08 /opt/backup/cronicle-backup-20261019.tar.gz
data/
data/_cleanup/
data/_cleanup/46/
...
恢复方式:停服务 → 解包覆盖 data/conf → 起服务。目录级冷备的恢复是「原样回填」,与 export/import 的逻辑恢复相比更依赖版本一致(跨大版本恢复建议用 export 文件走 import)。
3.4 备份策略建议
- 频率:数据量小(本例实例 data 856K),每日全量即可,cron 化(注意:Cronicle 自己排自己的备份作业时,export 命令走 shellplug 插件执行)
- 保留:7 天滚动 + 每月一份长期
- 校验:备份后
tar -tzf或python3 -m json.tool抽验可读性
四、监控告警
4.1 告警通道概览
Cronicle 原生提供两条告警通道:邮件通知(SMTP + 事件级 notify_success/notify_fail 收件人)与 Webhook(事件级 web_hook / 全局 universal_web_hook)。技术拓展:Webhook 通知在业界有两种对接姿势——直连 IM 机器人(钉钉/企微/飞书群机器人 URL 直接作为 webhook 地址,但报文格式需匹配各 IM 的 schema)与中转接收端(自建轻量 HTTP 接收端收下 Cronicle 的 JSON 再适配转发)。Cronicle 的报文是自有 JSON schema(含 action/message/text 字段),直连钉钉企微需要中转适配,因此生产常见做法是「universal_web_hook → 自建接收端 → 适配后转发《钉钉机器人 webhook 告警接入指南》群机器人」。本节实测中转接收端形态。
4.2 Webhook 告警最小实现
第一步:在内网起一个最小 HTTP 接收端(Python 标准库即可,无需框架),监听 19999 端口:
#!/usr/bin/env python3
# hook_receiver.py —— Cronicle Webhook 最小接收端(生产请替换为转发钉钉企微告警webhook的逻辑)
import http.server, datetime
LOG = "/opt/data/cronicle-hook/received.log"
class H(http.server.BaseHTTPRequestHandler):
def do_POST(self):
n = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(n).decode("utf-8", "replace")
with open(LOG, "a") as f:
f.write("=== %s %s ===\n" % (datetime.datetime.now().isoformat(), self.path))
f.write("User-Agent: %s\n" % self.headers.get("User-Agent", ""))
f.write("Content-Type: %s\n" % self.headers.get("Content-Type", ""))
f.write(body + "\n")
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
def log_message(self, *a):
pass
http.server.HTTPServer(("0.0.0.0", 19999), H).serve_forever()
第二步:启动接收端后,先用 curl 手工验证接收端可达(这是后续排障的分界点——先证接收端好,再看 Cronicle 发不发):
curl -s -X POST http://127.0.0.1:19999/hook-test -d '{"probe":"curl-manual-test"}'
实测输出:
ok
第三步:修改 config.json 启用全局 Webhook(指向接收端所在主机 IP):
"universal_web_hook": "http://<接收端主机IP>:19999/cronicle-hook",
修改后重启服务生效(/opt/cronicle/bin/control.sh restart)。
第四步:触发一个会失败的作业(超时事件:timeout 10 秒,脚本 sleep 30),验证告警送达。接收端实测收到两条报文:
报文一(作业启动 job_start,节选关键层):
=== 2026-10-19T13:56:20.487906 /cronicle-hook ===
User-Agent: Cronicle 0.9.131
Content-Type: application/json
{"category":"general","plugin":"shellplug","target":"cronicle-server","params":{"script":"#!/bin/sh\necho start-long-sleep; sleep 30; ..."},"timeout":10,"timezone":"Asia/Shanghai","source":"Manual (admin)","id":"jmveucxa401",...,"action":"job_start","base_app_url":"http://192.168.1.62:8115","job_details_url":"http://192.168.1.62:8115/#JobDetails?id=jmveucxa401","message":"Job started on cronicle-server: 测试-超时被杀 http://...","text":"Job started on cronicle-server: 测试-超时被杀 http://..."}
报文二(作业完结 job_complete,失败判定的关键字段):
action: job_complete | code: 1 | complete: 1 | description: 'Job Aborted: Exceeded maximum run time (10 seconds)'
其中 message 字段按失败模板渲染为:
Job failed on cronicle-server: 测试-超时被杀: Error 1: Job Aborted: Exceeded maximum run time (10 seconds) http://192.168.1.62:8115/#JobDetails?id=jmveucxa401
告警判定规则(接收端适配层据此分流):完结报文里 action == "job_complete" 且 code != 0 → 失败告警;code == 0 → 成功通知(可不转发只留档);action == "job_start" → 启动通知。message/text 字段是渲染好的可读文本,直接取用。
4.3 邮件告警实测
Webhook 之外,邮件通道作为兜底。配置三件套 + 事件收件人:
{
"smtp_hostname": "10.89.43.1",
"smtp_port": 25025,
"email_from": "admin@localhost"
}
事件上配置 notify_fail: "ops@quzuo.net" 后触发超时失败作业,内网邮件中继实测收到失败通知邮件,正文关键内容(quoted-printable 解码后):
Date/Time: 2026/10/19 14:02:44 (GMT+8)
Event Title: 测试-超时被杀
Category: General
Server Target: cronicle-server
Plugin: Shell Script
Job ID: jmveukp4r02
Hostname: cronicle-server
PID: 58
Elapsed Time: 20 seconds
Performance Metrics: (No metrics provided)
Avg. Memory Usage: 58.3 MB (Peak: 58.3 MB)
Avg. CPU Usage: 0.3% (Peak: 0.3%)
Error Code: 1
Error Description:
Job Aborted: Exceeded maximum run time (10 seconds)
Job Details:
http://192.168.1.62:8115/#JobDetails?id=jmveukp4r02
邮件主题实测带 emoji 前缀(失败 ⚠️ Cronicle Job Failed、成功 ✅ Cronicle Job Completed Successfully),正文自带作业的内存/CPU 性能摘要与详情链接,可直接作为最小可用的告警邮件。
4.4 外部拨测建议
Cronicle 自身宕机时它的告警通道也失效(告警者先死问题),需外部拨测兜底:对 http://<host>:3012/ 做 HTTP 拨测(Gatus/Uptime Kuma 等均可,见《Gatus 介绍与选型指南》健康探测配置),告警条件建议「连续 3 次非 200」。
五、扩容缩容
5.1 加工作节点(扩容)
按官方 Setup.md 的流程(转述并归纳):在目标服务器安装同版本 Cronicle(安装方式见官方 Setup.md 或《Cronicle 0.9.137 介绍与选型指南》第五节)→ 不要在新节点执行 control.sh setup(setup 只在主节点首启时跑)→ 把主节点的 conf/config.json 原样拷贝到新节点同路径(secret_key 必须一致)→ 新节点执行 control.sh start。新节点会经 UDP 广播(端口 3014)或手动添加进入集群,在 Web UI 的 Servers 页签可见;默认新节点只进「工作」角色,除非把其主机名加入「Primary Group」的正则。
官方 Ops Notes 的硬性前提(转述):所有节点配置文件完全一致、时钟同步、主机名唯一且可 DNS 解析、每节点至少一个活动 IPv4。
5.2 缩容
在 Web UI Servers 页签先移除目标节点,再停其服务。若移除的是主节点,集群按主机名字母序从 Primary Group 里选出新主(前提:主角色节点共享存储)。
六、升级与回滚
6.1 升级流程
- 备份先行:按第三节做 export 逻辑备份 + 目录冷备(双保险);
- 看 Release Notes:GitHub Releases 页核对目标版本变更(如 0.9.137 为依赖升级 Bump pixl-server-web to v3.0.6),无破坏性变更再升;
- 停服务:
/opt/cronicle/bin/control.sh stop; - 升级程序:按官方 Setup.md 的手工安装路径重跑(下载新版本 tar 包解压到 /opt/cronicle 后
npm install && node bin/build.js dist),或直接用官方一键安装脚本装新版; - 配置核对:diff 新旧 conf/config.json(新版可能带新默认键),确认 secret_key 与通知配置未丢;
- 启动验证:
control.sh start后按第二节六步巡检过一遍,重点 2.3(登录与调度状态)与 2.4(历史作业仍在)。
6.2 回滚方案
升级失败或行为异常的回滚:停服务 → 用 3.3 的目录级冷备包覆盖回 data/conf(或用 export 文件 control.sh import 逻辑恢复)→ 装回旧版本程序 → 启动后巡检验证。这也是升级前必须做双份备份的原因:目录冷备管「整体回退」,export 管跨版本的数据迁移兼容。
七、故障排查手册(附原始报错)
以下五例均为实测排障实录,报错逐字照抄,可直接对号入座。
7.1 Shell 作业立即失败:spawn ENOEXEC
现象:Shell Script 插件作业启动 1 秒即失败,job 状态 code: 1、description: "Child 44 crashed with code: 1",作业日志文件甚至来不及留存。
取日志:get_job_log API(或 UI 作业详情)实测原始报错:
Error: spawn ENOEXEC
at ChildProcess.spawn (node:internal/child_process:441:11)
at Object.spawn (node:child_process:796:9)
at __construct.<anonymous> (/opt/cronicle/bin/shell-plugin.js:25:17)
...
errno: -8,
code: 'ENOEXEC',
syscall: 'spawn'
根因:Shell Script 插件把脚本内容写进临时 .sh 文件后直接执行(cp.spawn(script_file)),脚本第一行没有 shebang 时内核无法识别解释器,spawn 返回 ENOEXEC。
解决:脚本第一行加 #!/bin/sh(或 #!/bin/bash)。修复后同一事件重跑,实测 code: 0 | complete: 1,日志正常输出。
7.2 作业被杀:SIGTERM(超过 timeout)
现象:设定 timeout: 10 的事件跑了 20.5 秒后失败,job 状态实测字段:"code": "SIGTERM"、"description": "Script exited with code: SIGTERM"。
作业日志实测原文:
# Job ID: jmveua9mf03
# Event Title: 测试-超时被杀
# Hostname: cronicle-server
# Date/Time: 2026/10/19 13:54:16 (GMT+8)
start-long-sleep
Caught SIGTERM, killing child: 49
# Job failed at 2026/10/19 13:54:37 (GMT+8).
# Error: Job Aborted: Exceeded maximum run time (10 seconds)
# End of log.
解读:Caught SIGTERM, killing child: 49 是 Cronicle 主动杀子进程的记录;elapsed 20.5 秒 = 10 秒超时 + 终止宽限(child_kill_timeout 10 秒)+ 清理。日志只输出到 start-long-sleep(sleep 30 后的 echo 未执行到),符合超时终止语义。解决:调大事件 timeout,或优化脚本耗时;长作业可启用 detached 模式。
7.3 第二个实例起不来:Please stop Cronicle
现象:对同一份存储数据再启动一个 Cronicle 实例(演练等价部署形态时实测复现),日志报错:
ERROR: Please stop Cronicle before running this script.
removing old pid file
根因:启动流程中的 setup 步骤检测到该存储上已有运行中的 Cronicle,拒绝执行(防止双主写坏数据)。
解决:一套数据目录同时只运行一个主节点进程;确需并行验证用独立数据目录。若是误报(原实例已死但残留 pid 文件),删除 logs/cronicled.pid 后重启。
7.4 改密码「成功」但不生效:password 字段陷阱
现象:调 /api/user/update 传 {"username":"admin","password":"新密码","old_password":"旧密码"},返回 {"code":0},但用新密码登录报:
{"code":"login","description":"Username or password incorrect."}
根因:改密的字段名是 new_password;password 键被当作普通用户属性存储而非密码哈希更新。
解决:{"username":"admin","old_password":"admin","new_password":"q1w2e3r4"},实测返回 {"code":0,"user":{...}} 且新密码登录成功(code: 0)。
7.5 Webhook 收不到:分段定位法
现象:配好 universal_web_hook 重启后接收端始终收不到报文。
分段排查(每段有实测依据):
- 先证接收端好:
curl -s -X POST http://<接收端>:19999/hook-test -d '{"probe":"x"}'→ 返回ok且接收端日志落盘(实测回显见 4.2 第二步)。curl 都打不通就是网络/防火墙问题,与 Cronicle 无关; - 再证 Cronicle 能发:接收端与 Cronicle 跨主机时,在 Cronicle 所在主机上 curl 接收端地址(实测从 Cronicle 服务器内 curl 接收端成功送达,User-Agent 显示
curl/8.22.0),排除主机间路由; - 查配置加载:config.json 改后是否重启;是否有
CRONICLE_universal_web_hook环境变量以更高优先级覆盖(实测环境变量优先级高于配置文件,见《Cronicle 0.9.131 配置参数详解》3.3); - 确认触发源:全局 hook 对完结的作业才会发 job_complete/job_start;作业未真正运行(如调度器 disabled)不会有报文——用 get_master_state 确认
state.enabled: true。
八、常见问题
Q1:巡检发现服务 not running 但进程其实活着?
control.sh status 依据 pid 文件(logs/cronicled.pid)判断。pid 文件丢失(如被误清理)会导致误报;按 7.3 的方式处理 pid 文件后重启。
Q2:作业失败但 Error.log 里没有记录?
job 类失败一定有 [Error][error][job] 记录(实测每次失败都有);没记录说明作业根本没被调度(调度器 disabled 或事件 enabled 0),用 get_master_state 与 get_schedule 排查。
Q3:如何快速清掉测试期的大量失败历史? 等每日 maintenance 窗口自动过期(job_data_expire_days),或临时改小该值重启加速清理;不建议手工删 data 目录文件(存储一致性风险)。
Q4:Web UI 能开但实时日志看不到? 实时日志走 WebSocket 直连执行作业的节点(官方文档要求用户到执行节点的主机名网络可达);跨网段部署时确认 DNS/hosts 能解析各节点主机名。
Q5:多个作业同时失败狂欢怎么止血? 先在 Web UI 关掉调度总开关(state_update 权限,或 API update_master_state),止血后逐个看失败 description 定因,修复后恢复调度。
九、参考链接
- 官方 Setup.md(多服务器 Ops Notes):https://github.com/jhuckaby/Cronicle/blob/master/docs/Setup.md
- 官方 Configuration.md(超时/维护/通知参数):https://github.com/jhuckaby/Cronicle/blob/master/docs/Configuration.md
- 官方 APIReference.md(巡检用 API 定义):https://github.com/jhuckaby/Cronicle/blob/master/docs/APIReference.md
- GitHub Releases(升级前核对):https://github.com/jhuckaby/Cronicle/releases
- 本专栏配套文档:《Cronicle 0.9.137 介绍与选型指南》《Cronicle Docker Compose 部署指南》《Cronicle 0.9.131 配置参数详解》