本专题把 Koa 应用从「跑通」推进到「抗压上线」:补全日志与错误、多进程/集群、优雅关闭、缓存与压缩、压测方法,以及 PM2、Docker 部署。
npm install pino pino-prettyconst pino = require("pino");
const logger = pino({
level: process.env.LOG_LEVEL || "info",
transport: process.env.NODE_ENV === "development"
? { target: "pino-pretty" }
: undefined, // 生产输出 JSON
});
app.on("error", (err, ctx) => {
logger.error({ err, reqId: ctx.state.reqId }, "unhandled error");
});// middlewares/error.js
const logger = require("../utils/logger");
module.exports = async (ctx, next) => {
try {
ctx.set("X-Request-Id", ctx.request.headers["x-request-id"] || Date.now());
await next();
// 404 兜底
if (ctx.status === 404 && !ctx.body) ctx.body = { code: "NOT_FOUND", message: "资源不存在" };
// 统一成功包裹(可选)
if (ctx.status >= 200 && ctx.status < 300) {
ctx.body = { code: 0, message: "ok", data: ctx.body, requestId: ctx.request.headers["x-request-id"] };
}
} catch (err) {
ctx.status = err.status || 500;
if (ctx.status >= 500) {
logger.error({ err }, "server error");
ctx.body = process.env.NODE_ENV === "production"
? { code: 500, message: "服务器内部错误" }
: { code: 500, message: err.message };
} else {
ctx.body = { code: err.code || err.status, message: err.message };
}
ctx.app.emit("error", err, ctx);
}
};pm2 start app.js -i max --name koa-api
pm2 save
pm2 startup # 开机自启-i max 按 CPU 核数启动 worker,共享端口、负载均衡。const cluster = require("node:cluster");
const os = require("node:os");
if (cluster.isPrimary) {
for (let i = 0; i < os.cpus().length; i++) cluster.fork();
cluster.on("exit", (worker) => cluster.fork()); // 自动重启
} else {
require("./app.js");
}在收到退出信号时停止接收新请求、完成进行中请求再退出:
const app = new Koa();
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
async function shutdown() {
console.log("shutting down...");
// 关闭 HTTP 服务,等待进行中连接完成
await new Promise((resolve) => server.close(resolve));
// 释放数据库/Redis 连接
await db.end();
await redis.quit();
process.exit(0);
}
const server = app.listen(3000);// 静态资源 gzip + 缓存头
const serve = require("koa-static");
app.use(serve("public", { gzip: true, maxage: 1000 * 60 * 60 * 24 }));
// 动态接口压缩
const compress = require("koa-compress");
app.use(compress({ threshold: 2048 }));
// Redis 缓存热点接口
async function cached(key, ttl, loader) {
const hit = await redis.get(key);
if (hit) return JSON.parse(hit);
const v = await loader();
await redis.set(key, JSON.stringify(v), { EX: ttl });
return v;
}// 限流
const ratelimit = require("koa-ratelimit");
app.use(ratelimit({
db: redis, duration: 60000, max: 100, id: (ctx) => ctx.ip,
}));
// body 大小限制
const bodyParser = require("koa-bodyparser");
app.use(bodyParser({ formLimit: "56kb", jsonLimit: "1mb" }));connect 阻塞。npx autocannon -c 100 -d 10 -p 10 http://localhost:3000/关注 QPS、P95/P99。基准:先测「空中间件」的某接口,再逐层加中间件,找出哪一层拉低了吞吐。
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app .
USER node
EXPOSE 3000
CMD ["node", "app.js"]services:
app:
build: .
ports: ["3000:3000"]
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
depends_on:
- redis
redis:
image: redis:7-alpineserver {
listen 80;
location / {
proxy_pass http://koa_app;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}记得在入口设置 app.proxy = true,才能让 ctx.ip 拿到真实客户端 IP。
NODE_ENV=production,关闭 debug 输出app.proxy = true后端/02-Redis 系列后端/01-MySQL 系列