Files
blog/docs/架构精简候选.md
T
zqlit 3eeafa07b2 fix(证书管家): writeapi.usj.cc 证书纳管(相对软链接);pushall 改为先 rebase 再推
用户:「动手 推送吧」—— 动手处理两个有死线的遗留。

一、writeapi.usj.cc 的证书(12 月会断)→ 已解决

现状复核推翻了上一轮的判断:
  · 上一轮记的「它的 ssl_certificate 引用不在 /www/sites/ 树里」是**错的** ——
    /www/conf.d/writeapi.usj.cc.conf 里明确写着
    ssl_certificate /www/sites/writeapi.usj.cc/ssl/fullchain.pem;
  · 真正原因是**它在 1Panel 面板上根本不是一个网站**(面板共 11 个站点,没有它),
    所以 `findWebsite('writeapi.usj.cc')` 必然抛「找不到网站」→
    加进 one_panel_sites 只会让**每次续期都报错**(而且只警告不阻断,长期没人发现)
  · 实测确认:redeploy 时其余 5 个站点正常,只有它失败

解法:**不写一行代码** —— 它与 artalk.usj.cc 本来就同用一张 *.usj.cc 证书,
让它用**相对软链接跟随 artalk** 即可,随续期自动更新:
  ln -sfn ../../artalk.usj.cc/ssl/fullchain.pem /1panel/1panel/www/sites/writeapi.usj.cc/ssl/fullchain.pem

★★ 踩到的坑:软链接**必须相对路径**
  宿主 /1panel/1panel/www/sites/ ↔ 容器内 /www/sites/(1Panel 的 openresty 跑在容器
  `1Panel-openresty-Kmh2`)。第一版写成宿主绝对路径 → **容器侧断链** →
  nginx -s reload **静默失败**(保留旧配置、服务不中断、握手照旧拿旧证书)→
  表面全绿,真实后果是「等某天 nginx 重启,writeapi 的 HTTPS 直接挂」。
  唯一线索是 worker 进程的启动时间没变 —— **所以验收必须看 worker 是否真的重启**。

验收(全部实测):
  替换前 CN=usj.cc    / LiteSSL RSA CA / 2026-12-07(mtime 10-04 21:50)
  替换后 CN=*.usj.cc  / LiteSSL ECC CA / 2027-01-04
  容器内 head -1 → -----BEGIN CERTIFICATE----- / -----BEGIN PRIVATE KEY-----
  预检 syntax is ok / test is successful
  reload → signal process started;**worker 22:51:39 重启**(证明软链接真被读了)
  握手 SNI=writeapi.usj.cc → *.usj.cc / Jan 4 2027;实访 /health → HTTP 200

新增 deploy/cn-certkeeper/src/dump-cert.mjs:把 KV 里(AES-GCM 加密)的证书导成明文 PEM,
补上「KV ↔ 手工 vhost 磁盘文件」之间原先不存在的桥。只读,用完即删(含明文私钥)。

操作层的两个坑(写进文档):
  · 1Panel 文件 API **拒绝写 .mjs**(返回 500「目标路径不存在」,实为可执行扩展名过滤);
    往新建的 root:root 700 目录写也会失败 → 临时脚本用
    `docker exec -i … sh -c 'cat > 路径' < 本地文件` 送
  · cnrun.py(1Panel 计划任务通道)偶发「退出码 0 但零输出」;
    加 `timeout 30 docker exec …` 包一层即稳定,判断状态优先看容器内直接证据

二、配置回退 + 文档更正

  · certkeeper-config.mjs:把 writeapi 从 one_panel_sites 移除,换成一段注释说明
    「它不是 1Panel 站点,别加进来」+ 软链接做法 + 必须相对的警告;
    线上 config.kv 同步回退(sha256 与改动前完全一致 ccac2530…)
  · 更正「3 个手工 vhost」的说法:vaultwarden **是**正常纳管的站点(面板 #13,
    主域名 vw.usj.cc,别名才是 vaultwarden)→ 手工 vhost 实际只剩 writeapi + dnsapi
  · dnsapi.usj.cc 重新定性为**不做**:它是 cn-dns-helper 的反代入口
    (proxy_pass 127.0.0.1:8018,仅 HTTP),去留应与 cn-dns-helper 一起决定,
    不该单独给一个待退役的服务加 HTTPS

三、pushall 加固:先 fetch + rebase 再推(本次真实撞到的问题)

推 CNB 时被 rejected —— 因为线上写作后台(editor-api)发文章会**直接推 main**,
本机两笔提交与之分叉。这不是异常,是**日常**。原别名直接 push,必然反复撞。

  · 新增 scripts/pushall.sh:fetch → 已在远端之后则 rebase → 推所有远端
    - 冲突时**停在 rebase 中途**交人工(不强推、不丢东西)
    - 工作区不干净时 git 自己会拒绝 rebase(不会吞改动)
    - 任一远端失败**不改判另一个**(主仓失败辅仓照样推),退出码以第一次失败为准
    - ⚠️ 修了自己写的一处疏漏:`if ! cmd; then ec=$?` 的 $? 是**取反后**的结果(0),
      不是 git 的退出码 —— 必须显式 ec=1(靠 set -o pipefail 保证管道退出码不被 sed 掩盖)
  · setup-cnb-remotes.sh 的别名改指向脚本;README 脚本表补这一行

四、文档

  · docs/证书管家.md:速览表「已知遗留」→ 已解决;§9.14 遗留段重写(含纠正自己写错的判据
    「路径像不像不构成判据,要直接查面板站点清单」);**新增 §9.15**
    「手工 vhost 的证书:用相对软链接跟随已纳管站点」(机制 + 路径坑 + 三条验收 + dump-cert 用法)
  · docs/架构精简候选.md:候选 5 标记完成,补上「为什么 A 走不通、B 未采用」;
    执行顺序里划掉它
  · 架构总览.md:§5.7「已知遗留」重写(2 个手工 vhost + vaultwarden 更正);
    §6 待办 #13 改为已解决、#14 标注完成
2026-10-06 22:54:58 +08:00

167 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构精简候选
> **用途**:本文是**待决策清单**,不是现状描述。现状看 [`../架构总览.md`](../架构总览.md)。
> 每条都是「已确认可以动、但需要你拍板」的项,附收益 / 代价 / 风险 / 怎么做。
>
> 更新时间:2026-10-06
---
## 一、复杂度的真实来源:不是「件数多」,是「跨了 6 个环境」
| 环境 | 谁在维护 | 上面跑什么 | 是否必需 |
|---|---|---|---|
| **腾讯云 CNB** | 托管 | 代码主仓 + 构建 + 发布(7 个 stage) | ★ 必需(发布的骨架) |
| **Cloudflare** | 托管 | 评论后端 + RSS + 证书只读面 + D1/KV + 200181.xyz 的 DNS | ★ 必需(零运维) |
| **国内机 `119.29.215.187`** | **自己** | 9 个容器(见下)+ 一堆手工 vhost | 部分必需 |
| **境外 VPS `23.254.236.47`** | **自己** | Gitea(代码辅仓) | ⚠️ **2026 年 11 月到期** |
| **家庭 F50 / OpenList** | **自己** | 离线加密备份 | ★ 必需(唯一的离线层) |
| **本机 Windows** | **自己** | 备份计划任务、本地写作前端 | 半必需 |
> **四台自维护设备 = 复杂度的真正来源**,而不是「用了几个云服务」。
> 托管服务再多也不累人(出问题找平台),自己维护的机器每一台都要有人记得它。
国内机 9 个容器里,**属于本项目的只有 3 个**:
| 容器 | 归属 | 状态 |
|---|---|---|
| `editor-api` | 本项目 | ✅ 写作后台的线上发布入口 |
| `cn-certkeeper` | 本项目 | ✅ 证书签发 + 部署 |
| `cn-dns-helper` | 本项目 | ⚠️ **疑似遗留**(见候选 2) |
| `1Panel-certimate-OKCO` | 历史遗留 | ⚠️ **可清**(见候选 1) |
| `1Panel-openresty` / `-mysql` | 用户的 | 面板与站点 |
| `1Panel-alist` / `vaultwarden` / `1Panel-frps` | 用户的 | 与本项目无关 |
→ **能动的只有两个**,而且都很干净。
---
## 二、精简候选
### 候选 1 ★ certimate 容器 —— 停掉 `1Panel-certimate-OKCO`
| | |
|---|---|
| **现状** | 容器还在跑(`certimate/certimate:v0.4.32`)。但 **7 个工作流里「会签发并部署」的 2 条已停用**(`enabled=0`),只剩 3 条纯监控告警 |
| **为什么可以停** | 本项目已**完全接管**签发与部署。certimate 现在唯一的作用是「万一要回滚」,而它的凭据早就导出到 `secrets-backup/`,库文件也有 `.bak-*` |
| **收益** | 少一个常驻容器(内存是这台机器的短板:总 1967 MB / 已用 791 MB);少一个「静默竞争」风险源 |
| **风险** | 低。但**告警能力会消失** —— 三条监控工作流会随之失效。而本项目的证书监控已由 `cn-certkeeper` 的 `/status` + Worker 后台接管,**功能不重叠** |
| **怎么做** | `docker stop 1Panel-certimate-OKCO` → 观察 1~2 周(覆盖一次 04:10 自动续期 + 一次证书到期检查)→ 确认无碍再考虑删容器与数据卷。**别急着删**,先 stop |
> ⚠️ 若将来要停用,先确认「证书到期提醒」这一层有人接手(现在是 Worker 后台 + 探针)。
---
### 候选 2 ★ `cn-dns-helper` 容器 —— 大概率已是遗留
| | |
|---|---|
| **它是什么** | 「DNS-01 助手」:接受 `POST /dns/txt`,代调用有 DNS 权限的 Cloudflare API 写 TXT 记录 |
| **当初为什么有** | Phase 1 时代**签发跑在 CF Worker 里**,而 Worker 那份 CF token 是 Workers/KV/D1 专用的(没有 Zone/DNS 权限)→ 只好让国内机代写 |
| **为什么现在不需要** | ★ 签发已搬到 `cn-certkeeper`(国内机),它**自带 dnsprovider**,直接调腾讯云 DNSPod / Cloudflare API。实测 `/preflight` **7/7 全过**,其中 `cloudflare → 200181.xyz` 报「**可读**」= 这份凭据有 Zone 权限 → **不需要中转** |
| **代码侧证据** | `deploy/cn-certkeeper/lib/dnsprovider.js` 里 `remote` 型是**兜底分支**(注释写明「用于 CF token 无 DNS 权限的场景」);Worker 侧 `dnsremoted.ts` **已无任何引用** |
| **收益** | 少一个常驻容器 + 少一个对外 HTTP 端点(虽然只绑回环) |
| **风险** | 低,但**要实测确认**:万一某条域名的 DNS-01 仍走 `remote`,停掉会让那次续期失败 |
| **怎么验证** | ① 查 `cn-certkeeper` 的凭据里有没有 `remote` 型(`/preflight` 的 dns 项只列了 `tencent-usj` / `tencent-tt` / `cloudflare`,**没有 remote**)② 停容器 → 手动跑一次单域续期(`renew-one.mjs`)→ 成功即可判死 |
| **结论** | **建议停掉**,跑一次续期验证 |
---
### 候选 3 Gitea —— 迁到新机,还是干脆不留
**这是本轮最大的一刀**,完整分析见 [`Gitea迁移指南.md`](Gitea迁移指南.md) 第九节。摘要:
| 选项 | 收益 | 代价 |
|---|---|---|
| **A. 迁到新机** | 保留「在线可 clone、可按提交追溯、不受平台规则约束」的副本 | 继续养一台机器 + 一个要运维的实例(**且 Gitea 不参与构建、不参与发布**) |
| **B. 不迁,去掉辅仓** | 少一台机器、少一个部件,架构回到 **CNB 主仓 + F50 离线加密 bundle** 两层 | 少一层在线冗余(但离线层是**完整历史**,且已带失败告警) |
| C. 换托管平台私有仓 | 不用自己运维 | CNB 已是托管平台,再挂一家同性质的收益有限 |
**判断依据**:Gitea 现在挡的是「CNB 跟 GitHub 一样出问题」——
但**离线 bundle 已经覆盖了这个场景**。Gitea 多出来的独有价值只有
「**在线可浏览**」与「**可增量拉取**(不用等一天一次的备份)」。
这两点用不上,**B 就是合理的简化**。
> 若选 B:`deploy/gitea/` 留在仓库里即可(部署包与迁移清单留着,随时能重建),
> 然后按 `Gitea迁移指南.md` 第三节把那 9 处 `gitea` 引用清掉 ——
> **别忘了线上那两处**(`/srv/editor-api/.env`、`/srv/blog` 的 remote)。
---
### 候选 4 写作前端两套并存(`write-server/` vs `write/`)
| | |
|---|---|
| **现状** | `write-server/`(Next.js,线上 `post.usj.cc`)+ `write/`(本地 Windows 前端),**架构总览已注明「功能重叠」** |
| **另外** | `editor-api/`(国内机)才是**真正的那条发布入口** —— 后台发的文章走它 |
| **收益** | 少维护一套前端;文档里的「写作系统」不再需要解释三个东西的关系 |
| **风险** | 取决于你实际用哪个。若两套都在用,就不是「精简」而是「迁移」 |
| **建议** | 先确认实际使用频率,再决定砍哪个。**本轮不动** |
---
### 候选 5 手工 vhost 的证书(不是精简,是收尾)—— ✅ **已完成**(2026-10-06)
`writeapi.usj.cc` 的证书**没被本项目纳管**,实测当时是 **RSA / 到期 2026-12-07** → **12 月会断**。
**实际采用的第三条路(比下面 A/B 都省)**:它和 `artalk.usj.cc` **本来就同用一张
`*.usj.cc` 证书**,所以直接让它**用相对软链接跟随 artalk** 即可 ——
零代码、零新增凭据、随续期自动跟随:
```bash
D=/1panel/1panel/www/sites/writeapi.usj.cc/ssl
ln -sfn ../../artalk.usj.cc/ssl/fullchain.pem "$D/fullchain.pem"
ln -sfn ../../artalk.usj.cc/ssl/privkey.pem "$D/privkey.pem"
```
> ⚠️ **必须用相对路径**:宿主是 `/1panel/1panel/www/…`、容器内是 `/www/…`,
> 写绝对路径会在容器侧**断链**,而断链后 `nginx -s reload` 会**静默失败**
> (服务不中断、握手照旧)—— 直到某天 nginx 重启才发现 HTTPS 挂了。
> 验收必须看 **worker 进程是否真的重启**,不能只看握手。
> 完整机制与排错见 [`证书管家.md`](证书管家.md) §9.15。
结果:`CN=usj.cc`/RSA/2026-12-07 → **`CN=*.usj.cc`/ECC/2027-01-04**,容器内可读、
预检通过、worker 已重启、`/health` 200。
当时考虑过的另外两条路(保留备查):
- **A. 让 cn-certkeeper 覆盖它**:把它纳入部署目标 —— ❌ **走不通**:它**不是 1Panel 站点**
(面板 11 个网站里没有它),`findWebsite('writeapi.usj.cc')` 必然抛「找不到网站」
- **B. 在 1Panel 里把它建成正式站点**,自然被 `ssl/upload` + `sslID` 覆盖 ——
技术上可行,但要动生产 conf(`client_max_body_size 30m`、`proxy/*.conf` 都得迁),
风险与本收益不成比例,**未采用**
另一个手工 vhost `dnsapi.usj.cc` **不做**:它是 `cn-dns-helper` 的反代入口(`127.0.0.1:8018`,仅 HTTP),
去留应与候选 2(`cn-dns-helper`)一起决定。
详见 [`../架构总览.md`](../架构总览.md) §6 待办 #13。
---
## 三、明确**不建议**动的(必要复杂度)
| 项 | 为什么保留 |
|---|---|
| **又拍云 + 多吉云 + EdgeOne 三个分发目标** | 境内/境外双线路是业务需求(境内走又拍云→多吉云,境外走 EdgeOne),不是历史堆积 |
| **Cloudflare Workers 全家(评论 + RSS + 证书只读面)** | 零服务器零运维,是本项目**最省事**的一层 |
| **国内机的 `editor-api`** | 写作后台的发布入口,没有替代品(CF Worker 跑不了 git push) |
| **国内机的 `cn-certkeeper`** | CF Worker 免费版 CPU 硬顶 10ms,签发跑不动;用户已否决 CF Paid(比这台机器还贵) |
| **家庭 F50 上的离线备份** | 唯一不依赖任何平台账号的层 |
| **三层留存** | 三层**失效模式不同**,不是重复(见 `架构总览.md` §5.3) |
---
## 四、建议的执行顺序
```
✅ 已完成(2026-10-06)候选 5 —— writeapi.usj.cc 证书纳管(相对软链接跟随 artalk,见上)
1. 【本周】候选 1 —— stop certimate 容器(零风险,观察即可)
2. 【本周】候选 2 —— 停 cn-dns-helper + 跑一次续期验证(大概率能省一个容器;
顺带决定 dnsapi.usj.cc 这个反代入口的去留)
3. 【11 月前】候选 3 —— Gitea:迁 or 不留(★ 必须决定,机器要到期了)
4. 【有空】候选 4 —— 写作前端收敛(先看使用频率)
```
> 前三项做完,国内机上属于**本项目**的容器从 3 个降到 1 个(只剩 `editor-api`),
> 自维护环境从 6 个降到 5 个(去掉境外 VPS)。
> **这才是「感觉复杂」的真正解药 —— 减的是「要记得它」的东西,不是「存在于架构图里」的东西。**