架构选型最忌「别人用什么我就用什么」。Netflix 用微服务是因为它有上千工程师、全球分布式部署、持续交付流水线——你的 3 人团队不需要。
真正驱动架构决策的 5 个变量:
| 变量 | 问自己 | 关键阈值 |
|---|---|---|
| 团队规模 | 多少人在写代码? | <20 人 → 单体;20-50 → 过渡区;50+ → 微服务可考虑 |
| 运维能力 | 有 SRE 吗?有 K8s 经验吗? | 无自动化部署 → 别碰微服务 |
| 领域复杂度 | 业务边界稳定吗? | 边界不清 → 先单体搞清楚 |
| 业务约束 | 预算?合规?上市时间? | 预算紧 → 单体;合规隔离 → 微服务 |
| 增长轨迹 | 验证期还是规模化期? | MVP → 单体;10x 增长中 → 准备拆分 |
是什么:所有代码在一个代码库、一个进程、一次部署。前端、后端、数据库访问全在一起。
my-saas/
├── src/
│ ├── modules/
│ │ ├── auth/ # 认证模块
│ │ ├── billing/ # 计费模块
│ │ ├── users/ # 用户模块
│ │ └── api/ # API 入口
│ ├── db/ # 数据库访问
│ └── utils/ # 公共工具
├── package.json
└── docker-compose.yml # 一个容器搞定
何时用:
// server.ts — 单体架构,所有模块在同一进程
import express from 'express';
import { authRouter } from './modules/auth/routes';
import { billingRouter } from './modules/billing/routes';
import { userRouter } from './modules/users/routes';
const app = express();
// 共享数据库连接
import { prisma } from './db';
app.locals.prisma = prisma;
// 所有模块挂载在同一 Express 实例
app.use('/auth', authRouter);
app.use('/billing', billingRouter);
app.use('/users', userRouter);
// 一个端口,一个进程,一个部署
app.listen(3000, () => console.log('Monolith running :3000'));
是什么:代码组织像微服务(模块间有明确边界和接口),但部署仍是一个进程。是单体和微服务之间的最佳折中。
my-saas/
├── src/
│ ├── modules/
│ │ ├── auth/
│ │ │ ├── auth.module.ts # 模块入口,声明公开 API
│ │ │ ├── auth.service.ts # 内部实现
│ │ │ ├── auth.controller.ts # HTTP 入口
│ │ │ └── auth.types.ts # 公开类型
│ │ ├── billing/
│ │ │ ├── billing.module.ts
│ │ │ ├── billing.service.ts
│ │ │ └── ...
│ │ └── users/
│ │ └── ...
│ ├── shared/ # 极少共享代码
│ └── app.ts # 组装模块
├── package.json
└── tsconfig.json
// modules/auth/auth.module.ts — 模块公开 API
export interface AuthModule {
verifyToken(token: string): Promise<User>;
createUser(input: CreateUserInput): Promise<User>;
}
// 内部实现不导出
class AuthService implements AuthModule {
async verifyToken(token: string) { /* ... */ }
async createUser(input: CreateUserInput) { /* ... */ }
}
// 只导出模块实例,不导出类
export const authModule: AuthModule = new AuthService();
// ❌ 错误:billing 直接 import auth 的内部文件
// import { hashPassword } from '../auth/auth.service';
// ✅ 正确:billing 通过模块公开接口调用
import { authModule } from '../auth/auth.module';
const user = await authModule.verifyToken(token);
强制模块边界:用 ESLint 或架构测试确保模块间不越界:
// architect.ts — 架构守卫测试
import { project } from 'ts-morph';
const p = project.addSourceFilesFromTsConfig('tsconfig.json');
// 确保 billing 模块不直接导入 auth 内部
for (const file of p.getSourceFiles('src/modules/billing/**/*.ts')) {
const imports = file.getImportDeclarations();
for (const imp of imports) {
const path = imp.getModuleSpecifierValue();
if (path.includes('modules/auth/') && !path.endsWith('auth.module')) {
throw new Error(`架构违规: ${file.getFilePath()} 直接导入了 auth 内部: ${path}`);
}
}
}
是什么:每个业务模块是独立的服务,独立部署、独立数据库、独立扩展。服务间通过 API/消息队列通信。
my-saas/
├── services/
│ ├── auth-service/ # 独立代码库或 mono-repo 子目录
│ │ ├── src/
│ │ ├── Dockerfile
│ │ └── docker-compose.yml
│ ├── billing-service/
│ │ ├── src/
│ │ └── ...
│ ├── user-service/
│ │ └── ...
│ └── api-gateway/ # 统一入口
│ └── ...
├── infra/ # 基础设施配置
│ ├── k8s/
│ └── terraform/
└── libs/ # 共享库
何时用:
// === 同步:gRPC (适合强一致性场景) ===
// proto/billing.proto
syntax = "proto3";
service BillingService {
rpc GetSubscription(GetSubscriptionReq) returns (Subscription);
}
// billing-service/src/server.ts
const server = new grpc.Server();
server.addService(billingProto.BillingService.service, {
getSubscription: async (call, callback) => {
const sub = await db.subscription.findUnique({ where: { userId: call.request.userId } });
callback(null, sub);
},
});
// === 异步:消息队列 (适合解耦和事件驱动) ===
// auth-service 发布事件
await messageQueue.publish('user.created', {
userId: user.id,
email: user.email,
timestamp: Date.now(),
});
// billing-service 订阅事件
messageQueue.subscribe('user.created', async (event) => {
await createFreeTrialSubscription(event.userId);
});
是什么:你写函数(Function),云平台负责运行。无需管理服务器,按调用次数付费。AWS Lambda、Vercel Functions、Cloudflare Workers 是代表。
何时用:
// app/api/subscribe/route.ts — Vercel Edge Function
import { stripe } from '@/lib/stripe';
import { kv } from '@vercel/kv';
export async function POST(req: Request) {
const { plan, email } = await req.json();
// 创建 Stripe Checkout Session
const session = await stripe.checkout.sessions.create({
customer_email: email,
payment_method_types: ['card'],
line_items: [{
price: plan === 'pro' ? process.env.STRIPE_PRO_PRICE : process.env.STRIPE_FREE_PRICE,
quantity: 1,
}],
mode: 'subscription',
success_url: `${process.env.URL}/dashboard?upgraded=true`,
cancel_url: `${process.env.URL}/pricing`,
});
// KV 存储状态(无需数据库!)
await kv.set(`sub:${email}`, { status: 'pending', plan });
return Response.json({ url: session.url });
}
// Cloudflare Workers 版本 — 更极致的 Edge
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (url.pathname === '/api/hello') {
// 在全球 300+ 边缘节点执行,延迟 < 50ms
return new Response(JSON.stringify({ hello: 'world', cf: request.cf?.country }));
}
return new Response('Not found', { status: 404 });
},
};
是什么:代码运行在 CDN 边缘节点,离用户最近。Cloudflare Workers、Vercel Edge、Deno Deploy 是代表。本质是 Serverless 的进化——更轻、更快、更全球。
// wrangler.toml
name = "my-saas-api"
main = "src/index.ts"
compatibility_date = "2025-01-01"
[[d1_databases]]
binding = "DB"
database_name = "saas-db"
database_id = "xxx"
// src/index.ts
interface Env { DB: D1Database; }
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
// 在离用户最近的边缘节点执行
if (url.pathname === '/api/users') {
// D1 是 SQLite,也在边缘
const { results } = await env.DB.prepare(
'SELECT id, name, plan FROM users WHERE id = ?'
).bind(userId).run();
return Response.json(results);
}
// 边缘 KV 缓存
if (url.pathname === '/api/config') {
const cached = await env.KV.get('app-config');
if (cached) return new Response(cached, {
headers: { 'content-type': 'application/json',
'cache-control': 'public, max-age=60' }
});
}
return new Response('Not found', { status: 404 });
},
};
| 维度 | 单体 | 模块化单体 | 微服务 | Serverless | Edge |
|---|---|---|---|---|---|
| 开发速度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 运维复杂度 | ⭐ (最低) | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| 扩展性 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 成本(低流量) | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 成本(高流量) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| 延迟 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 团队要求 | 1-3 全栈 | 3-10 全栈 | 50+ 含 SRE | 2-5 全栈 | 2-5 全栈 |
| 典型月成本 | $5-50 VPS | $5-50 VPS | $500-5000+ | $0-100 | $0-50 |
不要一次性重写。以下是经过验证的渐进拆分策略:
在单体中建立严格的模块边界。用架构测试强制执行。这一步不改变部署方式,只改变代码组织。
// 1. 先在单体中定义异步接口
interface NotificationService {
sendEmail(to: string, template: string, data: object): Promise<void>;
sendSMS(phone: string, message: string): Promise<void>;
}
// 2. 切换到消息队列实现(仍然在单体中)
class QueueNotificationService implements NotificationService {
async sendEmail(to, template, data) {
await queue.publish('notification.email', { to, template, data });
}
}
// 3. 拆出独立服务消费队列
// notification-service/main.ts
queue.subscribe('notification.email', async (msg) => {
await sendgrid.send({ to: msg.to, templateId: msg.template, data: msg.data });
});
// Phase 1: 服务独立部署,但共享数据库(读副本)
// notification-service 仍然读取主库的 users 表
// Phase 2: 引入事件同步,逐步迁移到独立数据库
// user-service 发布事件
await eventBus.publish('user.updated', {
userId: user.id,
email: user.email,
name: user.name,
});
// notification-service 维护本地副本
eventBus.subscribe('user.updated', async (event) => {
await localDb.users.upsert({
where: { id: event.userId },
update: { email: event.email, name: event.name },
create: { id: event.userId, email: event.email, name: event.name },
});
});
// Phase 3: 完全独立,不再读取主库
# .github/workflows/notification-service.yml
name: Deploy Notification Service
on:
push:
paths: ['services/notification-service/**']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- run: docker build -t notification-service ./services/notification-service
- run: docker push ${{ env.REGISTRY }}/notification-service:${{ github.sha }}
- run: kubectl set image deployment/notification-service app=${{ env.REGISTRY }}/notification-service:${{ github.sha }}
无论选择哪种架构风格,以下模式是 SaaS 的「标配」:
统一入口:路由、认证、限流、日志。
// gateway/index.ts — 基于Express的轻量API网关
import rateLimit from 'express-rate-limit';
import { createProxyMiddleware } from 'http-proxy-middleware';
const app = express();
// 全局限流
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
// JWT 认证中间件
app.use('/api/*', async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'Missing token' });
try {
req.user = await verifyJWT(token);
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
});
// 路由到后端服务
app.use('/api/auth', createProxyMiddleware({ target: 'http://auth-service:3001' }));
app.use('/api/billing', createProxyMiddleware({ target: 'http://billing-service:3002' }));
app.use('/api/users', createProxyMiddleware({ target: 'http://user-service:3003' }));
app.listen(8000);
写入和读取用不同的模型。SaaS 中极其常见——管理后台需要复杂查询,API 需要简单读取。
// Write side — 命令处理,只关心业务规则
class SubscriptionCommandHandler {
async handleUpgrade(cmd: UpgradeSubscription) {
const sub = await this.repo.findById(cmd.subscriptionId);
sub.upgrade(cmd.newPlan); // 业务逻辑
await this.repo.save(sub);
await this.eventBus.publish(new SubscriptionUpgraded(sub.id, cmd.newPlan));
}
}
// Read side — 查询模型,为读优化
class SubscriptionQueryService {
async getSubscriptionView(userId: string) {
// 从读取专用表查询,可能包含跨服务聚合的数据
return this.readDb.query(`
SELECT s.*, p.features, u.name as user_name
FROM subscription_views s
JOIN plan_features p ON s.plan_id = p.id
JOIN user_views u ON s.user_id = u.id
WHERE s.user_id = ?
`, [userId]);
}
}
// 事件同步写入模型 → 读取模型
eventBus.subscribe('SubscriptionUpgraded', async (event) => {
await readDb.subscriptionViews.update({
where: { id: event.subscriptionId },
data: { plan: event.newPlan, updatedAt: new Date() },
});
});
微服务中防止级联故障的关键模式。
import CircuitBreaker from 'opossum';
// 包装外部服务调用
const callBillingService = new CircuitBreaker(
async (userId: string) => {
const res = await fetch(`http://billing-service:3002/api/subscriptions/${userId}`);
if (!res.ok) throw new Error(`Billing service: ${res.status}`);
return res.json();
},
{
timeout: 3000, // 3s 超时
errorThreshold: 50, // 50% 错误率触发熔断
resetTimeout: 30000, // 30s 后尝试恢复
}
);
// 熔断时返回降级数据
callBillingService.fallback(() => ({
plan: 'free',
status: 'degraded', // 标记为降级数据
features: FREE_TIER_FEATURES,
}));
// 使用
const subscription = await callBillingService.fire(userId);
微服务下替代分布式锁的方案。每个步骤有对应的补偿操作。
// 用户注册需要:创建用户 → 初始化订阅 → 发送欢迎邮件
// 任何一步失败,执行之前步骤的补偿操作
async function registerUserSaga(input: RegisterInput) {
const saga = new Saga();
// Step 1: 创建用户
saga.addStep({
execute: async () => {
const user = await userService.create(input);
return { userId: user.id };
},
compensate: async (ctx) => {
await userService.delete(ctx.userId);
},
});
// Step 2: 初始化订阅
saga.addStep({
execute: async (ctx) => {
const sub = await billingService.initFreeSubscription(ctx.userId);
return { subscriptionId: sub.id };
},
compensate: async (ctx) => {
await billingService.cancelSubscription(ctx.subscriptionId);
},
});
// Step 3: 发送欢迎邮件(失败不需要补偿)
saga.addStep({
execute: async (ctx) => {
await emailService.sendWelcome(ctx.userId);
},
compensate: async () => { /* 邮件已发就算了 */ },
});
await saga.execute();
// 如果 Step 2 失败,自动执行 Step 1 的 compensate(删除用户)
}
渐进式从旧系统迁移到新系统。新功能走新架构,旧功能逐步替换。
// gateway/strangler.ts — 逐步将路由从旧系统切到新系统
const LEGACY_BACKEND = 'http://legacy-app:3000';
const NEW_BACKEND = 'http://new-service:4000';
// 迁移路由表:哪些路径已切到新系统
const migratedPaths = new Set([
'/api/auth', // Week 1: 认证模块已迁移
'/api/users/profile', // Week 2: 用户资料已迁移
// '/api/billing', // Week 3: 计划迁移中...
]);
app.use('/api/*', (req, res, next) => {
const migrated = Array.from(migratedPaths).some(p => req.path.startsWith(p));
const target = migrated ? NEW_BACKEND : LEGACY_BACKEND;
createProxyMiddleware({ target })(req, res, next);
});
详细内容见 多租户架构 专页,这里概述三种策略:
| 策略 | 隔离级别 | 成本 | 适合场景 |
|---|---|---|---|
| 共享数据库 + Row-Level | 逻辑隔离 | 最低 | 中小型 SaaS,每个 query 加 WHERE tenant_id = ? |
| Schema 隔离 | 中等 | 中等 | 需要一定隔离,但不想管多个数据库 |
| 独立数据库 | 物理隔离 | 最高 | 企业级,合规要求严格 |
每个租户可能有不同的功能开关、UI 配置、工作流。你需要一个特性旗标 + 配置系统:
// lib/features.ts
interface FeatureConfig {
maxProjects: number;
hasAI: boolean;
hasSSO: boolean;
customBranding: boolean;
}
const PLAN_FEATURES: Record<string, FeatureConfig> = {
free: { maxProjects: 3, hasAI: false, hasSSO: false, customBranding: false },
pro: { maxProjects: 50, hasAI: true, hasSSO: false, customBranding: false },
enterprise: { maxProjects: Infinity, hasAI: true, hasSSO: true, customBranding: true },
};
// 中间件:注入当前租户特性
function featureFlag(req: Request, res: Response, next: NextFunction) {
const tenant = req.user.tenant;
const plan = tenant.plan; // 从数据库或缓存获取
req.features = PLAN_FEATURES[plan];
next();
}
// 使用
app.post('/api/projects', featureFlag, (req, res) => {
if (req.user.projects.length >= req.features.maxProjects) {
return res.status(403).json({ error: 'Project limit reached', upgradeUrl: '/pricing' });
}
// 创建项目...
});
SaaS 必须记录谁在什么时候做了什么。不是可选的。
// middleware/audit.ts
interface AuditLog {
tenantId: string;
userId: string;
action: string; // 'project.create', 'user.invite', 'settings.update'
resource: string; // '/api/projects/123'
before?: object; // 变更前
after?: object; // 变更后
ip: string;
userAgent: string;
timestamp: Date;
}
async function auditLog(req: Request, action: string, before?: object, after?: object) {
await db.auditLog.create({
data: {
tenantId: req.user.tenantId,
userId: req.user.id,
action,
resource: req.originalUrl,
before: before ?? null,
after: after ?? null,
ip: req.ip,
userAgent: req.headers['user-agent'],
},
});
}
// 使用
app.put('/api/settings', async (req, res) => {
const before = await getSettings(req.user.tenantId);
const after = await updateSettings(req.user.tenantId, req.body);
await auditLog(req, 'settings.update', before, after);
res.json(after);
});
真实项目很少是「纯」某种架构。最常见的模式:
┌─────────────────────────────────────────────────┐
│ Cloudflare Workers (Edge) │
│ ┌───────────┐ ┌───────────┐ ┌────────────────┐ │
│ │ Auth Gate │ │ Rate Limit│ │ Static Cache │ │
│ └─────┬─────┘ └─────┬─────┘ └───────┬────────┘ │
└────────┼─────────────┼───────────────┼──────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ 模块化单体 (VPS / Container) │
│ ┌────────┐ ┌─────────┐ ┌──────┐ ┌───────────┐ │
│ │ Auth │ │ Billing │ │ Users│ │ Projects │ │
│ └────────┘ └─────────┘ └──────┘ └───────────┘ │
│ └───────────── PostgreSQL ─────────────────────┘│
└────────────────────────┬────────────────────────┘
│ 消息队列
▼
┌─────────────────────────────────────────────────┐
│ Serverless Workers (异步任务) │
│ ┌────────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Email Send │ │ PDF Gen │ │ Image Process │ │
│ └────────────┘ └──────────┘ └───────────────┘ │
└─────────────────────────────────────────────────┘
这种组合让你:
// Cloudflare Worker — Edge 路由层
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
// 静态资源 → CDN 缓存,零延迟
if (url.pathname.startsWith('/static/')) {
return caches.default.match(request) || fetch(request);
}
// 认证检查 → Edge JWT 验证,<5ms
const user = await verifyJWT(request.headers.get('Authorization'), env.JWT_SECRET);
if (!user && !PUBLIC_PATHS.includes(url.pathname)) {
return new Response('Unauthorized', { status: 401 });
}
// API 请求 → 转发到单体核心
if (url.pathname.startsWith('/api/')) {
return fetch(new Request(request, {
headers: { ...Object.fromEntries(request.headers), 'X-User': JSON.stringify(user) },
}));
}
return fetch(request);
},
};
3 人团队建 12 个微服务。结果:80% 时间花在运维上,功能迭代速度暴跌。先单体验证,再按需拆分。
服务独立部署了,但所有服务直连同一个数据库。一个服务改表结构,其他全挂。每个服务应该有自己的数据存储。
A → B → C → D,四跳同步调用。延迟叠加,任何一环出问题全链失败。用消息队列解耦,或合并为一个服务。
微服务没有分布式追踪 = 黑箱。从第一天起就要有:请求 ID 透传、结构化日志、指标采集。推荐 OpenTelemetry。
「我们暂停新功能,花 6 个月重写架构」。结果:6 个月后产品已落后,新架构也未必更好。用 Strangler Fig 模式渐进迁移。
| 需求 | 推荐 | 替代 |
|---|---|---|
| API Gateway | Kong / 自建 Express | Traefik, AWS API Gateway |
| 消息队列 | Redis Streams / BullMQ | RabbitMQ, Kafka, Inngest |
| 服务发现 | K8s Service (如用 K8s) | Consul, DNS |
| 分布式追踪 | OpenTelemetry + Jaeger | Zipkin, Datadog APM |
| 熔断器 | opossum | Polly (.NET), resilience4j |
| 特性旗标 | PostHog / 自建 | LaunchDarkly, Unleash |
| Serverless 平台 | Vercel | AWS Lambda, Netlify |
| Edge 平台 | Cloudflare Workers | Deno Deploy, Vercel Edge |
| 容器编排 | Docker Compose (小规模) | K8s (大规模), Nomad |
SaaS 架构决策直接影响 Agent 系统的设计:
| SaaS 架构选择 | 对 Agent 的影响 | 参考 |
|---|---|---|
| 单体 → 微服务拆分 | Agent 工具系统对应微服务边界;每个服务暴露为 Agent Tool | 🛠️ 工具系统 |
| CQRS 读写分离 | Agent 读取用查询模型,写入走命令模型;减少 Agent 上下文负担 | 🧠 记忆系统 |
| 消息队列解耦 | Agent 异步工具调用通过队列;Background Review Fork 模式 | 🔄 主循环 |
| 多租户数据隔离 | Agent 的知识库/记忆必须按租户隔离 | 🔒 安全机制 |
| Edge 认证网关 | Agent API 调用经 Edge 认证,Policy Pipeline 在网关层执行 | 🔒 安全机制 |
| 特性旗标 | Agent 功能按租户计划开放;Pro 用户才能用 AI Agent | 🚀 生产级特性 |