应用从「能跑」到「能上线」,需补齐日志、错误处理、配置、优雅关闭、安全与性能等环节。Fastify 默认集成 pino 结构化日志,配合统一错误处理和实例配置,可以低成本达到生产级水平。
前置知识:先阅读 钩子 Hooks 与生命周期 与 插件与封装机制。
Fastify 默认不打印日志(logger: false),开启后即获得极快的结构化 JSON 日志(Pino 号称比默认 logger 快约 5 倍)。
import Fastify from 'fastify';
const app = Fastify({ logger: true });每个 req 自带 req.log,自动携带 reqId、请求方法、URL 等上下文,便于串联链路:
app.get('/order/:id', async (req) => {
req.log.info({ orderId: req.params.id }, 'fetching order');
const order = await getOrder(req.params.id);
req.log.debug({ order }, 'fetched');
return order;
});const app = Fastify({
logger: {
level: process.env.LOG_LEVEL || 'info',
redact: ['req.headers.authorization', 'body.password'], // 脱敏字段
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' } // 开发可读输出
: undefined, // 生产输出 JSON
},
});| 字段 | 描述 |
|---|---|
reqId | 由 Fastify 生成的请求追踪 ID(可自定义) |
ts | 时间戳(epoch ms) |
level | 日志级别 |
msg | 日志消息 |
Schema 校验失败、路由未命中等都返回统一结构的错误体:
{ "statusCode": 400, "code": "FST_ERR_VALIDATION", "error": "Bad Request", "message": "..." }setErrorHandler用 setErrorHandler 统一拦截所有未处理的错误:
class AppError extends Error {
constructor(code, message, statusCode = 400) {
super(message);
this.code = code;
this.statusCode = statusCode;
}
}
app.setErrorHandler((error, req, reply) => {
const statusCode = error.statusCode || 500;
req.log.error({ error }, 'unhandled error');
// 业务可见错误
if (error instanceof AppError) {
return reply.code(statusCode).send({ ok: false, code: error.code, message: error.message });
}
// 内部错误:生产环境不泄露细节
return reply.code(statusCode).send({
ok: false,
code: 'INTERNAL_ERROR',
message: process.env.NODE_ENV === 'development' ? error.message : '服务器内部错误',
});
});业务侧抛出:
app.get('/users/:id', async (req) => {
const user = await findUser(req.params.id);
if (!user) throw new AppError('USER_NOT_FOUND', '用户不存在', 404);
return user;
});async Handler 抛出的异常会被 Fastify 捕获并进入错误流程;对非 async(遗留回调式)Handler 需手动 reply.send(err) 或包一层。
const app = Fastify({
logger: true,
trustProxy: true, // 代理后获取真实 IP 与 protocol
bodyLimit: 1024 * 1024, // 请求体上限 1MB
maxParamLength: 100, // 路径参数长度上限
requestTimeout: 30_000, // 请求超时
keepAliveTimeout: 5_000,
});CONFIG/CLI 暴露配置结合 --env-file(Node 20+)或在入口读取环境变量:
node --env-file=.env app.jsconst app = Fastify({
logger: { level: process.env.LOG_LEVEL },
trustProxy: process.env.TRUST_PROXY === 'true',
});避免进程被强杀时丢失进行中的请求或连接。
const app = Fastify({ logger: true });
async function shutdown(signal) {
app.log.info({ signal }, 'shutting down');
await app.close(); // 触发 onClose 钩子,等待进行中请求完成
process.exit(0);
}
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));onClose 释放资源app.addHook('onClose', async () => {
await db.disconnect();
await cache.close();
});| 手段 | 说明 |
|---|---|
| 开启 Schema | 使用预编译序列化替代手写 JSON.stringify |
| Node 原生压力 | 压测用 autocannon:npx autocannon -c 100 -d 10 http://localhost:3000/ |
| 压缩 | @fastify/compress 开启 Gzip/Brotli |
| 缓存 | 对幂等接口做响应级/请求级缓存 |
app.register(cors, { origin: allowList });
app.register(helmet);
app.register(rateLimit, { max: 100, timeWindow: '1 minute' });
// 校验 + 请求体上限 + 脱敏(logger.redact)已在前面覆盖Fastify 单实例受限于单核。生产上常用 PROCESS 级集群:
// 简洁多进程方案(Node 的 cluster 封装)
import { availableParallelism } from 'node:os';
import cluster from 'node:cluster';
if (cluster.isPrimary) {
for (let i = 0; i < availableParallelism(); i++) cluster.fork();
} else {
const { start } = await import('./startup.js');
start();
}PM2 也能以 cluster 模式管理:
pm2 start app.js -i 4 # 启动 4 个 workerFROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
FROM node:22-slim
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]docker build -t fastify-api .
docker run -p 3000:3000 fastify-apiupstream fastify {
server 127.0.0.1:3000;
}
server {
listen 80;
location / {
proxy_pass http://fastify;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}
}配合 trustProxy: true 才能在 req.ip 拿到真实客户端 IP。
Pino 输出 JSON 供日志平台统一采集。开发期可加 pino-pretty 让终端可读,两者互不干扰。
FST_ERR_CTP_BODY_TOO_LARGE?调大 bodyLimit:Fastify({ bodyLimit: 2 * 1024 * 1024 });或在路由级 config 覆盖。
在 setErrorHandler 里把所有错误映射为 { ok, code, message },保证客户端解析一致。