Skip to main content

第十篇:规模化 — 从 200 个客户到 10000 个

进入信号:部署变慢(>5 分钟)、一个模块挂了整个站跟着挂、数据库 CPU 长期高负载、有大客户问「能扛住多少用户?」。

本篇解决的问题:单体应用的极限在哪里?怎么拆服务?数据库怎么扩展?多租户架构怎么升级?怎么从服务 200 个客户变成服务 10000 个?


单体应用的极限

200 个客户的时候,系统开始暴露各种瓶颈。不是一下子全暴露的,是一个接一个:

  1. Build 时间从 2 分钟涨到 8 分钟:项目代码量从最初的 2000 行变成了 30000 行。Next.js 的 Build 每次要编译所有页面。

  2. 数据库 CPU 长期 60%,高峰期 90%:200 个客户里有 5 个重度用户,每周一早上同时跑对账。他们的对账任务互相抢数据库资源。

  3. 匹配引擎挂了,整个站 502:某个客户上传了一个格式异常的 CSV,匹配引擎报错未处理,导致 Node.js 进程崩溃。因为这个引擎和 API 服务在同一个进程里,整个应用挂了。

  4. 那个大客户的询问:一家 2000 人的公司发来 RFP(Request for Proposal),第一行就写:「请提供系统架构文档,说明如何保障 1000+ 用户同时使用的性能和稳定性。」

你需要为 10 倍的增长做准备——不只是服务器加配置,而是架构层面的重新思考。


第一步:服务拆分——不是微服务大爆炸

听到「服务拆分」,很多人第一反应是「微服务」。然后画一堆框图,拆出 20 个微服务,最后发现自己一个人维护不了 20 个服务。

规模化拆分的正确姿势:模块化单体 → 按业务域拆 → 必要才拆。

第一阶段:模块化单体(不拆服务,拆代码)

在真正拆服务之前,先确保你的单体应用内部已经模块化了。这不是微服务,是加强版的第六篇重构。

src/
modules/
reconciliation/ # 对账模块
reconciliation.service.ts
matching-engine.ts
reconciliation.repo.ts
notification/ # 通知模块
notification.service.ts
channels/
feishu.ts
wecom.ts
email.ts
billing/ # 计费模块
billing.service.ts
stripe-webhook.ts
team/ # 团队/用户模块
team.service.ts
permission.service.ts
report/ # 报告模块
report.service.ts
report-generator.ts
audit/ # 审计模块
audit.service.ts

每个模块有自己的 Service 和 Repository,模块之间通过明确的接口调用。同一个进程,但不互相污染。

这个阶段的目标:如果有一天要把 notification 模块拆成独立服务,它的代码不应该和 reconciliation 有任何耦合。

第二阶段:拆出第一批独立服务

什么时候必须拆?满足以下条件之一:

条件为什么必须拆
单个模块的故障会导致整个系统不可用隔离爆炸半径
单个模块的资源消耗影响其他模块防止互相争抢 CPU/内存
单个模块的部署频率远高于其他模块独立部署不影响全局
单个模块需要独立的技术栈比如用 Python 做 AI 匹配

你的对账系统里,第一个该拆的是 匹配引擎

原因:它计算密集(大量数据处理)、它一挂整个站挂、它需要独立扩缩容(周末几乎没人用,周一早上需要 10 倍算力)。

拆匹配引擎的实操

  1. 定义接口(不改代码,先定义契约)
// 匹配引擎对外暴露的接口
interface MatchingEngine {
reconcile(input: ReconcileInput): Promise<ReconcileResult>;
health(): Promise<boolean>;
}
  1. 把匹配引擎独立成一个服务
reconcile-api/        # 原 Next.js 应用(去掉匹配引擎)
matching-engine/ # 新匹配服务
Dockerfile
src/
server.ts # Express/Fastify,只暴露 /reconcile 和 /health
matching.ts # 匹配逻辑(从原项目搬过来)
queue.ts # 任务队列
  1. 通过消息队列解耦

不直接 HTTP 调匹配引擎(同步调用 = 它慢了你也慢)。改用消息队列:

// reconcile-api 发送对账任务到队列
await queue.send('reconciliation', {
teamId, bankFileId, orderFileId, options,
});

// matching-engine 消费队列
queue.consume('reconciliation', async (task) => {
const result = await runMatching(task);
await queue.send('reconciliation_result', result); // 结果发到另一个队列
});

// reconcile-api 消费结果
queue.consume('reconciliation_result', async (result) => {
await notifyClient(result); // 通知客户对账完成
});

用 Redis(BullMQ)或 Cloudflare Queues 作为消息队列。对账从同步 5 秒等待变成异步——客户点击「开始对账」,后台处理,完成后通知(WebSocket 或轮询)。

收益

  • 匹配引擎挂了不影响 API 服务和客户浏览其他页面
  • 周一早上可以临时给匹配引擎加 5 个实例,处理完高峰期后缩回 1 个
  • 匹配引擎可以独立用不同语言重写——比如计算密集部分用 Rust/Go

哪些不该现在拆?

  • 通知模块:逻辑简单,拆不拆性能没区别。保持单体。
  • 审计模块:写多读少,不是瓶颈。保持单体。
  • 报告模块:一天跑一次的任务,对实时性没要求。保持单体。

一句话原则:只为性能/稳定性瓶颈拆服务,不为「架构好看」拆。


第二步:数据库扩展

读写分离

200 个客户时,所有请求——客户的查询和后台的对账任务——都在同一个数据库实例上。对账任务跑满 CPU 时,客户的页面查询也变慢。

# Supabase 上配置只读副本
Database:
- Primary (read + write) # Solo 写,处理对账任务
- Replica 1 (read only) # 处理客户的查询请求

代码层把写操作指向 Primary,读操作指向 Replica:

// Prisma 读写分离
const prisma = new PrismaClient();
const readPrisma = new PrismaClient({
datasources: { db: { url: process.env.READ_REPLICA_URL } },
});

// 所有查询用读库
const recs = await readPrisma.reconciliation.findMany({...});

// 所有写入用写库
await prisma.reconciliation.create({...});

大表分区

BankTransaction 表已经有了几百万行。查询越来越慢。

-- 按时间分区:每个月一个分区
CREATE TABLE BankTransaction_202601 PARTITION OF BankTransaction
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');

CREATE TABLE BankTransaction_202602 PARTITION OF BankTransaction
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
-- ...

查询时如果带了日期条件,PostgreSQL 自动只扫描对应分区,性能提升 10 倍以上。

分析类查询搬到独立副本

客户想要「本月对账异常率趋势」「团队对账效率排行」这类分析查询。这些查询通常需要扫描大量数据。如果和线上交易查询混在一起,线上体验会被拖垮。

建一个分析专用的只读副本:

  • 线上查询 → Replica 1(优化的 OLTP 索引)
  • 分析查询 → Replica 2(专门的 OLAP 索引、允许慢查询)

第三步:多租户架构升级

从「WHERE team_id=?」到分级策略

以前所有客户的数据都在同一张表里,靠 team_id 字段隔离。这在 200 个客户时没问题。但 1000 个客户之后:

  • 单个大客户的数据量(每周 5000 笔对账)占整张表的 30%
  • 大客户的慢查询拖慢全体客户
  • 大客户要求「我们的数据必须物理隔离」

分级多租户策略:

客户级别月费数据隔离方式数据库资源
Free/Starter¥0–199共享表 + team_id共享实例
Professional¥499–999共享表 + team_id + RLS共享实例,更高配额
Enterprise¥5000+独立数据库实例完全隔离,专有资源

Enterprise 客户单独建一个 Supabase 项目(或 AWS RDS 实例)。他们花 ¥5000+/月,值得一个独立数据库。

// 多租户路由:根据 team tier 决定连哪个数据库
async function getPrismaForTeam(teamId: string) {
const team = await mainPrisma.team.findUnique({ where: { id: teamId } });

if (team.tier === 'enterprise') {
return dedicatedPrismaClients.get(team.databaseUrl!);
}

return sharedPrisma; // 中小企业共享
}

关键原则:数据隔离的铁律永远不变——无论用什么隔离策略,绝对不能出现「客户 A 看到客户 B 的数据」。


第四步:API 的专业化

200 个客户时,你的 API 还是「函数名随便起、返回结构随手写」的状态。

API 版本化

当你有了外部集成客户(通过 API 对账而不是界面上传),API 就不能随便改了。

/api/v1/reconcile/run          ← 稳定版本,不改行为
/api/v2/reconcile/run ← 新版本,支持更多参数
/api/v1/reconcile/run?deprecated_after=2027-01-01 ← 返回折旧警告头

破环性变更给 6 个月的折旧期。期间 v1 和 v2 并存。

自动生成 API 文档

// 用 OpenAPI/Swagger 标注你的 API
/**
* @openapi
* /api/v1/reconcile/run:
* post:
* summary: 执行对账
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* bankFileId:
* type: string
* orderFileId:
* type: string
*/

然后用 Swagger UI 自动生成文档页面。客户和合作伙伴自己能看懂 API,不需要你一个一个解释。

开放 API:让产品变成平台

当你的 API 足够稳定后,开放给客户和第三方。他们可以在自己的系统里调你的对账引擎:

  • ERP 系统集成:SalesOrder → 自动导入到你这里对账 → 结果回写 ERP
  • 银行直连:客户授权银行 API → 自动拉取流水 → 对账

这是 SaaS 的终极形态之一——你的产品不再是一个工具,而是客户基础设施的一部分。 迁移成本 = 极高,客户粘性 = 极高。


组织层面的变化

从 1 人到 5 人

1000 个付费客户之后,你一个人扛不住了。你需要招人:

  • 第一位后端工程师:接手日常运维和 bug 修复,把你解放出来做架构和产品
  • 第一位前端工程师:独立迭代 UI/UX,不再是你写完后端顺手糊一个前端
  • 客户成功 1 人:不再是「客户在微信上找你」,而是有一个标准化的支持流程

Code Review 不是找茬

当有第二个人写代码时,Code Review 必须变成制度:

  • 不是为了挑毛病,是为了知识共享:第二个人知道了这个改动,万一你请假了,有人能接手
  • Review 清单:这个改动有没有测试?加了新字段有没有 Migration?有没有破坏 API 兼容性?
  • PR 规范:标题写清楚「做了什么」,描述写清楚「为什么这么做」

On-call 值班

当系统支撑着几千个客户的生产,有人必须负责「出事了谁顶上」。

  • 两个人轮值,一周换一次
  • PagerDuty(或简单的飞书群告警)
  • 20 分钟内必须响应

什么时候进入下一阶段?

你做到了:

  • ✅ 匹配引擎独立成服务,消息队列解耦
  • ✅ 数据库读写分离 + 大表分区 + 分析副本
  • ✅ 多租户分级:Enterprise 客户独立数据库
  • ✅ API 版本化 + 开放 API + 自动文档
  • ✅ 5 人团队,有 Code Review 和 On-call 机制
  • ✅ 1000+ 付费客户,MRR ¥200,000+/月

技术上趋于成熟。但你也开始想一些不同的问题:

  • 产品已经很稳定了,但增长从哪里来?
  • 目前 90% 的客户来自口碑推荐。要不要做付费获客?
  • 你已经是一个「好用的工具」,但距离一个「值得信赖的品牌」还有多远?
  • 你怎么让刚注册的客户在 5 分钟内感受到价值,而不是花 2 周慢慢摸索?

进入下一阶段的信号:技术和收入都趋于稳定,你开始思考「这个产品十年后还在不在」。

这是第十一篇的内容——从生意到事业。


本篇总结

做了什么关键决策
服务拆分:匹配引擎独立不是因为微服务流行,是因为它拖垮了整体
数据库读写分离 + 分区 + 分析副本按实际压力驱动,不为提前优化
多租户分级:Enterprise 独立 DB大客户为物理隔离付费
API 版本化 + 开放 API让产品成为客户基础设施
从 1 人到 5 人团队Code Review、On-call、标准化

当前系统形态: 服务拆分后的平台级产品。前端 + 核心 API(Vercel)、匹配引擎(独立服务 + 消息队列)、数据库读写分离 + 多租户分级。1000+ 客户,支持开放 API。

**本月账单:~800/VercelPro800/月**(Vercel Pro 100 + Supabase + 独立 RDS + Redis + 消息队列 + 监控工具 + 团队工具)。MRR ¥200,000+/月。

如果是 ToC: 这个阶段你可能还是单体应用,但数据库负载不同——ToC 面对的是大量轻量请求(每个请求查一下用户的个人数据),而非少量重量请求(一个企业跑批处理)。ToC 的扩展重点是 CDN + 缓存 + 无状态水平扩展,拆分服务的需求反而比 ToB 晚。


下一步

开始思考长期价值——进入第十一篇:从生意到事业。