
特别鸣谢:这次重构借助 OpenAI Codex 的卓越辅助,在短短半天时间内就完成了整套复杂的双节点部署与全链路域名解耦调试。
最近抽空把博客的部署架构重构了一下。
原来的站点全部挤在国内单机上,海外访问延迟较高;且虽然图床等静态资源一直都在独立的 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最终网络架构与流量分流拓扑
重构后的系统分为以下四个层次,各层之间职责明确:
-
解析分流层:利用阿里云 DNS,对国内和境外流量做智能路由。
-
静态资源存储层:将原本绑定在主域名下的音乐子域(
music.dawn114514.site)也解耦迁移至独立的xxx.online域名(music.xxx.online)代理,与图床一起实现静态资源与主域名在故障域和带宽上的解耦,不再占用主站带宽。 -
Nginx 接入层:国内和海外节点各自部署 Nginx 并终结 SSL 证书,国内机房额外承载子系统反代。
-
CI/CD 管线:内容仓库与代码仓库解耦联动 —— 推送内容仓库触发
trigger-build.yml,通过repository_dispatch事件分发至代码仓库的deploy.yml,由后者同步内容、构建并以真正的后台并行&+wait聚合退码方式同步投递双端机器。
具体的流量和自动部署走向可以参考下图:
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]
%% 静态资源解耦(浏览器直连 R2,不经 Nginx 中转)
Browser[浏览器] -.->|直接请求外链| R2_Music[music.xxx.online <br>Cloudflare R2 音乐源]
Browser -.->|直接请求外链| R2_Img[img.xxx.online <br>Cloudflare R2 图床]
CN_Nginx -.->|HTML 引用外链 URL| Browser
OV_Nginx -.->|HTML 引用外链 URL| Browser
%% 自动化持续集成管线(内容/代码双仓库解耦联动)
Developer[开发者 git push] --> ContentRepo[内容仓库 MyBlog-Content]
ContentRepo -->|push main 触发 trigger-build.yml| Dispatch[repository-dispatch 事件 content-updated]
Dispatch -->|DISPATCH_TOKEN 鉴权分发| CodeRepo[代码仓库 MyBlog deploy.yml]
CodeRepo -->|浅克隆内容仓库并复制目录 + pnpm build| Build[GitHub Actions 构建管线]
Build -->|rsync 同步 后台并行 &| CN_Node
Build -->|rsync 同步 后台并行 &| OV_NodeGitHub Actions 双端部署的详细时序交互如下:
sequenceDiagram
participant Dev as 开发者
participant CR as 内容仓库 MyBlog-Content
participant TR as trigger-build.yml
participant DR as 代码仓库 deploy.yml
participant CI as Actions Runner
participant CN as 国内 ECS (Aliyun)
participant OS as 海外 Oracle
Dev->>CR: git push main (改动 posts/data 等)
CR->>TR: 触发 trigger-build.yml
TR->>DR: repository-dispatch 发送 content-updated 事件
Note over TR,DR: 携带 DISPATCH_TOKEN 鉴权
DR->>CI: 触发 deploy.yml 工作流
CI->>CI: checkout + setup pnpm/node 22 + 恢复 Astro 缓存
CI->>CI: pnpm install --frozen-lockfile
CI->>CI: pnpm run sync-content (浅克隆内容仓库并复制目录, 失败即标红)
CI->>CI: pnpm run build (注入 BILI_SESSDATA / UMAMI_SHARE_URL 等构建期密钥)
Note over CI: 构建产物生成于 dist/ 目录
par 后台并行部署到国内
CI->>CI: 写入 DEPLOY_SSH_KEY_CN
CI->>CN: ssh-keyscan 写入 known_hosts (每次扫描直接信任, 非核验指纹)
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 写入 known_hosts (每次扫描直接信任, 非核验指纹)
CI->>OS: mkdir -p /www/wwwroot/MyBlog/dist
CI->>OS: rsync -az --delete dist/
OS-->>CI: 同步完成
end
Note over CI: wait 聚合双端后台进程退出码改造步骤与细节
1. 音乐子域解耦与迁移(Cloudflare R2)
在此之前,我的图床静态资源其实一直托管在独立的 xxx.online 域名(img.xxx.online)下,并未进行迁移。
但是,我的音乐多媒体资源却仍然直接绑定在主域名的子域 music.dawn114514.site 下。这就导致如果我想把主域名 dawn114514.site 的 Nameservers 从 Cloudflare DNS 切走,静态音乐资源的解析就会直接跟着完蛋(因为 R2 绑定自定义域名需要域名托管在 CF)。
解决办法:
为了彻底将静态资源与主域名剥离,我决定将音乐子域名迁移至独立托管在 Cloudflare 上的 xxx.online 之下(即 music.xxx.online),使资源域名与主站域名分属不同顶级域,实现故障域解耦——主站 DNS 怎么改动都不影响资源解析。
也就是说:直接在两个 R2 存储桶里分别连接 img.xxx.online 与 music.xxx.online,连接成功后控制台会显示绿色 Active。Cloudflare 会在该域名的 DNS 区页中自动生成对应的 CNAME 记录(自动开启 CF 代理),不要自己去拼 pub-xxxx.r2.dev 的目标值。

随后,在博客内容仓库中进行一次全量正则替换,将所有旧的资源引用替换为新的解耦域名:
# 将旧的引用地址清洗为 music.xxx.onlinefind . -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。
R2 CORS 跨域配置
域名迁移后还需配置 CORS 策略,确保浏览器端对 R2 桶的跨域请求不被拦截。在 Cloudflare R2 控制台 → 存储桶 → Settings → CORS Policy 中,为两个存储桶分别添加如下 CORS 规则(整体是一个规则数组,注意最外层的 [ ]):
[ { "AllowedOrigins": ["*"], "AllowedMethods": ["GET", "HEAD"], "AllowedHeaders": ["*"], "ExposeHeaders": [], "MaxAgeSeconds": 3600 }]2. DNS 大陆/境外智能解析分流
主域名 dawn114514.site 迁离 Cloudflare,Nameservers 改为阿里云的 dns19.hichina.com 和 dns20.hichina.com。
在阿里云云解析后台,我对国内和国外线路做了 A 记录拆分,配置如下:
| 主机记录 | 记录类型 | 解析线路 | 记录值 | TTL |
|---|---|---|---|---|
| @ | A | 默认 (兜底必配) | xxx.xxx.xxx.xxx | 600s |
| @ | A | 中国内地 | xxx.xxx.xxx.xxx (国内 ECS) | 600s |
| @ | A | 中国境外 | yyy.yyy.yyy.yyy (境外 Oracle) | 600s |
| www | A | 默认 | xxx.xxx.xxx.xxx | 600s |
| www | A | 中国内地 | xxx.xxx.xxx.xxx | 600s |
| www | A | 中国境外 | yyy.yyy.yyy.yyy | 600s |

3. GitHub Actions 自动化双端同步发布
内容发布如果靠手动同步肯定会出问题。
我现在的 CI/CD 是内容/代码双仓库解耦的设计:内容仓库(MyBlog-Content)只管理文章、结构化数据与静态资源(posts/、spec/、data/、images/),推送后由一个极简的 trigger-build.yml 通过 repository_dispatch 事件”摇醒”代码仓库(MyBlog)里负责实际构建和双端投递的 deploy.yml。
这样一份内容推送,触发一次构建,并真正地以 Shell 后台 & + wait 的方式并行投递两台机器。
内容仓库触发器:trigger-build.yml
内容仓库的 Workflow 极薄,只做一件事:在 posts/**、spec/**、data/**、images/** 有变更时(也支持手动 workflow_dispatch),把 content-updated 事件 dispatch 给代码仓库。
name: Trigger Code Repo Build
on: push: branches: - main paths: - "posts/**" - "spec/**" - "data/**" - "images/**" workflow_dispatch: {}
jobs: dispatch: runs-on: ubuntu-latest steps: - name: Dispatch to code repo uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.DISPATCH_TOKEN }} repository: Dawn6666666/MyBlog event-type: content-updated代码仓库部署器:deploy.yml
代码仓库的 deploy.yml 同时承接三类触发:自身 custom-main 分支的 push、内容仓库 dispatch 来的 content-updated 事件、以及手动 workflow_dispatch。
注意它加了 concurrency 取消旧运行、permissions 收紧权限、timeout-minutes 兜底,并在构建期注入内容同步令牌与媒体源密钥。
name: Deploy to Server
on: push: branches: [ custom-main ] paths-ignore: - "docs/**" - "README*.md" - ".github/**" repository_dispatch: types: [ content-updated ] workflow_dispatch:
concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true
permissions: contents: read
jobs: build-and-deploy: runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v6
- name: Setup pnpm uses: pnpm/action-setup@v6 with: run_install: false
- name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 22 cache: pnpm
- name: Restore Astro cache uses: actions/cache@v4 with: path: | .astro node_modules/.astro key: ${{ runner.os }}-astro-${{ hashFiles('pnpm-lock.yaml', 'astro.config.mjs', 'src/config/**/*.ts') }} restore-keys: | ${{ runner.os }}-astro-
- name: Install dependencies run: pnpm install --frozen-lockfile --prefer-offline
- name: Sync content repo run: pnpm run sync-content env: ENABLE_CONTENT_SYNC: true CONTENT_REPO_URL: https://x-access-token:${{ secrets.CONTENT_REPO_TOKEN }}@github.com/Dawn6666666/MyBlog-Content.git CONTENT_SYNC_PULL: false GIT_TERMINAL_PROMPT: 0
- name: Build site run: pnpm run build env: ENABLE_CONTENT_SYNC: true CONTENT_REPO_URL: https://x-access-token:${{ secrets.CONTENT_REPO_TOKEN }}@github.com/Dawn6666666/MyBlog-Content.git CONTENT_SYNC_PULL: false GIT_TERMINAL_PROMPT: 0 BILI_SESSDATA: ${{ secrets.BILI_SESSDATA }} UMAMI_SHARE_URL: ${{ secrets.UMAMI_SHARE_URL }}
- name: Deploy to dual servers env: DEPLOY_PORT: 22 DEPLOY_DIR: /www/wwwroot/MyBlog DEPLOY_HOST_CN: ${{ secrets.DEPLOY_HOST_CN }} DEPLOY_USER_CN: ${{ secrets.DEPLOY_USER_CN }} DEPLOY_SSH_KEY_CN: ${{ secrets.DEPLOY_SSH_KEY_CN }} DEPLOY_HOST_OVERSEA: ${{ secrets.DEPLOY_HOST_OVERSEA }} DEPLOY_USER_OVERSEA: ${{ secrets.DEPLOY_USER_OVERSEA }} DEPLOY_SSH_KEY_OVERSEA: ${{ secrets.DEPLOY_SSH_KEY_OVERSEA }} 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
mkdir -p ~/.ssh chmod 700 ~/.ssh printf '%s\n' "$key" > ~/.ssh/deploy_key_"$name" chmod 600 ~/.ssh/deploy_key_"$name" 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 -az --delete \ -e "ssh -i ~/.ssh/deploy_key_$name -p $DEPLOY_PORT" \ --exclude=".user.ini" \ --exclude=".well-known" \ dist/ "$user@$host:$DEPLOY_DIR/dist/" }
# 后台并行投递两台机器,最后用 wait 聚合退出码 deploy_one cn "$DEPLOY_HOST_CN" "$DEPLOY_USER_CN" "$DEPLOY_SSH_KEY_CN" & pid_cn=$! deploy_one oversea "$DEPLOY_HOST_OVERSEA" "$DEPLOY_USER_OVERSEA" "$DEPLOY_SSH_KEY_OVERSEA" & pid_oversea=$!
status=0 wait "$pid_cn" || status=$? wait "$pid_oversea" || status=$? exit "$status"
代码仓库静态检查:lint.yml
代码仓库还配了一个手动触发的 lint.yml,用于发布前自检:用 Biome 跑 src/ 的 lint、pnpm run sync-content 同步内容做 Astro Check,最后再跑一次 pnpm build 验证整站能正常出包。
它只在 workflow_dispatch 下触发,不会干扰正常发布管线,适合改完前端代码不确定会不会炸时先手动跑一遍兜底。
GitHub Secrets 配置清单
双仓库解耦后,密钥分两处存放:内容仓库只持有一把用于跨仓库 dispatch 的令牌;代码仓库则承担真正的部署凭证与构建期注入密钥。
内容仓库 MyBlog-Content:
| Secret 名称 | 说明 | 示例值 |
|---|---|---|
DISPATCH_TOKEN | 用于跨仓库触发 repository_dispatch 的 PAT。Classic PAT 选 repo(私有仓库)或 public_repo(公开仓库);Fine-grained PAT 则需对目标代码仓库授予 Contents: write | ghp_xxxxxxxxxxxxxxxxxxxxxxxx |
代码仓库 MyBlog:
| Secret 名称 | 说明 | 示例值 |
|---|---|---|
CONTENT_REPO_TOKEN | 构建期拉取内容仓库的 PAT(含内容仓库 contents:read) | ghp_xxxxxxxxxxxxxxxxxxxxxxxx |
DEPLOY_HOST_CN | 国内服务器 IP | xxx.xxx.xxx.xxx |
DEPLOY_USER_CN | 国内服务器 SSH 用户 | admin |
DEPLOY_SSH_KEY_CN | 国内服务器免密私钥(完整内容) | -----BEGIN OPENSSH PRIVATE KEY-----... |
DEPLOY_HOST_OVERSEA | 海外服务器 IP | yyy.yyy.yyy.yyy |
DEPLOY_USER_OVERSEA | 海外服务器 SSH 用户 | ubuntu |
DEPLOY_SSH_KEY_OVERSEA | 海外服务器免密私钥(完整内容) | -----BEGIN OPENSSH PRIVATE KEY-----... |
BILI_SESSDATA | 构建期抓取 B 站数据所需的 SESSDATA | xxxxxxxxxxxxxxxx |
UMAMI_SHARE_URL | 构建期注入的 Umami 统计分享链接 | https://umami.xxx/share/xxxx |

坑 1:国内机密钥带 Passphrase 导致部署失败与 CI 挂起
我原来图省事,直接拷贝了国内阿里云服务器上现有的私钥文件(~/.ssh/id_ed25519)内容并填入 GitHub Secrets 的 DEPLOY_SSH_KEY_CN。然而,在 Actions 执行部署时,一直卡住并最终报错失败。
后来在服务器本地自测该私钥才发现,原来这把密钥在初次生成时被设置了密码保护。
在 GitHub Actions 这种非交互式的 CI 自动化环境下,SSH 握手因为需要输入密码而无法继续,最终导致部署直接被拒或挂死。
解决办法:
重新在本地生成一对免密的专属 Ed25519 密钥对,把公钥追加到服务器的授权名单中:
# 本地生成免密 Ed25519 密钥对(PowerShell)ssh-keygen -t ed25519 -f $env:USERPROFILE\.ssh\myblog-cn -C "github-actions-deploy-cn"# 注意:提示输入 passphrase 时直接回车留空!指纹审计核对:
将私钥内容存入 GitHub Secrets 的 DEPLOY_SSH_KEY_CN,公钥则要从本地 Windows 机器传到服务器并追加进 authorized_keys。
注意:上面 ssh-keygen 是在本地生成的,服务器上并没有 ~/myblog-cn.pub,不能直接 cat:
# 在本地 Windows 上:把公钥管道推给远端服务器追加进 authorized_keystype $env:USERPROFILE\.ssh\myblog-cn.pub | ssh admin@xxx.xxx.xxx.xxx "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"坑 2:rsync 擦除宝塔锁文件 .user.ini 导致权限报错
国内机用了宝塔面板,默认会在网站根目录下放一个防跨站的锁文件 .user.ini (系统用 chattr +i 给文件加了 不可变属性防护,连 root 默认都改不动)。
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 变量展开,导致写入服务器的配置文件变成乱码。
解决办法:
PowerShell 没有 Bash 的 << 'EOF' heredoc,要用单引号 here-string(@'...'@,禁止 $ 展开)把整段脚本拼好,再经管道交给远程的 bash -s:
# PowerShell:单引号 here-string + 管道传给远端 bash$config = @'cat > /tmp/nginx.conf << 'NEOF'location / { try_files $uri $uri/ /index.html;}NEOF'@
$config | ssh user@host "bash -s"要点:
- 本地用
@'...'@(单引号)hold 住字符串,PowerShell 不会展开里面的$uri/$host - 远端
bash -s从 stdin 读到内容后再用<< 'NEOF'(单引号)把$uri留给远端 bash 不展开,从而原样写进 Nginx 配置
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 站点配置参考:
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 updatesudo 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。
第四步:突破防火墙双重过滤限制
海外机有两层防火墙需要穿透:
| 层级 | 位置 | 配置方式 |
|---|---|---|
| OS 级别 | Ubuntu iptables | 命令行 iptables -I INPUT |
| 云平台级别 | Oracle OCI Security List | Web 控制台添加入站规则 |
OS 级别 iptables 配置:
# 先查看当前规则和行号,定位 REJECT 行所在的位置(注意用 -L 而不是 -S,-L 才支持 --line-numbers)sudo iptables -L INPUT -n --line-numbers
# 假设查到 REJECT 在第 6 行,就把 80/443 的 ACCEPT 插到它前面# 先在第 6 行处插入 80 放行(这条会顶替原第 6 行的位置,REJECT 顺延到第 7 行)sudo iptables -I INPUT 6 -p tcp --dport 80 -j ACCEPT# 再在第 7 行处插入 443 放行(在 REJECT 之前)sudo iptables -I INPUT 7 -p tcp --dport 443 -j ACCEPT# 实操时请把行号替换为你机器上 REJECT 所在的真实编号,不要照抄
# 验证规则顺序是否正确(ACCEPT 必须在 REJECT 前面)sudo iptables -L INPUT -n --line-numbers
# 重启后规则会丢失,需要持久化sudo apt-get install -y iptables-persistentsudo netfilter-persistent save云服务器平台防火墙放行:
除了系统内部的 iptables,甲骨文云在云平台外层还有一道安全规则守护。
我们需要登录 Oracle Cloud Web 控制台,在机器关联的网络安全列表中直接添加两条简单的入站规则来放行流量:
| 协议 | 目的端口范围 | 源 IP CIDR | 说明 |
|---|---|---|---|
| TCP | 80 | 0.0.0.0/0 | 允许 HTTP 流量访问博客 |
| TCP | 443 | 0.0.0.0/0 | 允许 HTTPS 加密流量访问博客 |
海外节点优化后的站点配置文件(/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/2. 证书基本指纹和 SAN 主机名查看
各自签好证书后,检查本地配置物理文件的发行商及有效期限,防止低级配置错误:
sudo openssl x509 -in /etc/letsencrypt/live/dawn114514.site/fullchain.pem \ -noout -subject -issuer -dates -ext subjectAltName3. 验证两端服务器返回的 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 分流是否生效
从不同地理位置的节点执行解析测试:
# 在境外机器上查询(境外真实环境解析)nslookup dawn114514.site
# 在国内机器上查询阿里 DNS(境内解析)nslookup dawn114514.site 223.5.5.5预期结果:境外真实解析返回 yyy.yyy.yyy.yyy(落 Oracle),境内解析返回 xxx.xxx.xxx.xxx(落 ECS)。
当前方案补充
本文前文记录的海外 Oracle 节点部署方案现已迁移至腾讯 EdgeOne,后续海外站点由 EdgeOne 负责构建与发布。
改造后状态与双节点分流总结
通过本轮的架构优化,博客构建了就近接入的双节点分流发布网络:阿里云 DNS 按境内外分线路把请求导到不同节点,国内访客落 ECS、境外访客落 Oracle,各自降低首屏延迟。
重构后的最终架构对比:
| 评估维度 | 重构前状况 | 重构后状况 |
|---|---|---|
| 主站域名解析 | Cloudflare DNS (国内响应偏慢) | Alibaba Cloud DNS |
| 就近分流策略 | 大陆与境外一律兜底打到国内 ECS | 智能双线:国内落 ECS,境外落 Oracle |
| 音乐静态资源 | music.dawn114514.site (与主域名强绑定) | music.xxx.online (解耦至 xxx.online,资源域名与故障域独立) |
| 图床静态资源 | 独立域名 img.xxx.online (未迁移) | 保持独立,仅把音乐子域一并迁出 |
| 持续集成效率 | 单台机器手动/脚本部署,易出版本差 | 内容/代码双仓库解耦:repository_dispatch 联动 + 后台并行 &/wait 双端同步投递 |
| 传输层安全级别 | 允许 TLSv1.1 老旧协议接入 | 完全淘汰 TLSv1.1,强制 TLSv1.2 + TLSv1.3 |
| 抗灾能力 | 机器挂掉则全网直接瘫痪 | 双节点分流 + 手动容灾:单节点宕机时人工切换 DNS 解析至另一健康节点,TTL 过期后生效 |
结语
在服务架构设计中,保持职责单一和适度解耦是一条黄金法则。
这次把博客拆成双节点就近接入的架构,虽然在期初多配置了几个域名解析和免密部署规则,但换来的是主站应用较强的迁移弹性、国内外首屏延迟的降低,以及多媒体资源带宽不再挤占主站的保障。
回顾整个过程,真正耗时最多的不是写配置本身,而是排查那些跨平台的隐性陷阱——非交互式环境的 SSH passphrase 部署失败与挂起、PowerShell 的变量展开污染、iptables 的规则顺序陷阱、以及云服务器平台外层安全规则拦截。
这些坑每个单拎出来都不复杂,但它们组合在一起的时候,就构成了部署效率的真正瓶颈。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 88 天
部分内容可能已过时
评论