SSR应用运维-Nextjs-Nuxt
1. 运行模式
| 模式 | 含义 | 部署方式 |
|---|---|---|
| CSR | 客户端渲染(纯静态) | CDN / OSS |
| SSR | 服务端渲染(每请求执行) | Node 服务 |
| SSG | 静态生成(构建时渲染) | CDN |
| ISR | 增量静态再生成 | Node + CDN |
SSR / ISR 需要跑 Node 进程,运维复杂度从 CDN 变成了服务治理。
2. Next.js standalone 模式
// next.config.js
module.exports = {
output: 'standalone',
}
构建产出 .next/standalone/:包含最小 node_modules + server.js,不需要完整源码。
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
COPY --from=builder --chown=node:node /app/public ./public
USER node
EXPOSE 3000
CMD ["node", "server.js"]
镜像 90-130MB(vs 全装 1GB+)。
3. 内存管理
SSR 每个请求都在 Node 里渲染 React 组件树,内存压力远大于纯 API:
# 容器限制 2G,Node 用 1.5G
NODE_OPTIONS="--max-old-space-size=1536"
常见泄漏:
- 全局 store 不清理(SSR 共享 Node 进程,请求间状态串)
- 大数据不释放(getServerSideProps 查 10MB 数据渲染后不 GC)
- module scope 缓存无限增长
3.1 请求隔离
// ✗ 模块顶层变量在所有请求间共享
let currentUser = null
export function getUser() { return currentUser }
// ✓ 用 AsyncLocalStorage 或每请求 context
import { AsyncLocalStorage } from 'async_hooks'
const store = new AsyncLocalStorage()
4. 缓存策略
4.1 页面级缓存
// Next.js getServerSideProps
export async function getServerSideProps() {
// 设置 CDN 缓存
res.setHeader('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300')
return { props: { ... } }
}
4.2 ISR
// Next.js pages
export async function getStaticProps() {
return {
props: { ... },
revalidate: 60, // 60 秒后访问触发后台重新生成
}
}
ISR = SSG 性能 + 数据新鲜度折中。
4.3 组件级缓存
React Server Components(Next.js 14+)内置缓存。或用 Redis 缓存渲染片段:
const cached = await redis.get(`page:${url}`)
if (cached) return cached
const html = renderToString(<Page />)
await redis.set(`page:${url}`, html, 'EX', 60)
return html
5. 冷启动
容器从 0 拉起到能服务:
- 镜像拉取:5-30s
- Node 启动 + JIT 编译:2-5s
- 首次渲染预热:1-2s
优化:
- 小镜像 + 预拉(K8s image pre-puller)
- readinessProbe 配 startupProbe
- prewarming:启动后主动请求首页
6. 健康检查
// pages/api/health.ts
export default function handler(req, res) {
// 深度检查:数据库连接是否正常
try {
await db.raw('SELECT 1')
res.status(200).json({ status: 'ok' })
} catch {
res.status(503).json({ status: 'unhealthy' })
}
}
K8s:
readinessProbe:
httpGet: { path: /api/health, port: 3000 }
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet: { path: /api/health, port: 3000 }
initialDelaySeconds: 30
periodSeconds: 30
liveness 不检查外部依赖(DB 短暂断不该重启自己)。
7. 日志
Next.js 默认 stdout 输出。生产用 pino:
// lib/logger.ts
import pino from 'pino'
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
...(process.env.NODE_ENV === 'production' ? {} : { transport: { target: 'pino-pretty' } }),
})
中间件里记请求日志:
app.use((req, res, next) => {
const start = Date.now()
res.on('finish', () => {
logger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration: Date.now() - start,
})
})
next()
})
8. 水平扩展
SSR 是 CPU 密集。单实例并发有限。
K8s HPA 按 CPU 扩缩:
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # SSR CPU 密集,阈值可低一些
每 Pod 1 进程(不用 PM2 cluster),K8s 多 Pod 代替多进程。
9. 静态资源分离
Next.js _next/static/ 是带 hash 的静态文件:
location /_next/static/ {
alias /var/www/.next/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location / {
proxy_pass http://node_app;
}
让 Nginx / CDN 服务静态文件,Node 只处理 SSR 请求,CPU 压力减半。
10. 常见反模式
- SSR 应用不限内存:渲染大页面 OOM
- 不分离静态资源:Node 跑静态白白占 CPU
- getServerSideProps 查大数据不分页:内存 + 响应慢
- 全局变量存用户状态:请求间串数据(严重 bug)
- 不做页面缓存:每请求都渲染 = 高 CPU + 高 TTFB
- 容器内跑 PM2 cluster max:和 K8s HPA 冲突
- liveness 检查 DB:DB 抖动所有 Pod 重启雪崩
11. 延伸阅读
- Next.js Deployment
- Next.js standalone
- Nuxt Deployment
- 模块 04 Dockerfile 多阶段构建
- 模块 05 K8s 部署全流程