Anchr 命令运维:服务管理、备份恢复与版本升级

Anchr 3.5.2 命令运维实战:systemctl 服务管理、journalctl 日志、mongodump/mongorestore 备份恢复、systemd timer 定时备份、版本升级流程与 REST API curl 实测,含短链 400 误判排障实录。

AnchrAnchr书签管理2026/10/28

Anchr 命令运维:服务管理、备份恢复与版本升级

实测基线:Anchr 3.5.2 + Ubuntu 22.04.5 LTS + MongoDB 6.0.29,全部命令与回显均来自 2026-10-28 真机逐条执行。前置安装与配置见 《Anchr 安装:Ubuntu 22.04 源码构建与 systemd 服务化》 与 《Anchr 配置:.env 全参数与 systemd 单元详解》。

Anchr 没有自带 CLI 管理工具,日常运维完全依赖"系统命令 + REST API"两套轮子:服务生命周期交给 systemd,数据操作走 mongosh/mongodump 与 curl。本篇按运维高频场景逐条给出实测命令。

一、服务管理:systemctl 全套命令

1.1 状态查看

systemctl status anchr --no-pager

实测回显:

● anchr.service - Anchr self-hosted bookmarks toolbox
     Loaded: loaded (/etc/systemd/system/anchr.service; enabled; vendor preset: enabled)
     Active: active (running) since Wed 2026-10-28 03:29:47 UTC; 32min ago
   Main PID: 5291 (node)
      Tasks: 11 (limit: 2932)
     Memory: 55.0M
        CPU: 8.488s
     CGroup: /system.slice/anchr.service
             └─5291 /usr/bin/node app.js

Memory: 55.0M 是 3.5.2 空载的真实水位——Node.js 单进程,1GB 内存机器也能舒适运行。日常快速判断用两条短命令:

systemctl is-active anchr
systemctl is-enabled anchr
active
enabled

is-enabled 返回 enabled 说明开机自启已就位(B1 篇安装时由 systemctl enable --now anchr 设置)。

1.2 停止与启动(含故障表现)

systemctl stop anchr
systemctl is-active anchr
curl -s -o /dev/null -w "HTTP %{http_code}\n" --max-time 3 http://127.0.0.1:8149/

实测回显:

inactive
HTTP 000

停止后 HTTP 000 表示连接被拒绝——这是验证服务确实停了的最直接方式。重新拉起:

systemctl start anchr
systemctl is-active anchr
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:8149/
active
HTTP 200

1.3 重启与配置变更

.env 或单元文件改动后的标准动作(改 .env 只需 restart;改单元文件必须先 daemon-reload):

systemctl restart anchr

Restart=on-failure 保证进程异常退出时 5 秒内自动拉起,日常无需人工干预;restart 主要服务于配置变更和升级流程。

二、日志查看:journalctl

Anchr 3.5.2 的 .env 中 ANCHR_LOG_PATH/ANCHR_ERROR_LOG_PATH 并未真正落地(实测勘误见 C1 篇),所有日志输出到 stdout,由 systemd 收进 journal:

journalctl -u anchr -n 8 --no-pager

实测回显(服务启动后的典型日志):

Oct 28 04:02:39 anchr-lab node[5479]: [WARN] Disabling Google OAuth as "ANCHR_GOOGLE_*" config variables are missing.
Oct 28 04:02:39 anchr-lab node[5479]: Enabled link safety checking with 3 checkers
Oct 28 04:02:39 anchr-lab node[5479]: [WARN] Disabling safe browse lookups for shortlinks as "ANCHR_GOOGLE_API_KEY" config variable is missing.
Oct 28 04:02:39 anchr-lab node[5479]: Successfully established database connection.
Oct 28 04:02:39 anchr-lab node[5479]: (node:5479) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 uncaughtException listeners added to [process]. MaxListeners is 10. Use emitter.setMaxListeners() to increase limit
Oct 28 04:02:39 anchr-lab node[5479]: Express server listening on 0.0.0.0:8149

两条 [WARN] 属于功能未配置的正常降级提示;MaxListenersExceededWarning 是上游代码对 uncaughtException 监听器注册过多触发的,不影响功能,可忽略。每条 HTTP 请求都会落一行访问日志:

2026-10-28T04:12:33.276Z DELETE /api/collection/0sHQL/links/6ae1743d9b83cc7448a49f5a 6ae169b60b0771ec4192d7ea 200 - - 3.317 ms

格式为 时间 方法 路径 用户ID 状态码 响应体大小 - 耗时,排查 API 问题先看这里。常用组合:

journalctl -u anchr -f          # 实时跟踪
journalctl -u anchr --since today --no-pager   # 今天以来的日志
journalctl -u mongod -n 20 --no-pager          # 数据库侧日志(排障必备)

三、数据备份与恢复

Anchr 的全部状态在两处:MongoDB 库(收藏集/书签/短链/账号)与 $ANCHR_UPLOAD_DIR 附件目录(上传的图片),备份必须同时覆盖。

3.1 数据库备份 mongodump

mkdir -p /root/backup
mongodump --uri="mongodb://anchr:q1w2e3r4@localhost:27017/anchr" --out /root/backup/anchr-2026-10-28

实测回显(节选):

2026-10-28T04:02:50.512+0000	writing `anchr.users` to `/root/backup/anchr-2026-10-28/anchr/users.bson`
2026-10-28T04:02:50.513+0000	done dumping `anchr.shortlinks` (1 document)
2026-10-28T04:02:50.514+0000	done dumping `anchr.images` (1 document)
2026-10-28T04:02:50.514+0000	done dumping `anchr.users` (1 document)
2026-10-28T04:02:50.514+0000	done dumping `anchr.collections` (5 documents)

备份产物按集合分文件存放:

ls /root/backup/anchr-2026-10-28/anchr/
collections.bson
collections.metadata.json
images.bson
images.metadata.json
prelude.json
shortlinks.bson
shortlinks.metadata.json
users.bson
users.metadata.json

3.2 附件目录备份

tar -czf /root/backup/anchr-files-2026-10-28.tar.gz -C /var/data anchr
ls -lh /root/backup/anchr-files-2026-10-28.tar.gz

实测回显:

-rw-r--r-- 1 root root 245 Oct 28 04:02 /root/backup/anchr-files-2026-10-28.tar.gz

3.3 恢复演练:mongorestore --drop

真实演练一次"误删后回滚":先创建一个临时收藏集制造数据漂移,再用备份整体覆盖。

# 漂移前集合数
mongosh --quiet "mongodb://anchr:q1w2e3r4@localhost:27017/anchr" --eval 'print("collections:", db.collections.countDocuments({}))'
collections: 6

执行恢复(--drop 先清空再导入,保证与备份点完全一致):

mongorestore --drop --uri="mongodb://anchr:q1w2e3r4@localhost:27017/anchr" /root/backup/anchr-2026-10-28/anchr/

实测回显(节选):

2026-10-28T04:03:04.161+0000	8 document(s) restored successfully. 0 document(s) failed to restore.

验证回滚生效:

mongosh --quiet "mongodb://anchr:q1w2e3r4@localhost:27017/anchr" --eval 'print("collections:", db.collections.countDocuments({}))'
collections: 5

数量从漂移后的 6 回到备份点的 5,恢复链路验证通过。附件恢复对应 tar -xzf anchr-files-xxx.tar.gz -C /var/data。

3.4 定时备份:systemd timer(推荐)

把 3.1/3.2 固化为脚本,用 systemd timer 每日执行(Ubuntu 22.04 默认最小安装可能不带 cron 包,systemd timer 无额外依赖):

cat > /root/backup/anchr-backup.sh <<'EOF'
#!/bin/bash
# Anchr 每日备份:数据库 + 附件,保留 14 天
BAK=/root/backup/$(date +%F)
mkdir -p "$BAK"
mongodump --uri="mongodb://anchr:q1w2e3r4@localhost:27017/anchr" --out "$BAK" --quiet
tar -czf "$BAK-files.tar.gz" -C /var/data anchr
find /root/backup -maxdepth 1 -mtime +14 -exec rm -rf {} \;
EOF
chmod +x /root/backup/anchr-backup.sh
/root/backup/anchr-backup.sh && ls /root/backup/$(date +%F)/

实测回显:

anchr

注册 timer(每日 02:30 执行):

cat > /etc/systemd/system/anchr-backup.service <<'EOF'
[Unit]
Description=Anchr daily backup (MongoDB dump + attachments)

[Service]
Type=oneshot
ExecStart=/root/backup/anchr-backup.sh
EOF
cat > /etc/systemd/system/anchr-backup.timer <<'EOF'
[Unit]
Description=Run Anchr backup daily at 02:30

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true

[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now anchr-backup.timer
systemctl list-timers anchr-backup.timer --no-pager | head -3

实测回显:

Created symlink /etc/systemd/system/timers.target.wants/anchr-backup.timer → /etc/systemd/system/anchr-backup.timer.
NEXT                        LEFT   LAST PASSED UNIT               ACTIVATES
Thu 2026-10-29 02:30:00 UTC 22h left n/a  n/a   anchr-backup.timer anchr-backup.service

Persistent=true 让错过的执行(如关机时段)在开机后补跑。手动触发一次验证:systemctl start anchr-backup.service;oneshot 服务执行完 is-active 显示 inactive 属正常,成功与否看 journalctl -u anchr-backup 与备份目录产物。

四、版本升级

官方 Releases 无预编译产物,升级 = 换源码 + 重放构建链。先看当前版本与远端最新 tag:

cd /opt/anchr && git fetch --tags
git tag -l | tail -6

实测回显:

3.2.0
3.3.0
3.3.3
3.3.9
3.4.2
3.5.2
git describe --tags
3.5.2

升级到目标版本的完整动作(以 checkout 新 tag 为界,前四步照抄,<新版本号> 按 Releases 页替换):

cd /opt/anchr
systemctl stop anchr
git checkout <新版本号>
npm install
cd public && bower install --allow-root && cd ..
npm run build
systemctl start anchr

构建重放实测回显(3.5.2 上重放验证,节选):

Run `npm audit` for details.
usemin:js                      47ms  ▇▇ 5%
Total 972ms

升级后三连验证:

systemctl is-active anchr
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:8149/
journalctl -u anchr -n 5 --no-pager | grep -E "database connection|listening"

实测回显:

active
HTTP 200

升级前先跑一遍 3.1 的备份——构建链不会动数据,但回滚旧版本时 git checkout 旧 tag 即可,数据库始终向前兼容一个版本内的小版本。

五、API 常用命令(curl 实测)

Anchr 的 REST API 与网页端同源,全部接口挂 /api 前缀,鉴权用 Authorization: Bearer <JWT>。完整 swagger 定义见实例自带页面 /api-docs。以下示例账号为 admin@example.com / q1w2e3r4。

5.1 登录获取 Token

curl -s -X POST http://127.0.0.1:8149/api/auth/token \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"q1w2e3r4"}'

实测回显(token 为 204 字符 JWT,有效期 7 天):

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhbCI6eyJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIn0sInN0cmF0ZWd5IjoibG9jYWwiLCJpYXQiOjE3OTMxNzA2NjMsImV4cCI6MTc5Mzc3NTQ2M30.xK9fQ2mZp3vT5wE8rN1cY6bD0aL4hG7uJ2sV9oP3qXw"}

口令错误时的失败行为:

curl -s -X POST http://127.0.0.1:8149/api/auth/token \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"wrongpass"}' -w '\nHTTP %{http_code}\n'
{"error":"Wrong password.","status":401}
HTTP 401

后续命令统一先取 token 到变量:

TOKEN=$(curl -s -X POST http://127.0.0.1:8149/api/auth/token \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"q1w2e3r4"}' | grep -o '"token":"[^"]*"' | cut -d'"' -f4)

5.2 收藏集管理

列出全部收藏集(必须带分页参数,原因见第六节排障实录):

curl -s "http://127.0.0.1:8149/api/collection?page=1" -H "Authorization: Bearer $TOKEN"

实测回显:

[{"_id":"rPnUJ","name":"My shortlinks","shared":false,"size":2,"id":"rPnUJ"},{"_id":"CcRy4","name":"My images","shared":false,"size":1,"id":"CcRy4"},{"_id":"kJcOv","name":"运维手册","shared":false,"size":1,"id":"kJcOv"},{"_id":"qLybe","name":"Tech Links","shared":false,"size":1,"id":"qLybe"},{"_id":"mYNFi","name":"中文测试集","shared":false,"size":1,"id":"mYNFi"}]

创建收藏集:

curl -s -X POST http://127.0.0.1:8149/api/collection \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"D1 运维演示集"}' -w '\nHTTP %{http_code}\n'
{"_id":"0sHQL","name":"D1 运维演示集","links":[],"shared":false,"owner":"6ae169b60b0771ec4192d7ea","id":"0sHQL"}
HTTP 201

删除收藏集(连带其中全部书签):

curl -s -X DELETE http://127.0.0.1:8149/api/collection/0sHQL -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
HTTP 200

5.3 书签管理

向收藏集添加书签:

curl -s -X POST http://127.0.0.1:8149/api/collection/0sHQL/links \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"url":"https://docs.mongodb.com/manual/tutorial/backup-and-restore-tools/","description":"MongoDB Backup and Restore 官方手册"}' -w '\nHTTP %{http_code}\n'
{"url":"https://docs.mongodb.com/manual/tutorial/backup-and-restore-tools/","description":"MongoDB Backup and Restore 官方手册","date":"2026-10-28T04:04:13.916Z","_id":"6ae1743d9b83cc7448a49f5a"}
HTTP 201

按描述关键词搜索(q 参数):

curl -s "http://127.0.0.1:8149/api/collection/0sHQL/links?page=1&pageSize=20&q=MongoDB" -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
[{"url":"https://docs.mongodb.com/manual/tutorial/backup-and-restore-tools/","description":"MongoDB Backup and Restore 官方手册","date":"2026-10-28T04:04:13.916Z","_id":"6ae1743d9b83cc7448a49f5a"}]
HTTP 200

删除单条书签(_id 即上一步返回的 ObjectId):

curl -s -X DELETE http://127.0.0.1:8149/api/collection/0sHQL/links/6ae1743d9b83cc7448a49f5a -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
HTTP 200

5.4 短链:创建与跳转

curl -s -X POST http://127.0.0.1:8149/api/shortlink \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"url":"https://ubuntu.com/server/docs","description":"Ubuntu Server 文档"}'

实测回显:

{"url":"https://ubuntu.com/server/docs","_id":"XJ5mE","href":"http://10.89.73.2:8149/s/XJ5mE"}

href 即对外发布的短链(由 ANCHR_PUBLIC_URL 拼接,见 C1 篇 2.1)。用浏览器默认行为访问是 302 跳转:

curl -s -o /dev/null -w "HTTP %{http_code} -> %{redirect_url}\n" http://127.0.0.1:8149/s/XJ5mE
HTTP 302 -> https://ubuntu.com/server/docs

带 Accept: application/json 头则返回元数据:

curl -s http://127.0.0.1:8149/s/XJ5mE -H 'Accept: application/json' -w '\nHTTP %{http_code}\n'
{"_id":"XJ5mE","url":"https://ubuntu.com/server/docs","href":"http://10.89.73.2:8149/s/XJ5mE","id":"XJ5mE"}
HTTP 200

5.5 Prometheus 指标

开启 ANCHR_EXPOSE_METRICS=true 后(见 C1 篇 2.6):

curl -s http://127.0.0.1:8149/api/metrics | head -3
# HELP anchr_process_cpu_user_seconds_total Total user CPU time spent in seconds.
# TYPE anchr_process_cpu_user_seconds_total counter
anchr_process_cpu_user_seconds_total 0.927621

六、排障实录

6.1 短链创建全部报 400 "not safe"(3.5.2 真实缺陷,含定位与处置)

现象:POST /api/shortlink 无论什么 URL 一律失败:

{"error":"The link you try to reference is not safe!","status":400}

定位过程(全程源码 + 复现):

  1. 拦截逻辑在短链创建前运行 3 个黑名单检查器(启动日志 Enabled link safety checking with 3 checkers),实现在 app/services/linkcheck.js:任一检查器命中即拒绝;
  2. 逐个复现 checkBulk 判定,uBlock 与 PhishingArmy 两个检查器对 github.com、ubuntu.com、mongodb.com 等正常站点全部返回"恶意";
  3. uBlock 检查器基于 @brigand/abp-filter-parser 解析 badware 列表,逐行定位命中规则——列表中存在多条形如下方的规则:
||com^$doc,ipaddress=206.82.7.123
||com/$doc,ipaddress=/^(172\.240\.(108\.(68|76|84)|127\.234|253\.132)|185\.246\.188\.125|1...

该类规则本意是"限定来源 IP 的 .com 拦截",但这个 2016 年的解析库不认识 ipaddress= 修饰符,直接把 ||com^ 当裸模式匹配——任何 .com 域名全部命中;

  1. PhishingArmy 检查器用子串匹配(url.includes(pattern)),实测从当前列表快照中过滤出命中模式,结果只有一个字符长度为 2 的垃圾条目:
github hits: ["co"]

列表混入的 co 条目让任何包含 "co"(包括所有 .com 域名)的 URL 命中。

根因:两个上游黑名单数据各自携带脏数据(ipaddress= 新语法 + co 垃圾条目),叠加 Anchr 3.5.2 未识别新语法的老解析库与无长度的子串匹配,短链功能在当前数据快照下整体不可用。

处置(清洗数据文件后重启;数据文件位于 /tmp/,由应用启动时下载/加载):

sed -i '/ipaddress=/d' /tmp/filters_badware.txt
sed -i '/^co$/d' /tmp/filters_phishingarmy.txt
systemctl restart anchr

重测创建:

{"url":"https://github.com/muety/anchr","_id":"ya1mU","href":"http://10.89.73.2:8149/s/ya1mU"}
HTTP 201

注意事项:检查器数据默认每日 00:10 自动重新下载(cron 表达式由 ANCHR_LINKCHECK_UPDATE_CRON 控制,见 C1 篇 2.5),上游数据修复前清洗需按日重做,或把该 cron 表达式改长(如 0 3 * * 1 每周一)降低复发频率。此缺陷不影响书签收藏与图片上传,仅影响短链创建。

6.2 直连 API 列表接口报 500(分页参数缺失)

现象:GET /api/collection/<id>/links 不带查询参数时返回:

{"error":"PlanExecutor error during aggregation :: caused by :: Second argument to $slice can't be represented as a 32-bit integer: -nan","status":500}

根因是 3.5.2 对 page 参数的处理在缺失时得到 NaN 并传入 MongoDB $slice(完整分析见 C1 篇 4.1)。解法:请求永远显式携带 page=1&pageSize=20——本篇所有 API 示例均遵循此约定。

6.3 备份 timer 手动触发后 is-active 显示 inactive

现象:systemctl start anchr-backup.service 后 systemctl is-active 返回 inactive,误以为失败。这是 Type=oneshot 服务的正常表现——执行完即退出。确认成功与否:

journalctl -u anchr-backup -n 5 --no-pager
ls /root/backup/$(date +%F)/

产物目录存在且含 anchr/ 子目录与 -files.tar.gz 即为成功。

七、运维命令速查表

场景命令
看状态systemctl status anchr --no-pager
停/起/重启systemctl stop/start/restart anchr
看日志journalctl -u anchr -n 20 --no-pager / -f
备份数据库mongodump --uri="mongodb://anchr:...@localhost:27017/anchr" --out /root/backup/<日期>
恢复数据库mongorestore --drop --uri=... /root/backup/<日期>/anchr/
备份附件tar -czf <日期>-files.tar.gz -C /var/data anchr
查看备份计划systemctl list-timers anchr-backup.timer
升级git fetch --tags && git checkout <tag> → npm install → bower install → npm run build → restart
API 登录POST /api/auth/token
列收藏集GET /api/collection?page=1
建/删短链跳转POST /api/shortlink / GET /s/<id>

系列阅读:部署全过程见 《Anchr 安装:Ubuntu 22.04 源码构建与 systemd 服务化》,配置项逐条说明见 《Anchr 配置:.env 全参数与 systemd 单元详解》,功能定位与竞品对比见 《Anchr 认知选型:三合一轻量书签工具箱的定位、活跃度与替代方案》。