复刻字节 AI 开发流:实践 Node.js 通用脚手架

0 阅读17分钟

前言

最近与前同事深入交流,他现为字节某组的 TL(组长),团队规模近 10 人。在讨论他们团队的 AI 工作流实践中,也得到一些八卦信息。

第一个是: 人才储备变化

他们团队已基本停止招聘实习生,今年仅招一人 个人解读: AI 协作模式下,新人的边际效应大幅下降

第二个是:组织预期转变

业内多位跟他同级的 Leader 私下评估:今年可能出现大规模调整 个人解读: 生成式 AI 在降本增效上的潜力被普遍看好,提前做好裁员准备

第三个是:开发方法论的周期性

  • 层出不穷的新概念:Vibe Coding、SDD 规范、Harness Engineering……
  • 理性看待:这些都是过渡阶段产物,随着大模型升级,这些方法论的生命周期可能只有几个月 个人建议: 不必投入过多精力去追赶,因为等你熟练一种方法论后,可能用不了多久就被淘汰了

第四个是:招聘标准演变 前端岗位招聘标准很多 JD 上开始冠以"全栈开发" 的名义招聘了 实际面试:侧重前端能力考察,但开始要求一些后端基础能力了 个人解读: AI 协作工具提效下,企业希望前端承担更多职能

好了,八卦之后,关于他们的 ai 工作流的核心如下:

你得教 AI,不断沉淀并固化它的规范

具体实践方法:

  • 识别问题:第一次遇到某类问题,AI 执行效果不理想
  • 人工介入:手动处理并解决问题
  • 规则沉淀:将解决方案固化为规则或 Skill
  • 迭代积累:问题解决越多,效率提升越明显,也就是后期 ai 执行的只要不是新的场景和复杂业务,基本不会出错

本质: 通过持续的反馈循环来训练和优化 AI 的工作能力,而不是一开始就寻求完美的工作流

这个理念在字节技术专家杨晨的全软软件开发大会分享中也提出过:Prompt = 可训练资产(像模型一样优化)

让后我个人抽象了这个单 Agent 的工作流程图如下:

image.png

接下来我会解释每个步骤的思路和如何落地:

步骤 1:项目初始化 + 全局规则设计

项目初始化是指,大多数现代框架都提供了开箱即用的脚手架工具,例如:

# Vue/React 生态
npm create vite@latest

# Nest.js
nest new project-name

# Hono.js
pnpm create hono@latest

初始化完成后,立即在根目录创建规则文件,通常命名为 CLAUDE.md 或 AGENTS.md。

这个文件是 AI 协作的宪法,应包含例如项目定位,技术栈清单,核心哲学(例如测试先行,采用 TDD 测试方案),项目结构示例,AI 协作原则等等内容。有兴趣的同学可以找我要这个项目的全局规则。

两个关键注意事项动态迭代 规则文件不是一成不变的。当发现 AI 写的代码不规范时,要想到如何抽离抽象的规则来约束,而不是一味手动修改

SKILL 机制(规则的模块化) 当规则内容过多时,抽象可复用的 Skill 文件。好处是 AI 按需加载,不会每次都把全局规则加入上下文,大大减少 token 用量

规则中引用 SKILL 的示例:

## 3. 核心哲学:测试先行 (TDD)

参考 `.trae/skills/tdd-first/SKILL.md` 中的测试驱动开发规范。

所有新功能开发或 Bug 修复必须遵循 **「红-绿-重构」** 循环。**严禁**在没有对应测试用例的情况下提交业务逻辑代码。
---

## 4. 响应格式

参考 `.trae/skills/response-standard/SKILL.md` 中的响应格式规范。

**[强制]** 所有接口统一返回 JSON,使用 `@/utils/response` 中的工具函数生成响应。

---

步骤 2:需求分析

如果你很清楚自己要做什么,可以直接下发任务。但如果只有模糊想法,建议先和 AI 一起做需求分析。

推荐提示词

你好!现在的任务是:我们要从零开始设计并实现 `hono.js boilerplate`。
 
你现在是资深的 Node.js 工程师。我有一个初步的想法,需要你通过向我提问,帮助我澄清需求、挖掘边缘场景,最终目标是理清我做一个通用后端功能的脚手架需要实现哪些功能。并按顺序输出一个实现这些功能的大纲。
 
请开始你的提问。

效果: Claude 会像资深 PM 一样向你提问,你逐一回答这些问题后,AI 会生成一份完整的功能清单文档。


步骤 3:测试先行 + AI 执行代码

这是整个工作流的核心环节,必须让 AI 严格按照 TDD 测试方案执行代码。

执行策略

  1. 任务拆解 → 将需求拆分为最小可测试单元
  2. 红阶段 → 先编写测试用例(此时测试应当失败)
  3. 绿阶段 → 编写最小化实现代码,使测试通过
  4. 重构阶段 → 在测试通过的基础上,优化代码结构和性能

关键约束

在全局规则中强制要求 AI:

  • ✅ 任何业务代码提交前,必须有对应的单元测试覆盖
  • ✅ 测试用例需包含正常场景 + 边界场景 + 异常场景
  • ✅ 测试执行必须通过,覆盖率不低于 80%
  • ✅ 严禁为了赶进度而跳过测试环节

步骤 4:代码审核 + 规则反馈循环

AI 执行代码后,进入审核阶段,分为 AI 自动审核和人工审核两部分。

AI 自动审核

AI 每次完成任务时,需要自动进行:

  • Eslint 校验
  • TypeScript 类型校验

如果报错,AI 自己修复直到通过(可以限制修复次数,避免死循环)

人工审核

前期必须进行人工审核,一旦发现问题,思考如何抽象成规则或 Skill,避免 AI 再次犯错。

核心发现: 你会发现后期 AI 执行的效果会越来越好。对于 CRUD 场景,基本不需要人审核了,大概看一下产出代码就知道没问题。


步骤 5:持续迭代与精准化

随着迭代轮次增加,形成正向循环:

迭代轮数 ↑
    ↓
规则精确度 ↑  → AI 执行准确率 ↑
    ↓
处理边界场景的能力 ↑
    ↓
人工介入频率 ↓
    ↓
开发效率 ↑

这就是 AI 时代的竞争力所在: 不是盲目信任 AI,而是通过不断的反馈循环来「教」AI,逐步固化和优化工作规范。


我举个自己实践中的迭代案例:

  • 例如我让 ai 实现超时中间件的时候,ai 是自己原生实现的,然后我感觉这种很常用的功能一般都有现成的库,就查了一下,果然是有 'hono/timeout' 这个库,然后就在全局规则加入了类似:”优先使用社区成熟,稳定的库解决问题“。
  • 例如我在设计后端的 url 的时候,突然想起 K8s 有一个类似的 url 设计规范,可以跟权限结合起来,例如,/api/v1/roles/{roleId},其中 roles 代表就是资源,roleId 代表的是子资源。
resources: ["roles"] # 操作的资源
verbs: ["get"] # 操作类型,增删改查
resourceNames: ["{roleId}"] # 可选(精确到某个子资源)

简单就是说一个 url 代表对什么资源的什么操作。

HTTP 请求RBAC
GET /roles/{id}verbs: ["get"]
GET /rolesverbs: ["list"]
POST /rolesverbs: ["create"]
DELETE /roles/{id}verbs: ["delete"]

然后就可以结合我们的 rbac 模型(权限模型),用来标识一个权限是什么,简单来说就是资源名 + 操作,就能标识一个权限。

然后我干脆让 ai 把这个 url 规范抽象为一个 skill,下次 ai 定义 url 的时候就会调用这个规则。

  • 还有很多例子就不一一列举了...

需要注意的是,字节内部的复杂度是更高的,我们后期也会探索,例如字节内部还有:

  • 多 Agent 系统,比如主 Agent 来负责 plan 的制定,有 Coder Agent 负责编码,还有测试 Agent, test Agent 等等,这种多 Agent 协作,我个人估计字节迟早会推出一个开源库让我们使用的,所以不必着急。
  • 评测体系,会对 AI 输出质量进行打分,也就是评测 AI 的输出质量,这条我们目前这一版是靠人工来识别,但我觉得还好,前期人工介入,后期规则越来越好,模型能力越来越强,就可以进入 AI 自我评测阶段了。
  • 可观测性体系:就是对于 AI 写错的地方,是否能知道哪一步错了,然后根据这个让 AI 自动修正 Prompt,然后自动修正全局规则或者抽象为 SKILL。

上面要这么玩,目前个人还是很难的,需要大的平台支持,本文主要是先走通一个小循环,也欢迎大家一起交流(如果觉得不错,感谢点赞关注哦,欢迎入群交流~)。

项目实战:通用后端脚手架

技术栈: Hono.js + Drizzle ORM + PostgreSQL 数据库

前置声明: 整个工作流仅使用免费版工具(Trace + GLM5/豆包模型)即可高质量完成,充分说明该方法论的实际效果令人满意,而非纸上谈兵。

源码获取: 因外链限流问题,需要的朋友可以联系我获取完整代码(github)

脚手架目前的架构图如下:

image.png

第二部分:企业级技术要点

分享上述实战过程中,网上 Node.js 教程很少提及的企业级最佳实践。

优雅关闭(Graceful Shutdown)

无论部署在 Kubernetes、Docker Compose 还是物理机,都需要实现优雅关闭逻辑。

为什么重要?

当应用报错或升级时,容器编排系统会执行关闭流程:

应用故障/升级 → 容器启动关闭流程
  ↓
向 PID 1 进程发送 SIGTERM 信号
  ↓
开启倒计时(默认 10 秒)
  ↓
如果 10 秒后进程未退出,发送 SIGKILL(强制杀死)

问题场景

例:电商扣款业务
 
1. 扣除用户余额 ✅
2. Docker 信号来了,进程被强杀 🔥
3. 积分增加 ❌(未执行)
 
结果:用户钱扣了但没到账,投诉炸裂。

问题根源: Docker 强杀是瞬间的,Node.js 无法执行完事件循环中的剩余回调。

解决方案

优雅关闭可以让你在收到信号后,停止接收新请求,但把内存中已排队的写操作执行完。同时及时释放系统资源(如数据库连接),避免占满最大连接数。


链路追踪(TraceId)

TraceId 是请求在系统中的唯一标识符,从进入系统到返回响应,始终伴随整个生命周期。

为什么需要?

场景:前端用户报错
用户说:"我提交表单后,收到错误 ID:abc123def456"
 
后端排查:
❌ 日志有 1000 条,怎么找到那条错误?
✅ 按 traceId = abc123def456 过滤,立即定位问题

Node.js 的特殊性

与 Java/Go 等多线程模型相比,Node.js 的单进程模型在处理 TraceId 时有本质区别:

语言模型上下文隔离方案难度
Java/Go多线程/协程ThreadLocal⭐ 简单
Node.js单进程AsyncLocalStorage⭐⭐⭐ 复杂

错误做法

方案 1:全局变量

let traceId;  // 全局变量
 
app.use((req, res, next) => {
  traceId = generateId();  // 请求 A 的 traceId
  next();
});
 
// 问题:请求 B 来了,traceId 被覆盖,日志全乱

方案 2:函数传参

// controller → service → dao,每层都要传 traceId
// 代码极其丑陋,难以维护
 
async function getUserOrder(traceId, userId) {
  const user = await getUser(traceId, userId);
  const order = await getOrder(traceId, user.id);
  return { user, order };
}

正确方案:AsyncLocalStorage

Node.js 官方在 async_hooks 基础上,封装了更高级、性能更优的 API:

import { AsyncLocalStorage } from 'async_hooks';
 
const traceIdStorage = new AsyncLocalStorage();
 
// 在请求中间件中创建隔离上下文
app.use((req, res, next) => {
  const traceId = generateId();
  
  // 在当前上下文中存储 traceId(自动隔离)
  traceIdStorage.run(traceId, () => {
    next();
  });
});
 
// 任何地方都可以取,不用传参
function getTraceId() {
  return traceIdStorage.getStore();
}
 
// 使用示例
async function getUserOrder(userId) {
  const traceId = getTraceId();  // 直接取,无需传参
  logger.info(`[${traceId}] Fetching user`, { userId });
  
  const user = await getUser(userId);
  logger.info(`[${traceId}] User fetched`, { userId: user.id });
  
  return user;
}

日志集成

const logger = createLogger((level, msg, meta) => {
  const traceId = getTraceId();
  const logEntry = {
    timestamp: new Date().toISOString(),
    level,
    traceId,    // 自动注入
    message: msg,
    ...meta,
  };
  console.log(JSON.stringify(logEntry));
});

TDD 测试驱动开发

TDD 是企业级后端项目的核心质量保障手段,在 AI 协作开发模式下更是确保代码质量的关键。

核心流程:红-绿-重构

  1. 红阶段 → 编写测试用例,预期会失败(功能未实现)
  2. 绿阶段 → 实现最小化代码,使测试通过
  3. 重构阶段 → 优化代码结构,保持测试通过

Hono.js 项目中的实践

采用 Hono 原生集成测试方案,结合 Vitest 测试框架:

// test/user.test.ts
import { describe, it, expect } from 'vitest';
import app from '../src/app';
 
describe('User API', () => {
  it('should return 404 for non-existent user', async () => {
    const res = await app.request('/api/users/9999', {
      method: 'GET'
    });
    
    expect(res.status).toBe(404);
    const data = await res.json();
    expect(data.code).toBe(0);
    expect(data.message).toBe('User not found');
  });
  
  it('should create a new user', async () => {
    const res = await app.request('/api/users', {
      method: 'POST',
      body: JSON.stringify({
        name: '测试用户',
        email: 'test@example.com',
        password: 'password123'
      }),
      headers: {
        'Content-Type': 'application/json'
      }
    });
    
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.code).toBe(1);
    expect(data.data.name).toBe('测试用户');
  });
});

表格驱动测试

对于多分支逻辑和边界情况,采用表格驱动测试风格:

describe('User Validation', () => {
  const testCases = [
    {
      desc: '缺少必填字段',
      body: { name: '测试用户' },
      expectedStatus: 400,
      expectedMessage: 'Email is required'
    },
    {
      desc: '邮箱格式错误',
      body: { name: '测试用户', email: 'invalid-email' },
      expectedStatus: 400,
      expectedMessage: 'Invalid email format'
    },
    {
      desc: '密码长度不足',
      body: { name: '测试用户', email: 'test@example.com', password: '123' },
      expectedStatus: 400,
      expectedMessage: 'Password must be at least 6 characters'
    }
  ];
  
  test.each(testCases)('$desc', async ({ body, expectedStatus, expectedMessage }) => {
    const res = await app.request('/api/users', {
      method: 'POST',
      body: JSON.stringify(body),
      headers: { 'Content-Type': 'application/json' }
    });
    
    expect(res.status).toBe(expectedStatus);
    const data = await res.json();
    expect(data.message).toBe(expectedMessage);
  });
});

请求超时处理

请求超时处理是后端服务稳定性的重要保障,可以防止长时间运行的请求占用系统资源。

为什么需要?

  • 保护用户体验:与其让用户等待 30 秒,不如在 5 秒内返回"请求超时"
  • 防止系统雪崩:大量超时请求堆积会导致 CPU/内存被迅速耗尽

API 接口级超时

利用 Hono 自带的 timeout 中间件:

import { timeout } from 'hono/timeout'
 
// 1. 全局配置:所有请求默认 5 秒超时
app.use('/api/*', timeout(5000))
 
// 2. 局部配置:针对耗时操作,允许更长时间
app.get('/api/export', timeout(30000), async (c) => {
  // 执行耗时操作...
  return c.json({ success: true })
})
 
// 3. 自定义超时后的逻辑
const customTimeout = timeout(5000, {
  onTimeout: (c) => {
    return c.json({ code: 0, message: '服务器繁忙,请稍后再试' }, 408)
  }
})

数据库级超时

API 层超时只是"切断了回传给用户的路",但数据库内部的任务可能仍在运行。需要更细粒度的控制:

// Drizzle ORM 配置:通过底层驱动设置超时
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
 
const queryClient = postgres(process.env.DATABASE_URL, {
  timeout: 5,          // 建立连接超时 (秒)
  idle_timeout: 20,    // 空闲连接释放
  max_lifetime: 60 * 30 // 连接存活最大时间
})
 
// 在业务代码中手动控制单次查询超时
async function getSlowData() {
  return await db.select().from(users).execute();
}

全局错误处理

在复杂的后端系统中,错误可能来自业务逻辑、数据库约束、第三方 API 失败或语法错误。没有统一处理的话,返回给前端的可能是难看的堆栈信息。

设计原则

  1. 收口原则 → 业务代码通过 throw 抛出错误,由顶层中间件统一拦截处理
  2. 分类分级 → 区分"预期内错误"和"预期外错误"
  3. 安全性 → 生产环境下严禁将详细 Stack 返回给客户端

实现方案

步骤 1:定义标准错误类

// src/utils/errors.ts
export class AppError extends Error {
  constructor(
    public statusCode: number,
    public message: string,
    public code: number = 0 // 自定义业务状态码
  ) {
    super(message);
    this.name = 'AppError';
  }
}

步骤 2:配置全局捕获钩子

import { Hono } from 'hono';
import { AppError } from './utils/errors';
 
const app = new Hono();
 
app.onError((err, c) => {
  const traceId = c.get('traceId') || 'unknown';
  
  // 1. 处理已知业务异常
  if (err instanceof AppError) {
    return c.json({
      code: err.code,
      message: err.message,
      traceId
    }, err.statusCode as any);
  }
 
  // 2. 处理参数校验错误
  if (err.name === 'ZodError') {
    return c.json({
      code: 400,
      message: '参数验证失败',
      details: err,
      traceId
    }, 400);
  }
 
  // 3. 处理未知错误
  console.error(`[Fatal Error] [${traceId}]:`, err);
 
  return c.json({
    code: 500,
    message: process.env.NODE_ENV === 'production' 
      ? '服务器内部错误' 
      : err.message,
    traceId
  }, 500);
});

步骤 3:业务层使用

export async function deleteUser(id: string) {
  const user = await db.findUser(id);
  
  if (!user) {
    throw new AppError(404, '用户不存在', 10001);
  }
  
  return db.delete(id);
}

RBAC 权限控制

RBAC(基于角色的访问控制)是中后台系统最通用的权限模型。通过"用户-角色-权限"的关联,实现权限的解耦。

为什么不直接判断角色?

如果代码里写 if (user.role === 'admin'),当新增一个"超级编辑"角色也需要此权限时,得修改所有代码。判断权限点(Permission)而非角色名,才是系统扩展性的关键。

核心概念

  • 用户 (User) → 拥有一个或多个角色
  • 角色 (Role) → 如 Admin、Editor、Viewer
  • 权限 (Permission) → 如 user:create、order:delete

实现方案

步骤 1:定义数据模型

// 简化版 schema
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  role: text('role').default('viewer'),
});
 
// 权限映射表
const ROLE_PERMISSIONS = {
  admin: ['user:all', 'post:all'],
  editor: ['post:edit', 'post:create'],
  viewer: ['post:read'],
} as const;

步骤 2:实现 RBAC 中间件

// middleware/rbac.ts
import { createMiddleware } from 'hono/factory';
import { AppError } from '../utils/errors';
 
export const checkPermission = (requiredPermission: string) => {
  return createMiddleware(async (c, next) => {
    const user = c.get('user');
    
    if (!user) {
      throw new AppError(401, '未授权访问');
    }
 
    const userPermissions = ROLE_PERMISSIONS[user.role] || [];
    
    // 支持通配符或精确匹配
    const hasPermission = userPermissions.some(p => 
      p === requiredPermission || p === `${requiredPermission.split(':')[0]}:all`
    );
 
    if (!hasPermission) {
      throw new AppError(403, '权限不足,无法执行此操作');
    }
 
    await next();
  });
};

步骤 3:在路由层应用

const api = new Hono();
 
// 只有拥有 post:create 权限的角色才能访问
api.post('/posts', checkPermission('post:create'), async (c) => {
  return c.json({ message: '发布成功' });
});
 
// 管理员专属接口
api.get('/admin/stats', checkPermission('user:all'), async (c) => {
  return c.json({ stats: '...' });
});

日志轮转

在生产环境中,如果所有日志都无限制地写入同一个文件,最终会导致磁盘爆满和日志文件难以打开。

核心目的

  1. 防止单个文件过大(难以检索、占用磁盘空间)
  2. 自动化归档(按日期分类)
  3. 过期清理(例如只保留最近 14 天的日志)

实现方案:Winston + Daily Rotate File

import winston from 'winston';
import 'winston-daily-rotate-file';
 
const transport = new winston.transports.DailyRotateFile({
  filename: 'logs/application-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  zippedArchive: true,           // 历史日志压缩
  maxSize: '20m',                // 单个文件超过 20MB 也会切分
  maxFiles: '14d',               // 只保留最近 14 天的日志
  level: 'info',
});
 
const logger = winston.createLogger({
  transports: [
    transport,
    new winston.transports.Console()
  ]
});

应对 DDoS 攻击

DDoS 攻击的本质是发大量垃圾请求,导致带宽占满、CPU/内存耗尽、连接数耗尽。

现实: 普通企业很难防住大规模 DDoS,目的是提高攻击成本。

限流

在接入层(Nginx)—— 粗筛

性能极高,在流量进入 Node.js 之前就拦截:

limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req zone=api burst=20;

在应用层(Middleware)—— 精滤

灵活度高,根据业务维度限流:

// 限制某个登录用户每分钟只能发 5 条评论
app.use(rateLimit({
  windowMs: 60 * 1000,
  max: 5,
  keyGenerator: (c) => c.get('user').id
}));

限制请求体大小

防止内存溢出 (OOM):

// 攻击场景:发送 2GB 垃圾字符的 JSON POST 请求
// 后果:Node.js 进程尝试分配 2GB 内存,很快就 Out of Memory
 
// 解决:在 Nginx 层配置
client_max_body_size 1m;

Helmet 安全头

Helmet 通过设置各种 HTTP 响应头,自动防御常见的 Web 漏洞(XSS、点击劫持、MIME 类型嗅探等)。

性价比最高的安全加固方案。

Hono.js 官方支持 hono/helmet 中间件,在入口文件 src/app.ts 中引入即可:

import { helmet } from 'hono/helmet';
 
app.use(helmet());

告警机制

告警机制是"及时发现问题"的关键,通过监控关键指标,在异常情况下主动通知相关人员。

告警规则设计

根据应用的 SLA,定义不同严重等级:

export const alertRules = [
  {
    name: 'High Error Rate',
    condition: 'error_rate > 5%',
    severity: 'critical',
    duration: '5m',
    action: 'page_oncall',  // 立即电话/Slack 通知
  },
  {
    name: 'High Response Latency',
    condition: 'p95_latency > 1000ms',
    severity: 'warning',
    duration: '10m',
    action: 'send_to_slack',
  },
  {
    name: 'Database Connection Pool Exhausted',
    condition: 'db_connections > 90%',
    severity: 'critical',
    duration: '1m',
    action: 'page_oncall',
  }
];

与监控系统集成

使用 Prometheus + Alertmanager:

# prometheus.yml
global:
  scrape_interval: 15s
 
scrape_configs:
  - job_name: 'hono-app'
    static_configs:
      - targets: ['localhost:3000']
    metrics_path: '/metrics'
 
alerting:
  alertmanagers:
    - static_configs:
        - targets: ['localhost:9093']

多渠道通知

export async function sendAlert(
  title: string,
  message: string,
  severity: 'critical' | 'warning' | 'info'
) {
  const timestamp = new Date().toISOString();
 
  // 1. Slack 通知
  if (severity === 'critical' || severity === 'warning') {
    await axios.post(process.env.SLACK_WEBHOOK_URL, {
      text: `[${severity.toUpperCase()}] ${title}`,
      attachments: [{
        color: severity === 'critical' ? 'danger' : 'warning',
        text: message,
        ts: Math.floor(new Date().getTime() / 1000),
      }],
    });
  }
 
  // 2. 邮件通知(仅限 critical)
  if (severity === 'critical') {
    await sendEmail({
      to: process.env.ALERT_EMAIL,
      subject: `🚨 CRITICAL: ${title}`,
      html: `<h2>${title}</h2><p>${message}</p><p>${timestamp}</p>`,
    });
  }
 
  // 3. 记录到数据库
  await db.insert(alerts).values({
    title,
    message,
    severity,
    createdAt: new Date(),
  });
}

性能测试

性能测试是确保应用在生产环境中稳定运行的最后一道防线。

基准测试(Benchmarking)

使用 Autocannon 进行简单的吞吐量和延迟测试:

# 安装 Autocannon
npm install -g autocannon
 
# 基准测试:100 并发,持续 30 秒
autocannon -c 100 -d 30 http://localhost:3000/api/users
 
# 输出示例
# Req/Sec: 1234
# Latency: { mean: 45.2, p50: 42, p95: 78, p99: 120 }

压力测试(Load Testing)

使用 K6 模拟真实用户行为:

// load-test.js
import http from 'k6/http';
import { check, sleep, group } from 'k6';
 
export const options = {
  stages: [
    { duration: '2m', target: 100 },
    { duration: '5m', target: 100 },
    { duration: '2m', target: 200 },
    { duration: '5m', target: 200 },
    { duration: '2m', target: 0 },
  ],
};
 
export default function () {
  group('User API', () => {
    // 测试获取用户列表
    let listRes = http.get('http://localhost:3000/api/users');
    check(listRes, {
      'list status is 200': (r) => r.status === 200,
      'list response time < 100ms': (r) => r.timings.duration < 100,
    });
 
    // 测试创建用户
    let createRes = http.post('http://localhost:3000/api/users', {
      name: `user-${__VU}-${__ITER}`,
      email: `user-${__VU}-${__ITER}@example.com`,
      password: 'password123',
    });
    check(createRes, {
      'create status is 200': (r) => r.status === 200,
    });
 
    sleep(1);
  });
}

运行压力测试:

# 安装 K6
npm install -g k6
 
# 执行测试
k6 run load-test.js

数据库性能测试

// src/tests/db-performance.test.ts
import { describe, it, expect } from 'vitest';
import { db } from '../db';
 
describe('Database Performance', () => {
  it('should query 10k users in < 500ms', async () => {
    const start = performance.now();
    const users = await db.query.users.findMany({ limit: 10000 });
    const duration = performance.now() - start;
 
    expect(users.length).toBe(10000);
    expect(duration).toBeLessThan(500);
  });
 
  it('should create 1k users in batch < 2s', async () => {
    const data = Array.from({ length: 1000 }, (_, i) => ({
      name: `user-${i}`,
      email: `user-${i}@example.com`,
      password: 'hashed-password',
    }));
 
    const start = performance.now();
    await db.insert(users).values(data);
    const duration = performance.now() - start;
 
    expect(duration).toBeLessThan(2000);
  });
});

数据持久化与备份

数据持久化本质上解决的是:当系统崩溃、误操作、甚至被攻击时,数据还能不能恢复?

重要认知: 数据库 ≠ 数据安全。数据库只是"存储",而备份 + 恢复能力才是安全的核心。

备份脚本示例

#!/bin/bash
set -o pipefail  # 核心:捕获管道中任何一步的错误
 
DB_NAME="your_db"
BACKUP_FILE="/data/backups/db_$(date +%Y%m%d).sql.gz"
 
# 执行备份
pg_dump -U admin -d $DB_NAME | gzip -1 > $BACKUP_FILE
 
# 检查备份是否成功
if [ $? -ne 0 ]; then
    echo "❌ 备份失败!清理空文件..."
    rm -f $BACKUP_FILE
    # 调用告警机制
    # sendAlert "Database Backup Failed" "pg_dump connection error" "critical"
    exit 1
else
    echo "✅ 备份成功"
fi

可观测性(Observability)

可观测性与监控的区别:

  • 监控 → 告诉你"系统出了问题"(基于预定义的指标和阈值)
  • 可观测性 → 告诉你"系统为什么出了问题"(通过日志、指标、链路追踪)

可观测性的三大支柱

支柱 1:结构化日志

// src/utils/logger.ts
import winston from 'winston';
 
const logger = winston.createLogger({
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.errors({ stack: true }),
    // 自定义格式化,确保输出为结构化 JSON
    winston.format.printf(({ timestamp, level, message, ...meta }) => {
      return JSON.stringify({
        timestamp,
        level,
        traceId,
        message,
        ...meta,
      });
    })
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' }),
  ],
});

支柱 2:指标收集(Metrics)

使用 Prometheus 收集性能指标:

// src/utils/metrics.ts
import promClient from 'prom-client';
 
// 创建指标
export const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request latency',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.1, 0.5, 1, 2, 5],
});
 
export const dbQueryDuration = new promClient.Histogram({
  name: 'db_query_duration_seconds',
  help: 'Database query latency',
  labelNames: ['operation', 'table'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1],
});
 
// 暴露 Prometheus 指标端点
export function registerMetricsRoute(app: Hono) {
  app.get('/metrics', (c) => {
    return c.text(promClient.register.metrics());
  });
}

支柱 3:链路追踪(Traces)

已在前面的 TraceId 部分详细说明。


最后

这套工作流的核心理念就是 持续反馈、不断优化。感谢您的阅读,建议点赞收藏,我们下期再见!