mcpField
A
api-performance-api-performance

api-performance-api-performance

github.com/agents-inc开发2680 次安装2 天前更新
代码仓库

关于 api-performance-api-performance

面向 API 和数据库服务开发者的后端性能优化技能,强调先测量再调优,涵盖查询、缓存、连接管理、异步处理与分页。

主要功能

  1. 01数据库索引与查询调优
  2. 02连接池及安全释放
  3. 03通过预加载避免 N+1
  4. 04基于 TTL 的缓存模式
  5. 05事件循环与工作线程指南
  6. 06键集分页

适用场景

  1. 01排查缓慢的数据库查询
  2. 02减少重复数据读取负载
  3. 03避免 CPU 密集任务阻塞请求
  4. 04为大型数据集分页

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill api-performance-api-performance

安装后在 Claude Code 中输入/api-performance-api-performance确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
async.md
caching.md
core.md

详细介绍

后端性能优化

快速指南: 通过优化数据库查询(索引、预处理语句、避免 N+1)、采用缓存策略(cache-aside、write-through)、使用连接池和非阻塞异步模式来优化后端性能。优化前务必先进行度量——运行 EXPLAIN ANALYZE、检查事件循环延迟,并跟踪缓存命中率,再考虑增加复杂性。


<critical_requirements>

重要:使用此技能前须知

(必须始终使用 finally 代码块将数据库连接释放回连接池)

(必须使用预加载或批处理(DataLoader)来防止 N+1 查询——绝不能在循环中延迟加载)

(必须为所有缓存数据设置 TTL,以防止数据过时和内存耗尽)

(必须将 CPU 密集型工作交由 Worker Threads 处理——阻塞事件循环会降低所有请求的性能)

</critical_requirements>


详细资源:


自动检测: 连接池、查询优化、数据库索引、N+1、缓存、缓存失效、预处理语句、工作线程、事件循环、CPU 密集型、延迟、吞吐量、性能调优、EXPLAIN ANALYZE、键集分页、cache-aside、write-through

适用场景:

  • 数据库查询耗时超过 100ms
  • 高流量端点重复获取数据
  • API 响应包含多个相关实体(存在 N+1 风险)
  • CPU 密集型操作阻塞请求处理
  • 需要通过缓存降低数据库负载

不适用场景:

  • 尚未进行度量就过早优化
  • 低流量的简单 CRUD(增加复杂性却没有收益)
  • 频繁变化且必须始终保持最新的数据(缓存会导致数据过时)
  • 开发/调试(缓存会掩盖问题)

涵盖的关键模式:

  • 数据库索引策略(复合、部分、覆盖)
  • 确保连接释放的连接池
  • 防止 N+1 查询(预加载、DataLoader)
  • 缓存策略(cache-aside、write-through、失效处理)
  • 事件循环优化(异步 I/O、setImmediate 分块处理)
  • 使用工作线程处理 CPU 密集型操作
  • 大型数据集的键集分页

理念

后端性能优化遵循一个核心原则:先度量,再优化。过早优化会浪费开发时间,并在缺乏收益证据的情况下增加复杂性。

后端性能的三大支柱:

  1. 数据库优化 - 索引、查询规划、防止 N+1、分页
  2. 缓存 - 使用有 TTL 限制的缓存,减少重复的高开销操作
  3. 异步效率 - 绝不阻塞事件循环

适合优化的时机:

  • 响应时间超过 SLA 阈值
  • 数据库 CPU/内存接近上限
  • 指标显示存在具体瓶颈(EXPLAIN ANALYZE、事件循环延迟)
  • 负载测试发现扩展性问题

不适合优化的时机:

  • “将来可能会变慢”(过早优化)
  • 优化冷路径(很少执行的代码)
  • 尚未通过性能分析确定实际瓶颈

核心模式

模式 1:连接池与确保释放

连接池通过重复使用数据库连接,而不是为每个请求创建新连接。PostgreSQL 握手需要 20-30ms——连接池可消除此开销。

关键规则:

  • 对简单查询使用 pool.query()(自动管理连接生命周期)
  • 对事务,使用 pool.connect() 手动获取连接,并且始终在 finally 中释放
  • 监听连接池错误(空闲客户端仍可能发出错误)
// Transaction with guaranteed connection release
async function createUserWithProfile(
  userData: UserData,
  profileData: ProfileData,
) {
  const client = await pool.connect();
  try {
    await client.query("BEGIN");
    const userResult = await client.query(
      "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
      [userData.name, userData.email],
    );
    await client.query("INSERT INTO profiles (user_id, bio) VALUES ($1, $2)", [
      userResult.rows[0].id,
      profileData.bio,
    ]);
    await client.query("COMMIT");
    return userResult.rows[0];
  } catch (error) {
    await client.query("ROLLBACK");
    throw error;
  } finally {
    client.release(); // CRITICAL: Always release back to pool
  }
}

为什么好: finally 可确保即使发生错误也会释放连接,避免连接池耗尽

完整的连接池配置、容量规划公式和外部连接池工具指南,请参阅 examples/core.md。


模式 2:防止 N+1 查询

N+1 问题是指获取 N 条记录时,会因关联数据而触发 N 次额外查询。获取 100 条记录,就会产生 101 次数据库往返。

两种解决方案:

  1. 预加载(ORM .with())——通过一次带 JOIN 的查询加载已知关联
  2. DataLoader——将 .load() 调用批处理为每个事件循环周期一次查询,非常适合 GraphQL
// Eager loading: single query fetches jobs + companies + skills
const jobs = await db.query.jobs.findMany({
  where: and(eq(jobs.isActive, true), isNull(jobs.deletedAt)),
  with: {
    company: { with: { locations: true } },
    jobSkills: { with: { skill: true } },
  },
});
// BAD: N+1 anti-pattern -- one query per job
for (const job of jobs) {
  job.company = await db.query.companies.findFirst({
    where: eq(companies.id, job.companyId),
  });
}

为什么不好: 获取 jobs 需要 1 次查询,获取 companies 需要 N 次查询;延迟会随数据量线性增长

DataLoader 批处理模式请参阅 examples/core.md。


模式 3:数据库索引

索引通过避免全表扫描来加快查询速度。为 WHERE、JOIN 和 ORDER BY 子句中使用的列建立索引。

// Strategic indexes on a table definition
export const jobs = pgTable(
  "jobs",
  {
    id: uuid("id").primaryKey().defaultRandom(),
    companyId: uuid("company_id").notNull(),
    country: varchar("country", { length: 100 }),
    employmentType: varchar("employment_type", { length: 50 }),
    isActive: boolean("is_active").default(true),
    createdAt: timestamp("created_at").defaultNow(),
    deletedAt: timestamp("deleted_at"),
  },
  (table) => [
    // Composite index for common filter combination
    index("jobs_country_employment_idx").on(
      table.country,
      table.employmentType,
    ),
    // Partial index -- only indexes active non-deleted jobs
    index("jobs_active_idx")
      .on(table.isActive, table.createdAt)
      .where(sql`${table.deletedAt} IS NULL`),
    // Foreign key index for JOIN performance
    index("jobs_company_id_idx").on(table.companyId),
  ],
);

索引选择框架:

列的用途索引类型适用场景
WHERE 等值条件B-tree(默认)高选择性列
WHERE 范围条件(>, <, BETWEEN)B-tree日期范围、数值范围
WHERE 多列条件复合索引查询始终同时按相同列进行筛选
WHERE 子集条件部分索引大多数查询按 active/non-deleted 筛选
全文搜索GIN/GiST使用 LIKE、tsvector 进行文本搜索
JSON 字段访问GINJSONB 列查询

复合索引列顺序: 等值条件列在前,范围条件列在后;高选择性列优先。

EXPLAIN ANALYZE 示例、索引监控查询和未使用索引检测,请参阅 examples/core.md。


模式 4:带 TTL 的旁路缓存

最常见的缓存模式。先检查缓存,未命中时从数据库获取,并设置 TTL 存储。

const CACHE_TTL_SECONDS = 300;
const CACHE_PREFIX = "app:user";

async function getUserById(userId: string): Promise<User | null> {
  const cacheKey = `${CACHE_PREFIX}:${userId}`;
  const cached = await cacheClient.get(cacheKey);
  if (cached) return JSON.parse(cached) as User;

  const user = await db.query.users.findFirst({ where: eq(users.id, userId) });
  if (!user) return null;

  await cacheClient.set(cacheKey, JSON.stringify(user), {
    EX: CACHE_TTL_SECONDS,
  });
  return user;
}

为什么好: TTL 可防止过期数据不断累积,带命名空间的键可避免冲突,缓存命中时会提前返回

写穿缓存、基于标签的失效、键策略和 TTL 指南,请参阅 examples/caching.md。


模式 5:使用 Worker Threads 处理 CPU 密集型操作

Node.js 使用单个线程执行 JavaScript。CPU 密集型工作会阻塞所有并发请求。

经验法则:

CPU 耗时解决方案原因
< 50ms保持在主线程执行Worker 开销不值得
50-500ms使用 setImmediate 分块在各分块之间让出执行权给事件循环
> 500msWorker Threads完全卸载到独立线程
// Chunked processing with setImmediate -- yields to event loop between batches
const CHUNK_SIZE = 100;

async function processLargeArray(items: Item[]): Promise<ProcessedItem[]> {
  const results: ProcessedItem[] = [];
  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);
    for (const item of chunk) {
      results.push(expensiveTransform(item));
    }
    if (i + CHUNK_SIZE < items.length) {
      await new Promise((resolve) => setImmediate(resolve));
    }
  }
  return results;
}

Worker 池实现、使用 p-limit 控制并发以及事件循环延迟监控,请参阅 examples/async.md。


模式 6:大型数据集的键集分页

OFFSET 分页会扫描所有之前的行——当 OFFSET 为 100,000 时,数据库会读取并丢弃 100,000 行。键集分页使用游标,实现恒定时间的性能。

-- BAD: OFFSET scans all previous rows
SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 100000;

-- GOOD: Keyset pagination -- constant time regardless of position
SELECT * FROM products
WHERE id > :last_seen_id
ORDER BY id LIMIT 20;

适用 offset 的情况: 小型数据集(< 100k 行)、需要总数、需要随机访问页面。

适用键集分页的情况: 大型数据集、无限滚动、实时数据(插入数据不应导致重复)。

参见 examples/core.md,了解这两种模式的完整 TypeScript 实现。


<red_flags>

危险信号

高优先级问题:

  • 未释放连接——连接永远不会返回连接池,导致连接池耗尽和应用程序挂起
  • 循环中的 N+1 查询——逐个获取关联数据,而非预先加载,会严重损害性能
  • 阻塞事件循环——同步 I/O 或 CPU 密集型工作会阻塞所有并发请求
  • 缓存没有 TTL——无限增长的缓存会导致内存耗尽,或提供永不过期的陈旧数据
  • 对大型表进行全表扫描——WHERE/JOIN 列缺少索引

中优先级问题:

  • 外键没有索引——JOIN 和 ON DELETE CASCADE 会变慢
  • 索引过多——每个索引都会降低写入速度;使用 pg_stat_user_indexes 删除未使用的索引
  • 缓存键冲突——通用键会导致向错误用户返回错误数据
  • 对大型表使用 offset 分页——OFFSET 会扫描所有之前的行;请使用键集分页
  • 并行度不受限制——对 10,000 个项目调用 Promise.all 会使下游服务不堪重负

注意事项与边缘情况:

  • EXPLAIN 显示估算值;EXPLAIN ANALYZE 显示实际值——务必使用 ANALYZE 获取真实的性能数据
  • 复合索引 (a, b) 无法帮助只按 b 过滤的查询——列的顺序很重要
  • 对索引列应用函数(例如 YEAR(created_at))会阻止使用索引——请改写为范围条件
  • DataLoader 在请求内缓存——不要跨请求复用,否则会得到陈旧数据
  • Worker 线程的启动开销约为 30ms——不要用于快速操作
  • 如果将连接池的 idleTimeoutMillis 设置得过短,可能会出现 “connection terminated unexpectedly”
  • SELECT * 会获取不必要的数据,并阻止覆盖索引优化——请指定具体列
  • 使用 EX 选项执行缓存 SET 会替换现有 TTL——再次调用 SET 会重置过期计时器

</red_flags>


<critical_reminders>

重要提醒

(必须始终使用 finally 块将数据库连接释放回连接池)

(必须使用预先加载或批处理(DataLoader)来防止 N+1 查询——切勿在循环中延迟加载)

(必须为所有缓存数据设置 TTL,以防止陈旧数据和内存耗尽)

(必须将 CPU 密集型工作转移到 Worker 线程——阻塞事件循环会降低所有请求的性能)

不遵循这些规则会导致连接池耗尽、N+1 查询造成性能下降、无界缓存导致内存泄漏,以及事件循环阻塞并影响所有并发请求。

</critical_reminders>

评论

支持 Markdown