mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
4645 字
12 分钟
博客国内外双节点部署与全链路架构解耦
2026-05-19

特别鸣谢:感谢山姆·奥特曼提供了强大的 GPT-5.5,让我得以借助 OpenAI Codex 的卓越辅助在短短半天时间内就完成了整套复杂的双节点部署与全链路域名解耦调试。(奥特曼的 GPT 真是太好用了,重点是居然还有路子白嫖,实在是太感谢他了 🥳)

最近抽空把博客的部署架构重构了一下。原来的站点全部挤在国内单机上,海外访问延迟较高;且虽然图床等静态资源一直都在独立的 xxx.online 域名(img.xxx.online)下,没有进行过迁移,但音乐资源子域(music.dawn114514.site)依然绑定在主域名 dawn114514.site 下,导致如果将主域名从 Cloudflare DNS 迁出,静态音乐资源的解析就会随之失效。

经过这次重构,我将博客升级为国内外双机房就近接入 + 音乐子域解耦迁移 + GitHub Actions 自动化发布的架构。在此过程中扫平了关于 SSH 密钥、Nginx 虚拟主机配置以及网络防火墙的各种隐性坑。这里做个完整的复盘和配置细节记录,方便以后查阅。


改造时间线与实施甘特图#

整个重构过程虽然断断续续,但大致可以分为六个阶段。以下甘特图呈现了各阶段的时间推进关系:

gantt title 博客双节点部署改造时间线 (2026-05-19) dateFormat YYYY-MM-DD HH:mm axisFormat %H:%M section 音乐子域解耦迁移 R2 自定义域名绑定 :done, r2-1, 2026-05-19 13:45, 13m 代码中音乐链接全量替换 :done, r2-2, 2026-05-19 13:59, 5m CORS 策略配置与验证 :done, r2-3, 2026-05-19 14:04, 2m section CI/CD 改造 双端部署 Workflow 编写调试 :done, ci-3, 2026-05-19 14:05, 45m 生成免密 SSH 密钥对 :done, ci-1, 2026-05-19 14:09, 35m GitHub Secrets 配置 :done, ci-2, 2026-05-19 14:44, 4m section 海外服务器 Nginx 安装与站点配置 :done, os-1, 2026-05-19 14:25, 3m iptables + OCI 防火墙放行 :done, os-3, 2026-05-19 15:02, 53m Certbot SSL 证书签发 :done, os-2, 2026-05-19 15:49, 3m section DNS 迁移 域名迁回阿里云 DNS :done, dns-1, 2026-05-19 15:06, 9m 分线路智能解析配置 :done, dns-2, 2026-05-19 15:15, 28m section 国内服务器优化 Nginx 配置审计与整理 :done, cn-2, 2026-05-19 15:56, 15m TLSv1.1 协议移除 :done, cn-1, 2026-05-19 16:08, 2m section 验证与文档 全链路连通性测试 :done, verify-1, 2026-05-19 14:50, 65m 撰写部署文档与博客 :done, verify-2, 2026-05-19 16:13, 12m

最终网络架构与流量分流拓扑#

重构后的系统分为以下四个层次,各层之间职责明确:

  1. 解析分流层:利用阿里云 DNS,对国内和境外流量做智能路由。
  2. 静态资源存储层:将原本绑定在主域名下的音乐子域(music.dawn114514.site)也解耦迁移至独立的 xxx.online 域名(music.xxx.online)代理,与图床一起彻底实现静态资源与主域名的物理隔离,不再占用主站带宽。
  3. Nginx 接入层:国内和海外节点各自部署 Nginx 并终结 SSL 证书,国内机房额外承载子系统反代。
  4. CI/CD 管线:代码提交至 GitHub 后,由 Actions 编译并多线程同步投递至两台机器。
架构拆分原则

职责单一,解耦至上。无论未来业务节点如何扩充,四层分工的核心原则都应该保持稳定:DNS 负责路由分流,Nginx 负责反代接入,GitHub Actions 负责构建与同步,Cloudflare R2 负责静态数据持久化。

具体的流量和自动部署走向可以参考下图:

flowchart TD %% 流量流向 User[用户访问 dawn114514.site] --> DNS{Alibaba Cloud DNS <br>智能分流} %% 国内分流 DNS -->|中国内地/默认| CN_Node[国内 ECS 服务器 <br>IP: xxx.xxx.xxx.xxx] CN_Node --> CN_Nginx[Nginx + SSL <br>主站目录: /www/wwwroot/MyBlog] CN_Nginx --> CN_Subs[子系统反代 <br>自建服务 A / 自建服务 B] %% 海外分流 DNS -->|中国境外| OV_Node[海外 Oracle 服务器 <br>IP: yyy.yyy.yyy.yyy] OV_Node --> OV_Nginx[Nginx + SSL <br>主站目录: /www/wwwroot/MyBlog] %% 静态资源解耦 CN_Nginx -.->|引用外链| R2_Music[music.xxx.online <br>Cloudflare R2 音乐源] OV_Nginx -.->|引用外链| R2_Music CN_Nginx -.->|引用外链| R2_Img[img.xxx.online <br>Cloudflare R2 图床] OV_Nginx -.->|引用外链| R2_Img %% 自动化持续集成管线 Developer[开发者 git push] --> GH_Repo[GitHub 博客源码仓库] GH_Repo --> GH_Actions[GitHub Actions 构建管线] GH_Actions -->|编译产物 rsync 同步| CN_Node GH_Actions -->|编译产物 rsync 同步| OV_Node

GitHub Actions 双端部署的详细时序交互如下:

sequenceDiagram participant Dev as 开发者 participant GH as GitHub 仓库 participant CI as GitHub Actions Runner participant CN as 国内 ECS (Aliyun) participant OS as 海外 Oracle Dev->>GH: git push main 分支 GH->>CI: 触发 deploy.yml 工作流 CI->>CI: pnpm install && pnpm build Note over CI: 构建产物生成于 dist/ 目录 par 并行部署到国内 CI->>CI: 写入 DEPLOY_SSH_KEY_CN CI->>CN: ssh-keyscan 获取主机指纹 CI->>CN: mkdir -p /www/wwwroot/MyBlog/dist CI->>CN: rsync -az --delete dist/ CN-->>CI: 同步完成 and 并行部署到海外 CI->>CI: 写入 DEPLOY_SSH_KEY_OVERSEA CI->>OS: ssh-keyscan 获取主机指纹 CI->>OS: mkdir -p /www/wwwroot/MyBlog/dist CI->>OS: rsync -az --delete dist/ OS-->>CI: 同步完成 end Note over CI: 双端部署全部成功

改造步骤与细节#

1. 音乐子域解耦与迁移(Cloudflare R2)#

在此之前,我的图床静态资源其实一直托管在独立的 xxx.online 域名(img.xxx.online)下,并未进行迁移。但是,我的音乐多媒体资源却仍然直接绑定在主域名的子域 music.dawn114514.site 下。这就导致如果我想把主域名 dawn114514.site 的 Nameservers 从 Cloudflare DNS 切走,静态音乐资源的解析就会直接跟着完蛋(因为 R2 绑定自定义域名需要域名托管在 CF)。

解决办法: 为了彻底将静态资源与主域名剥离,我决定将音乐子域名迁移至独立托管在 Cloudflare 上的 xxx.online 之下(即 music.xxx.online),从而在域名层面实现完美的物理隔离。

在 Cloudflare 控制台中,为该域名的子域添加指向 Cloudflare R2 桶的 CNAME 记录,并开启 CF Proxy 代理:

img.xxx.online. 1 IN CNAME pub-xxxxxxxxxxxxxxxxxxxxxxxx.r2.dev. (开启 CF-Proxied)
music.xxx.online. 1 IN CNAME pub-xxxxxxxxxxxxxxxxxxxxxxxx.r2.dev. (开启 CF-Proxied)

随后,在博客内容仓库中进行一次全量正则替换,将所有旧的资源引用替换为新的解耦域名:

# 将旧的引用地址清洗为 music.xxx.online
find . -type f -name "*.md" -exec sed -i 's/music.dawn114514.site/music.xxx.online/g' {} +

对于代码仓库中的硬编码资源路径(如音乐播放器组件),同样需要做全量替换。我的音乐播放器常量文件 src/components/widgets/music-player/constants.ts 中 32 首歌曲的资源 URL 均从 https://music.dawn114514.site/... 迁移至统一的 ${MUSIC_BASE_URL}/... 模式,其中 MUSIC_BASE_URL 定义为 https://music.xxx.online

资源物理隔离原则

切记:主站的归主站,资源的归资源。主站域名和资源域名尽量分开,不要交叉使用同一个顶级域名。资源独立出去之后,后续主站不管怎么折腾 DNS 或更换云厂商,文章里的静态资源链接都不需要动。

R2 CORS 跨域配置#

域名迁移后还需配置 CORS 策略,确保浏览器端对 R2 桶的跨域请求不被拦截。在 Cloudflare R2 控制台中为两个存储桶分别添加如下 CORS 规则:

{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": [],
"MaxAgeSeconds": 3600
}
音乐桶的原有自定义域名

原本绑定在音乐桶上的 music.dawn114514.site 自定义域名在迁移完成后即可删除 —— 因为此时所有引用地址已经改为 music.xxx.online,旧域名不再有任何流量。

截图预留:Cloudflare R2 自定义域名绑定成功截图

2. DNS 大陆/境外智能解析分流#

主域名 dawn114514.site 迁离 Cloudflare,Nameservers 改为阿里云的 dns19.hichina.comdns20.hichina.com

在阿里云云解析后台,我对国内和国外线路做了 A 记录拆分,配置如下:

主机记录记录类型解析线路记录值TTL
@A默认 (兜底必配)xxx.xxx.xxx.xxx600s
@A中国内地xxx.xxx.xxx.xxx (国内 ECS)600s
@A中国境外yyy.yyy.yyy.yyy (境外 Oracle)600s
wwwA默认xxx.xxx.xxx.xxx600s
wwwA中国内地xxx.xxx.xxx.xxx600s
wwwA中国境外yyy.yyy.yyy.yyy600s

阿里云云解析 DNS 分线路控制台配置截图。

DNS 配置误区

配置智能解析时,“默认”解析线路是绝不可省的。如果只添加了”中国内地”和”中国境外”,当遇到某些无法识别来源的特定边缘节点请求时,可能会因为没有匹配记录而返回解析失败。默认线路直接解析到国内 ECS 即可。

为什么不用 Cloudflare DNS 分流?

虽然 Cloudflare 也有基于 智能地域解析 的流量导向功能,但阿里云 DNS 在中国大陆的分线路解析更精细(可按运营商、省份等粒度拆分)。对于国内/境外双线这种场景,阿里云的线路拆分能力完全是免费可用的,不需要额外付费。


3. GitHub Actions 自动化双端同步发布#

内容发布如果靠手动同步肯定会出问题。用 GitHub Actions 写个简单的 Workflow 就能解决单次构建、双端同步的问题。

Dawn6666666
/
MyBlog
Waiting for api.github.com...
00K
0K
0K
Waiting...

部署工作流的核心配置模板:

name: Deploy to Server
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build site
run: pnpm build
- name: Deploy to dual servers
env:
DEPLOY_PORT: 22
DEPLOY_DIR: /www/wwwroot/MyBlog
run: |
set -euo pipefail
deploy_one() {
local name="$1"
local host="$2"
local user="$3"
local key="$4"
if [ -z "$host" ] || [ -z "$user" ] || [ -z "$key" ]; then
echo "Missing deploy secret(s) for $name"
exit 1
fi
echo "====== Deploying to $name ($host) ======"
mkdir -p ~/.ssh
chmod 700 ~/.ssh
printf '%s\n' "$key" > ~/.ssh/deploy_key_"$name"
chmod 600 ~/.ssh/deploy_key_"$name"
# 扫描远端指纹,写入 SSH 信任名单,防 CI 脚本卡住
ssh-keyscan -p "$DEPLOY_PORT" -H "$host" >> ~/.ssh/known_hosts
# 确保远端主目录存在
ssh -i ~/.ssh/deploy_key_"$name" -p "$DEPLOY_PORT" "$user@$host" \
"mkdir -p '$DEPLOY_DIR/dist'"
# 使用 rsync 精准对齐远程静态目录
rsync -az --delete \
-e "ssh -i ~/.ssh/deploy_key_$name -p $DEPLOY_PORT" \
--exclude=".user.ini" \
--exclude=".well-known" \
dist/ "$user@$host:$DEPLOY_DIR/dist/"
echo "====== Success Deploy to $name ======"
}
deploy_one "CN" "${{ secrets.DEPLOY_HOST_CN }}" "${{ secrets.DEPLOY_USER_CN }}" "${{ secrets.DEPLOY_SSH_KEY_CN }}"
deploy_one "OVERSEA" "${{ secrets.DEPLOY_HOST_OVERSEA }}" "${{ secrets.DEPLOY_USER_OVERSEA }}" "${{ secrets.DEPLOY_SSH_KEY_OVERSEA }}"

GitHub Actions 双端同步成功的多线程运行日志截图。

GitHub Secrets 配置清单#

双端部署需要的配置项总量并不多,关键是区分好国内和海外两套凭证:

Secret 名称说明示例值
DEPLOY_HOST_CN国内服务器 IPxxx.xxx.xxx.xxx
DEPLOY_USER_CN国内服务器 SSH 用户admin
DEPLOY_SSH_KEY_CN国内服务器免密私钥(完整内容)-----BEGIN OPENSSH PRIVATE KEY-----...
DEPLOY_HOST_OVERSEA海外服务器 IPyyy.yyy.yyy.yyy
DEPLOY_USER_OVERSEA海外服务器 SSH 用户ubuntu
DEPLOY_SSH_KEY_OVERSEA海外服务器免密私钥(完整内容)-----BEGIN OPENSSH PRIVATE KEY-----...

GitHub Actions Secrets 秘钥配置管理面板截图。

为什么 DEPLOY_DIR 和 DEPLOY_PORT 不作为 Secret?

端口固定 22、部署目录固定 /www/wwwroot/MyBlog,这两项属于基础设施约定而非敏感信息。将它们硬编码在 Workflow 中有助于减少 Secret 数量,降低配置出错的概率。如果需要灵活性,可以在 Workflow 文件中以环境变量形式定义。

坑 1:国内机密钥带 Passphrase 导致部署失败与 CI 挂起#

我原来图省事,直接拷贝了国内阿里云服务器上现有的私钥文件(~/.ssh/id_ed25519)内容并填入 GitHub Secrets 的 DEPLOY_SSH_KEY_CN。然而,在 Actions 执行部署时,一直卡住并最终报错失败。(当时前 3 次全部红灯报错,连旁边放着的咖啡都急凉了 ☕)

后来在服务器本地自测该私钥才发现,原来这把密钥在初次生成时被设置了密码保护。在 GitHub Actions 这种非交互式的 CI 自动化环境下,SSH 握手因为需要输入密码而无法继续,最终导致部署直接被拒或挂死。

非交互式环境下的 SSH 陷阱

GitHub Actions Runner 是一个完全无头的自动执行环境。当 SSH 私钥设置了密码保护后,SSH 客户端会尝试弹出密码提示框或读取终端 /dev/tty。在 CI 中这完全行不通,最终会导致进程因为等待凭证输入而直接失败或无限挂起。因此,任何写入 GitHub Secrets 用于自动部署的 SSH 私钥,都必须是完全免密的

解决办法:重新在本地生成一对免密的专属 Ed25519 秘钥对,把公钥追加到服务器的授权名单中:

Terminal window
# 本地生成免密 Ed25519 秘钥对(PowerShell)
ssh-keygen -t ed25519 -f $env:USERPROFILE\.ssh\myblog-cn -C "github-actions-deploy-cn"
# 注意:提示输入 passphrase 时直接回车留空!

指纹审计核对:国内节点公网密钥的指纹为 SHA256…,部署前务必核对确认与服务器 authorized_keys 中写入的公钥指纹一致。指纹不匹配是 CI 连接失败的常见原因之一。

将私钥内容存入 GitHub Secrets 的 DEPLOY_SSH_KEY_CN,公钥追加至国内机的 ~/.ssh/authorized_keys,并锁死权限:

# 国内服务器上执行
mkdir -p ~/.ssh # 如果目录已存在则无影响
chmod 700 ~/.ssh
cat ~/myblog-cn.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
NOTE

mkdir -p ~/.ssh 在目录已存在时不会报错,也不影响已有的 authorized_keys 文件,安全无害。

Secret 更新后记得触发重跑

这个坑导致我前 3 次部署全部失败,因为密钥替换后没有及时更新 GitHub Secret。务必在 GitHub 仓库的 Settings → Secrets and variables → Actions 中,删除旧 Secret 后重新创建(而不是编辑),确保存入的是完整的新密钥内容。更新后手动触发一次 Workflow 验证连通性。

坑 2:rsync 擦除宝塔锁文件 .user.ini 导致权限报错#

国内机用了宝塔面板,默认会在网站根目录下放一个防跨站的锁文件 .user.ini (宝塔面板:“我是为了你好!”;rsync:“你妨碍我全删全清了!”) (系统加了 chattr +i 只读属性防护)。Actions 里的 rsync -az --delete 机制在同步时发现本地没有这个文件,试图在远端将其强制抹除,结果直接权限报错导致发布管线崩溃。

解决办法:在 rsync 投递命令中加入 --exclude=".user.ini" --exclude=".well-known",排除这些系统级只读文件以及 Let’s Encrypt 的验证目录。

坑 3:PowerShell 变量展开破坏 Nginx 配置#

在通过 SSH 远程写入 Nginx 配置文件时,如果使用双引号字符串或未加引号的 here-document,PowerShell 会把 $uri$uri/$host 等 Nginx 内置变量当作 PowerShell 变量展开,导致写入服务器的配置文件变成乱码。

跨平台字符串传递的隐形地雷

Windows 端的 PowerShell 会展开 $ 开头的 token,而 Nginx 配置中大量使用这些变量。一旦被展开,try_files $uri $uri/ /index.html 可能变成 try_files / /index.html(空值替换),导致 Nginx 语法检查失败。

解决办法:使用单引号 here-string(@'...'@)或 SSH 的 bash -s 模式传递配置内容:

Terminal window
# PowerShell 中正确传递含 $ 变量的配置
ssh user@host "bash -s" << 'EOF'
cat > /tmp/nginx.conf << 'NEOF'
location / {
try_files $uri $uri/ /index.html;
}
NEOF
EOF

关键是在 SSH 端使用 'EOF'(单引号包裹,禁止本地 shell 展开),并且服务端也用 'NEOF' 禁止 bash 展开。


4. 两端服务器环境配置与安全优化#

国内机:移除老旧的 TLSv1.1 协议#

国内节点使用的是宝塔环境,Nginx 版本为 1.28.1。在排查站点配置时,我发现旧的站点文件里还在支持已经不安全的 TLSv1.1 协议。

直接编辑对应的虚拟主机配置文件 /www/server/panel/vhost/nginx/html_xxx.xxx.xxx.xxx.conf,将老旧的 ssl_protocols 过滤收紧:

# 剔除 TLSv1.1,仅放行主流安全的版本
ssl_protocols TLSv1.2 TLSv1.3;
宝塔面板 Nginx 配置的查找方式

宝塔面板生成的站点配置文件位于 /www/server/panel/vhost/nginx/ 目录下,文件名格式为 html_xxx.xxx.xxx.xxx.conf。修改后务必执行 nginx -t 做语法检查,再 systemctl reload nginx 使配置生效。不要直接在宝塔面板 UI 中编辑后再手动改文件,容易产生冲突。

国内服务器优化后的典型 Nginx 站点配置参考:

server {
listen 80;
listen 443 ssl;
listen 443 quic;
http2 on;
server_name www.dawn114514.site dawn114514.site;
index index.html index.htm;
root /www/wwwroot/MyBlog/dist;
include /www/server/panel/vhost/nginx/extension/xxx.xxx.xxx.xxx/*.conf;
include /www/server/panel/vhost/nginx/well-known/xxx.xxx.xxx.xxx.conf;
ssl_certificate /www/server/panel/vhost/cert/xxx.xxx.xxx.xxx/fullchain.pem;
ssl_certificate_key /www/server/panel/vhost/cert/xxx.xxx.xxx.xxx/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_tickets on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 启用极速 HTTP/3 (QUIC) 支持
add_header Strict-Transport-Security "max-age=31536000";
add_header Alt-Svc 'quic=":443"; h3=":443"';
error_page 497 https://$host$request_uri;
location / {
try_files $uri $uri/ /index.html;
}
# 针对自建服务 A 与自建服务 B 的 WebSocket 反向代理转发
# 依赖外部 0.websocket.conf 的全局 Upgrade map 指令
}

海外机:零面板环境下的安全加固与配置#

海外机用的是纯净 Ubuntu 系统,没有任何面板辅助。操作步骤如下:

第一步:安装基本服务

sudo apt-get update
sudo apt-get install -y nginx certbot python3-certbot-nginx

第二步:创建站点配置文件并测试

/etc/nginx/sites-available/myblog 中编写初始配置(仅 HTTP),确认 nginx -t 通过后 systemctl reload nginx

第三步:利用 certbot 一键申请并修改配置

sudo certbot --nginx -d dawn114514.site -d www.dawn114514.site \
--agree-tos --register-unsafely-without-email --non-interactive

该命令会自动在 /etc/nginx/sites-available/myblog 中添加 Let’s Encrypt 证书引用和 HTTP 强制跳转 HTTPS。

Certbot 非交互模式

--non-interactive 标志让 certbot 在无终端环境下自动完成证书申请,不需要手动输入邮箱或同意协议。加上 --register-unsafely-without-email 可以跳过邮箱注册步骤(适用于个人项目,生产环境建议填写真实邮箱以便接收证书过期提醒)。

第四步:突破防火墙双重过滤限制

海外机有两层防火墙需要穿透:

层级位置配置方式
OS 级别Ubuntu iptables命令行 iptables -I INPUT
云平台级别Oracle OCI Security ListWeb 控制台添加入站规则

OS 级别 iptables 配置:

Iptables 尾部追加规则失效陷阱

Ubuntu 镜像在 iptables 中默认有一行拒绝规则:-A INPUT -j REJECT --reject-with icmp-host-prohibited,位于规则链的最底部。如果你习惯性地使用 iptables -A 往表尾追加 80/443 放行命令,这些命令排在 REJECT 后面会彻底失效。必须使用 -I INPUT 明确插到过滤表的头部(例如第 4、5 行)才会起作用

# 正确示例:插入到过滤表的最前排
sudo iptables -I INPUT 4 -p tcp --dport 80 -j ACCEPT
sudo iptables -I INPUT 5 -p tcp --dport 443 -j ACCEPT
# 验证规则顺序是否正确(ACCEPT 必须在 REJECT 前面)
sudo iptables -S INPUT

云服务器平台防火墙放行:

除了系统内部的 iptables,甲骨文云在云平台外层还有一道安全规则守护。我们需要登录 Oracle Cloud Web 控制台,在机器关联的网络安全列表中直接添加两条简单的入站规则来放行流量:

协议目的端口范围源 IP CIDR说明
TCP800.0.0.0/0允许 HTTP 流量访问博客
TCP4430.0.0.0/0允许 HTTPS 加密流量访问博客

Oracle Cloud 的 Always Free 资源包括每月 10TB 出站流量,对于个人博客来说完全够用。但要注意免费实例有资源回收策略——如果 CPU/内存/网络使用率长期过低,Oracle 可能会回收实例。建议配置一个简单的 cron 定时任务保持适度活跃。

海外节点优化后的站点配置文件(/etc/nginx/sites-available/myblog)结构:

server {
server_name dawn114514.site www.dawn114514.site yyy.yyy.yyy.yyy _;
root /www/wwwroot/MyBlog/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# 对静态资源文件启用 30 天超长缓存,优化首屏加载
location ~* \.(?:css|js|mjs|json|xml|txt|ico|png|jpg|jpeg|gif|webp|avif|svg|woff2?|ttf|otf|mp3|flac|ogg)$ {
try_files $uri =404;
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
}
error_page 404 /404.html;
# Certbot 自动化注入的 SSL 配置
listen [::]:443 ssl ipv6only=on;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/dawn114514.site/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dawn114514.site/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
if ($host = www.dawn114514.site) {
return 301 https://$host$request_uri;
}
if ($host = dawn114514.site) {
return 301 https://$host$request_uri;
}
listen 80 default_server;
listen [::]:80 default_server;
server_name dawn114514.site www.dawn114514.site yyy.yyy.yyy.yyy _;
return 404;
}

运维验证百宝箱#

重构阶段使用最频繁的几个高频调试命令:

1. 绕过 DNS 缓存强制回源测试#

当域名刚切完 DNS,本地运营商的解析还没生效时,我们可以使用 curl--resolve 参数,强行将目标请求解析到特定的物理 IP 上,用于检验证书配置是否正常:

# 回源到海外机房测试
curl -I --resolve dawn114514.site:443:yyy.yyy.yyy.yyy https://dawn114514.site/
# 回源到国内机房测试
curl -I --resolve dawn114514.site:443:xxx.xxx.xxx.xxx https://dawn114514.site/
--resolve 的工作原理

curl --resolve 会在本地覆盖 DNS 解析结果,将指定域名的请求直接发送到指定 IP。这条命令不依赖系统 DNS,非常适合在 DNS 切换后的过渡期做证书和连通性验证。测试通过后,可以放心等待全球 DNS 缓存自然过期(通常 10 分钟到 1 小时)。

2. 证书基本指纹和 SAN 主机名查看#

各自签好证书后,检查本地配置物理文件的发行商及有效期限,防止低级配置错误:

sudo openssl x509 -in /etc/letsencrypt/live/dawn114514.site/fullchain.pem \
-noout -subject -issuer -dates -ext subjectAltName

3. 验证两岸服务器返回的 HTTP 头#

确认两端 Nginx 配置正确、证书有效后,还可通过比较响应头验证两端配置一致性:

# 比较两端响应头(Server / Cache / HSTS 等)
diff <(curl -sI --resolve dawn114514.site:443:xxx.xxx.xxx.xxx https://dawn114514.site/) \
<(curl -sI --resolve dawn114514.site:443:yyy.yyy.yyy.yyy https://dawn114514.site/)

通过 Bash 的 进程替换 特性,我们无需创建临时文件,便可直接比对过滤两端服务器返回的 Header 差异。

4. 测试 DNS 分流是否生效#

从不同地理位置的节点执行解析测试:

# 使用 Google DNS(模拟境外解析)
nslookup dawn114514.site 8.8.8.8
# 使用阿里 DNS(模拟境内解析)
nslookup dawn114514.site 223.5.5.5

预期结果:境外 DNS 返回 yyy.yyy.yyy.yyy,境内 DNS 返回 xxx.xxx.xxx.xxx


改造后状态与高可用总结#

通过本轮的架构优化,博客成功构建了能够就近自适应解析的高可用发布网络。

截图预留:ITDOG / 全球解析延迟对比拨测图

重构后的最终架构对比:

评估维度重构前状况重构后状况
主站域名解析Cloudflare DNS (国内响应偏慢)Alibaba Cloud DNS
就近分流策略大陆与境外一律兜底打到国内 ECS智能双线:国内落 ECS,境外落 Oracle
音乐静态资源music.dawn114514.site (与主域名强绑定)music.xxx.online (解耦至 xxx.online 并彻底物理隔离)
图床静态资源一直处于物理隔离状态 (img.xxx.online)保持物理隔离,仅将音乐子域迁移解耦
持续集成效率单台机器手动/脚本部署,易出版本差GitHub Actions 全自动并发双向同步投递
传输层安全级别允许 TLSv1.1 老旧协议接入完全淘汰 TLSv1.1,强制 TLSv1.2 + TLSv1.3
机房抗灾能力机器挂掉则全网直接瘫痪单机宕机时,手动切换 DNS 解析至另一健康节点即可秒级兜底

结语#

在服务架构设计中,保持职责单一和适度解耦是一条黄金法则。

这次把博客拆成双机房就近接入的架构,虽然在期初多配置了几个域名解析和免密部署规则,但换来的是主站应用极强的迁移弹性、极速的国内外首屏响应,以及完全摆脱多媒体资源带宽挤占的安全保障。

回顾整个过程,真正耗时最多的不是写配置本身,而是排查那些跨平台的隐性陷阱——非交互式环境的 SSH passphrase 部署失败与挂起、PowerShell 的变量展开污染、iptables 的规则顺序陷阱、以及云服务器平台外层安全规则拦截。这些坑每个单拎出来都不复杂,但它们组合在一起的时候,就构成了部署效率的真正瓶颈。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

博客国内外双节点部署与全链路架构解耦
https://github.com/Dawn6666666/MyBlog
作者
黎明
发布于
2026-05-19
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录