跳到主要内容

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. 延伸阅读