第十篇:规模化 — 从 200 个客户到 10000 个
进入信号:部署变慢(>5 分钟)、一个模块挂了整个站跟着挂、数据库 CPU 长期高负载、有大客户问「能扛住多少用户?」。
本篇解决的问题:单体应用的极限在哪里?怎么拆服务?数据库怎么扩展?多租户架构怎么升级?怎么从服务 200 个客户变成服务 10000 个?
单体应用的极限
200 个客户的时候,系统开始暴露各种瓶颈。不是一下子全暴露的,是一个接一个:
-
Build 时间从 2 分钟涨到 8 分钟:项目代码量从最初的 2000 行变成了 30000 行。Next.js 的 Build 每次要编译所有页面。
-
数据库 CPU 长期 60%,高峰期 90%:200 个客户里有 5 个重度用户,每周一早上同时跑对账。他们的对账任务互相抢数据库资源。
-
匹配引擎挂了,整个站 502:某个客户上传了一个格式异常的 CSV,匹配引擎报错未处理,导致 Node.js 进程崩溃。因为这个引擎和 API 服务在同一个进程里,整个应用挂了。
-
那个大客户的询问:一家 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 倍算力)。
拆匹配引擎的实操
- 定义接口(不改代码,先定义契约):
// 匹配引擎对外暴露的接口
interface MatchingEngine {
reconcile(input: ReconcileInput): Promise<ReconcileResult>;
health(): Promise<boolean>;
}
- 把匹配引擎独立成一个服务:
reconcile-api/ # 原 Next.js 应用(去掉匹配引擎)
matching-engine/ # 新匹配服务
Dockerfile
src/
server.ts # Express/Fastify,只暴露 /reconcile 和 /health
matching.ts # 匹配逻辑(从原项目搬过来)
queue.ts # 任务队列
- 通过消息队列解耦:
不直接 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。
**本月账单:~100 + Supabase + 独立 RDS + Redis + 消息队列 + 监控工具 + 团队工具)。MRR ¥200,000+/月。
如果是 ToC: 这个阶段你可能还是单体应用,但数据库负载不同——ToC 面对的是大量轻量请求(每个请求查一下用户的个人数据),而非少量重量请求(一个企业跑批处理)。ToC 的扩展重点是 CDN + 缓存 + 无状态水平扩展,拆分服务的需求反而比 ToB 晚。
下一步
开始思考长期价值——进入第十一篇:从生意到事业。