主题
第 25 章 · 部署与运维 · 高频面试题
部署题虽然不是每场必问,但问到一道就是劝退题——能答上来直接显得你"会用"而不止"会写"。
1. 静态部署平台对比?怎么选?
答:
| 平台 | 优势 | 劣势 | 适合 |
|---|---|---|---|
| Vercel | Next.js 最佳、Edge Function | 国内慢、商业付费 | 全球 SaaS |
| Netlify | Form / Identity 一站式 | 国内慢 | 小型站 |
| Cloudflare Pages | 全球最快、免费 / 无限带宽 | UI 一般 | 个人项目 |
| GitHub Pages | 免费 / 集成 GitHub | 仅静态、CN 极慢 | 文档站 |
| 腾讯/阿里 OSS+CDN | 国内速度好 | 要自配 CDN/HTTPS | 国内商业项目 |
选型思路:
- 用户在国外 → Vercel / Cloudflare Pages
- 用户在国内 → 腾讯 EdgeOne / 阿里 OSS+CDN / Cloudflare(全球都快)
- 个人简历项目 → Cloudflare Pages(免费、全球可访问)
2. SPA 部署到 Nginx,刷新页面 404 怎么解决?
答:
原因:history 模式的 URL(如 /about)在服务器上没有对应文件,刷新会真实请求服务器找不到。
解决:所有不存在的路径 fallback 到 index.html,让前端路由接管。
nginx
server {
root /var/www/dist;
location / {
try_files $uri $uri/ /index.html;
}
}try_files 的语义:
- 先找
$uri(如/about→/var/www/dist/about) - 再找
$uri/(目录) - 都没有 → 返回
/index.html(200,不是 404)
Apache 用 .htaccess:
apache
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]Cloudflare Pages / Vercel / Netlify:天然支持,不用配。
3. CDN 的工作原理?发布后如何确保用户拿到新版本?
答:
原理:
用户 → DNS 解析到最近边缘节点
│
├── 命中缓存 → 直接返回
│
└── 未命中 → 回源(去源站拉)→ 缓存 → 返回确保用户拿到新版本的两种方式:
方式 1(推荐):hash 文件名 + 长缓存
app.a3f9c4.js ← 内容变了,hash 变了,URL 变了
← CDN 视为新文件,自动从源站拉文件名变 = 新文件,根本不存在"刷新缓存"问题。HTML 里引用新名字即可。
方式 2:主动刷新(Purge)
调 CDN API 把旧资源从所有节点驱逐。代价:刷新后第一次访问要回源,慢。
部署顺序:先发静态资源(CDN 同步好),再发 HTML(源站)。否则用户拿到新 HTML 引用了 CDN 还没有的 JS → 404。
4. Docker 多阶段构建是什么?为什么用?
答:
问题:单阶段构建会把 node_modules、构建工具、源码都打进最终镜像,几百 MB。
多阶段构建:分多个 FROM,前面阶段只做构建,后面阶段只 COPY --from=... 取产物。
dockerfile
# 阶段 1:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 阶段 2:仅 nginx + dist 产物
FROM nginx:1.25-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]收益:
| 维度 | 单阶段 | 多阶段 |
|---|---|---|
| 镜像体积 | 800MB | 30MB |
| 启动速度 | 慢 | 快 |
| 安全性 | 含构建工具 | 极简 |
| 拉取速度 | 慢 | 快 |
5. 写一个 GitHub Actions 自动部署的 workflow。
答:
yaml
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run test
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
env:
VITE_API_URL: ${{ secrets.API_URL }}
- name: Upload to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
projectName: my-app
directory: dist要点:
- test 和 deploy 分开,PR 只跑 test
- deploy 依赖 test:
needs: test - 用 secrets 存敏感配置
cache: 'npm'加速依赖安装
6. 缓存策略怎么配?为什么 HTML 要 no-cache?
答:
| 资源类型 | 推荐策略 | 原因 |
|---|---|---|
index.html | Cache-Control: no-cache | 必须实时拿到最新版(指向新 hash) |
app.[hash].js/css | Cache-Control: max-age=31536000, immutable | 文件名变 = 新文件 |
| 图片 | Cache-Control: max-age=2592000 | 30 天,平衡更新和性能 |
| 动态 API | Cache-Control: no-store | 永不缓存 |
为什么 HTML 必须 no-cache:
如果 HTML 强缓存了 1 小时,发布新版后老用户:
- 浏览器用 1 小时前的 HTML
- HTML 引用旧的
app.abc.js - 旧的 JS 可能调用新 API,参数不对 → 报错
no-cache 不是"不缓存",而是"必须协商缓存"——发请求带 If-None-Match,服务端返回 304 也算命中。
7. 灰度发布有哪些方式?
答:
| 方式 | 实现 | 适合 |
|---|---|---|
| 按比例 | Nginx split_clients / 网关按 hash 分流 | 通用 |
| 按 Cookie | 给特定用户植入 Cookie,路由到新版本 | 内部测试 |
| 按地域 | CDN 边缘节点按 IP 切流 | 大型公司 |
| 按用户 ID | user_id % 100 < 5 命中灰度 | 业务层灰度 |
| 蓝绿部署 | 两套环境(蓝/绿),LB 整体切换 | 极致快速回滚 |
Nginx 按 IP 灰度示例:
nginx
split_clients "${remote_addr}${http_user_agent}" $version {
5% "v2"; # 5% 用户走 v2
* "v1";
}
upstream v1 { server 10.0.0.1; }
upstream v2 { server 10.0.0.2; }
server {
location / { proxy_pass http://$version; }
}8. Nginx 反向代理 vs 正向代理?
答:
正向代理:
客户端 → [代理] → 目标服务器
客户端知道有代理;服务端不知道
场景:科学上网、隐藏客户端 IP
反向代理:
客户端 → [代理] → 后端服务器
客户端不知道;服务端知道(其实代理就是它的"门卫")
场景:负载均衡、SSL 终止、缓存、隐藏内部架构Nginx 主要做反向代理:把外部请求转发到内部多台后端服务器。
9. 怎么实现"一键回滚"?
答:
核心思路:每次发布带版本号,保留若干历史版本,回滚 = 把指针指回去。
实战方案 1:CDN 路径 + 软链
/cdn/dist/v20251018-1023/ ← 当前
/cdn/dist/v20251015-0912/ ← 上一版本(保留 7 天)
/cdn/dist/current ── 软链 ──► v20251018-1023/回滚 = ln -sfn v20251015-0912 current,CDN 缓存几分钟自动失效。
实战方案 2:K8s 镜像版本回滚
bash
# 当前版本
kubectl set image deploy/app web=registry/app:v20251018-1023
# 一行回滚
kubectl rollout undo deploy/app实战方案 3:Nginx upstream 切换
nginx
upstream backend {
server 10.0.0.1; # v2,注释掉就回滚
# server 10.0.0.2; # v1
}好的回滚预案:
- 一键脚本 / 一个按钮
- 5 分钟内完成
- 每月演练
- 数据库变更单独管理(DB schema 不能简单回滚)
10. 怎么做前端错误监控?
答:
捕获时机:
js
// 1. JS 同步错误
window.addEventListener('error', e => {
report({
type: 'js',
msg: e.message,
stack: e.error?.stack,
url: e.filename,
line: e.lineno,
col: e.colno,
});
});
// 2. Promise 未捕获 rejection
window.addEventListener('unhandledrejection', e => {
report({ type: 'promise', msg: e.reason?.message, stack: e.reason?.stack });
});
// 3. 资源加载失败(image/script/link)
window.addEventListener('error', e => {
if (e.target !== window) {
report({ type: 'resource', tag: e.target.tagName, src: e.target.src || e.target.href });
}
}, true); // 注意:用捕获阶段,资源错误不冒泡
// 4. 接口失败:在 axios/fetch 拦截器里上报上报方式:用 navigator.sendBeacon —— 即使页面关了也能发出去。
还要做:
- Source Map 解析:把混淆代码栈还原成源码(私有上传到 Sentry)
- 去重 + 聚合:同样的错误不要重复上报
- 采样率:错误太多时按比例采样
- 业务标签:上报时带 user_id、route、version,方便排查
工具:Sentry(业界标杆)、阿里 ARMS、字节火山引擎、自建。
11. (进阶)说说蓝绿部署 vs 滚动更新 vs 灰度发布的区别。
答:
| 策略 | 做法 | 优势 | 劣势 |
|---|---|---|---|
| 蓝绿部署 | 准备两套环境(蓝、绿),LB 整体切换 | 回滚快(秒级) | 资源 ×2,成本高 |
| 滚动更新 | 一台台机器替换(K8s 默认策略) | 资源占用低 | 中间状态会有两个版本共存 |
| 灰度发布 | 按比例/用户/地域慢慢放量 | 风险可控 | 周期长,要观察期 |
| 金丝雀部署 | 灰度发布的细化(先 1%,观察后 5%,再 20% ...) | 极度安全 | 操作复杂 |
实战建议:
- 小团队:滚动更新 + 监控告警
- 中等规模:蓝绿 + 一键切换
- 大厂:金丝雀 + 全链路灰度
12. (进阶)什么是 Edge Function / Serverless?跟传统部署有啥区别?
答:
| 维度 | 传统服务器 | Serverless | Edge Function(Workers/Edge) |
|---|---|---|---|
| 运行位置 | 中心机房 | 中心机房 | 全球边缘节点(200+ 城市) |
| 启动延迟 | 常驻运行 | 冷启动 100ms~几秒 | 冷启动 < 5ms |
| 计费 | 按时间 | 按请求 | 按请求 |
| 限制 | 任意 | 单次执行 < 几分钟 | 通常 < 50ms CPU |
| 适用 | 持久任务 | API、定时任务 | 鉴权、A/B 测试、个性化首页 |
前端典型用途:
- Edge Function 重写 HTML:根据用户地理位置动态注入语言、广告
- Edge 鉴权:在最近的节点拦截未登录请求
- Edge SSR:Vercel / Cloudflare Workers + Next.js / SvelteKit 在边缘渲染
一句话总结
部署面试套路:怎么部署(Vercel/Nginx/Docker)→ 怎么加速(CDN)→ 怎么自动化(CI/CD)→ 怎么不翻车(灰度+回滚)→ 怎么发现问题(监控)——能讲完这条链,就证明你不只是"写代码的",是能把项目"跑起来"的工程师。