全球化 SaaS 基础设施清单:域名、CDN、支付与监控
去年我做了一个面向北美市场的 SaaS 工具,上线第一周就收到了几条用户反馈:页面加载慢、支付失败没提示、注册邮件进了垃圾箱。这些都是基础设施层面的问题,不是功能 bug,但直接影响用户对产品的信任。
独立开发者通常把精力放在产品逻辑上,觉得「先上线再说」。这个思路没错,但如果关键链路不可靠——用户付不了款、收不到邮件、页面加载超过 3 秒——增长流量会被直接浪费。基础设施不需要一开始做得很重,但域名、CDN、支付、邮件、监控这几条链路,在面向海外市场时必须可靠。
这篇文章是我过去两年做全球化 SaaS 踩坑后的总结,覆盖域名与 DNS 配置、CDN 与对象存储选型、多区域部署策略、支付与 Webhook 处理、邮件送达率、监控与日志追踪、数据驻留与合规。每个部分都有具体的翻车经历和修复方案,最后附了一份可执行的出海检查清单。
域名与 DNS:被忽视的第一道门
域名配置看起来简单,但 DNS 解析策略直接影响全球用户的访问速度。我早期犯过一个错误:把主域名、API 域名、管理后台都指向同一个 IP,结果某个区域的 DNS 解析延迟导致整个产品不可用。
后来我学到的教训是:子域名拆分 + 就近解析。主站用 app.example.com,API 用 api.example.com,静态资源用 cdn.example.com 或独立的对象存储域名。DNS 服务商选择上,Cloudflare 和 AWS Route 53 都支持基于地理位置的智能解析(Geo DNS),可以根据用户所在地返回最近的服务器 IP。
// ❌ 错误做法:所有服务指向同一 IP
// DNS 记录
// app.example.com A 1.2.3.4
// api.example.com A 1.2.3.4
// cdn.example.com A 1.2.3.4
// ✅ 正确做法:按职责拆分域名,配合 CDN 和智能解析
// app.example.com CNAME cname.vercel-dns.com
// api.example.com CNAME api-lb-123456789.us-east-1.elb.amazonaws.com
// cdn.example.com CNAME cdn.cloudflare.net
// static.example.com CNAME r2.cloudflarestorage.com另一个容易踩的坑是 DNS 缓存。TTL(Time To Live)设置过短会增加 DNS 查询次数,设置过长会导致故障切换慢。我的经验是:正常记录设 300 秒(5 分钟),故障切换时临时调到 60 秒。
CDN 与对象存储:静态资源加速的核心
CDN 的选择取决于目标市场和流量模式。Cloudflare 的全球网络覆盖最广,免费套餐就能满足大部分独立开发者的需求。如果目标用户主要在北美和欧洲,Vercel 自带的 CDN 也够用。但如果用户分布在亚洲、南美、非洲,Cloudflare 的节点覆盖优势就明显了。
对象存储方面,Cloudflare R2 的零出口流量(zero egress)定价是杀手锏。AWS S3 的标准定价是 $0.023/GB/月存储 + $0.09/GB 出口流量,R2 是 $0.015/GB/月存储 + $0 出口流量。对于一个月流出 10TB 数据的产品,R2 能省约 $891/月的出口费用。
// ❌ 错误做法:直接从服务器提供静态资源,没有 CDN 加速
app.get('/images/:id', async (req, res) => {
const image = await fs.readFile(`./uploads/${req.params.id}`)
res.set('Content-Type', 'image/jpeg')
res.send(image)
})
// ✅ 正确做法:静态资源上传到对象存储,通过 CDN 分发
// 上传时直接写入 R2/S3,应用只生成签名 URL
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
const s3 = new S3Client({
region: 'auto',
endpoint: 'https://<account_id>.r2.cloudflarestorage.com',
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
},
})
async function getUploadUrl(key: string) {
const command = new PutObjectCommand({
Bucket: 'my-saas-uploads',
Key: key,
ContentType: 'image/jpeg',
})
return getSignedUrl(s3, command, { expiresIn: 3600 })
}
// 前端直接上传到 R2,CDN 自动加速访问
// https://cdn.example.com/uploads/user-123/avatar.jpg图片压缩也是 CDN 层该做的事。Cloudflare 的 Polish 和 image resizing 功能可以在边缘节点自动转换格式(WebP/AVIF)和压缩,不需要在应用层处理。
多区域部署:什么时候该做,什么时候不该做
AWS 官方文档把多区域灾备策略分为四种:Backup & Restore(备份恢复)、Pilot Light(引导灯)、Warm Standby(温备)、Multi-Site Active/Active(多活)。RTO(恢复时间目标)和 RPO(恢复点目标)从小时级降到秒级,成本也依次递增。
对于独立开发者的早期产品,大部分情况下不需要多活架构。一个区域的计算节点 + 全球 CDN 加速静态资源,就能覆盖大部分场景。等真实用户分布稳定后,再考虑是否需要在其他区域部署计算节点。
graph TB
User[全球用户] -->|DNS 解析| Route53[Route 53 Geo DNS]
Route53 -->|北美用户| US[US-East-1 Vercel]
Route53 -->|欧洲用户| EU[EU-West-1 Vercel]
Route53 -->|亚太用户| AP[AP-Southeast-1 Vercel]
US -->|API 请求| APIGateway[API Gateway]
EU -->|API 请求| APIGateway
AP -->|API 请求| APIGateway
APIGateway -->|读写| PrimaryDB[(Primary DB<br/>US-East-1)]
PrimaryDB -->|异步复制| ReplicaEU[(Replica<br/>EU-West-1)]
PrimaryDB -->|异步复制| ReplicaAP[(Replica<br/>AP-Southeast-1)]
US -.->|静态资源| CDN[Cloudflare CDN]
EU -.->|静态资源| CDN
AP -.->|静态资源| CDN
CDN -->|回源| R2[R2 Object Storage]
// ❌ 错误做法:所有区域直连同一个区域的数据库,跨洋延迟高
// 欧洲用户请求 API → US-East-1 API → US-East-1 DB → 响应延迟 150ms+
async function getUser(userId: string) {
// 直接从主库读取,无论用户在哪个区域
return db.query('SELECT * FROM users WHERE id = $1', [userId])
}
// ✅ 正确做法:读请求路由到就近的只读副本
// 欧洲用户请求 API → EU API → EU Replica DB → 响应延迟 20ms
import { Client } from 'pg'
async function getUser(userId: string, region: 'us' | 'eu' | 'ap') {
const dbConfig = {
us: { host: 'db-us.example.com', port: 5432 },
eu: { host: 'db-eu.example.com', port: 5432 },
ap: { host: 'db-ap.example.com', port: 5432 },
}
const client = new Client(dbConfig[region])
await client.connect()
try {
// 读请求走就近副本
const result = await client.query(
'SELECT * FROM users WHERE id = $1',
[userId]
)
return result.rows[0]
} finally {
await client.end()
}
}
// 写请求始终路由到主库
async function updateUser(userId: string, data: Partial<User>) {
const primaryClient = new Client({ host: 'db-us.example.com', port: 5432 })
await primaryClient.connect()
try {
await primaryClient.query(
'UPDATE users SET name = $1, updated_at = NOW() WHERE id = $2',
[data.name, userId]
)
} finally {
await primaryClient.end()
}
}Fly.io 的多区域部署方案比较独特,它通过 fly-replay 头实现请求路由:如果一个请求到达的区域没有对应的数据,应用可以返回 fly-replay: instance=<target> 让 Fly.io 把请求重定向到正确的区域。这种方式适合数据库分片的场景,但需要在应用层处理路由逻辑。
支付与 Webhook:不能丢的事件
Stripe 的 Webhook 机制有严格的超时要求:必须在 20 秒内返回 2xx 响应,否则 Stripe 会认为投递失败并重试。重试策略是指数退避,最长重试 72 小时。如果 Webhook 处理器有 bug 返回了 5xx,会触发「重试风暴」——Stripe 会持续重试,把你的服务器打垮。
我遇到过一次事故:支付成功后的邮件发送逻辑超时,导致 Webhook 返回 500,Stripe 开始重试。每次重试又触发邮件发送,又超时,又 500。三天内积累了上万次重试,服务器资源被耗尽。
// ❌ 错误做法:在 Webhook 处理器中同步执行耗时操作
app.post('/webhook/stripe', async (req, res) => {
const event = req.body
switch (event.type) {
case 'checkout.session.completed':
// 同步发送邮件 + 更新数据库 + 调用第三方 API
await sendWelcomeEmail(event.data.object.customer_email) // 可能超时
await updateSubscription(event.data.object) // 可能失败
await notifySlack(event) // 可能超时
break
}
res.json({ received: true })
})
// ✅ 正确做法:快速响应,耗时操作异步处理
import { Queue } from 'bullmq'
const webhookQueue = new Queue('stripe-webhooks', {
connection: { host: 'redis.example.com', port: 6379 },
})
app.post('/webhook/stripe', async (req, res) => {
// 1. 验证签名
const sig = req.headers['stripe-signature']
let event
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET
)
} catch (err) {
console.error('Webhook signature verification failed')
return res.status(400).json({ error: 'Invalid signature' })
}
// 2. 幂等性检查:检查是否已处理过这个事件
const alreadyProcessed = await db.query(
'SELECT 1 FROM processed_events WHERE event_id = $1',
[event.id]
)
if (alreadyProcessed.rows.length > 0) {
return res.json({ received: true })
}
// 3. 入队异步处理,快速返回 200
await webhookQueue.add('process-event', {
eventId: event.id,
eventType: event.type,
eventData: event.data.object,
})
res.json({ received: true })
})
// 4. 异步处理器:消费队列,处理实际业务逻辑
webhookQueue.process('process-event', async (job) => {
const { eventId, eventType, eventData } = job.data
try {
switch (eventType) {
case 'checkout.session.completed':
await sendWelcomeEmail(eventData.customer_email)
await updateSubscription(eventData)
await notifySlack(eventData)
break
// ...
}
// 标记事件已处理
await db.query(
'INSERT INTO processed_events (event_id, processed_at) VALUES ($1, NOW())',
[eventId]
)
} catch (err) {
console.error(`Failed to process event ${eventId}:`, err)
// 抛出异常,让 BullMQ 自动重试这个 job
throw err
}
})关键点是幂等性和异步处理。每个 Webhook 事件用 event.id 做幂等键,即使 Stripe 重试也不会重复处理。耗时操作(邮件、数据库更新、第三方调用)全部入队异步执行,Webhook 处理器只负责验签、幂等检查、入队、返回 200。
邮件送达率:事务邮件不能进垃圾箱
邮件送达率取决于 DNS 配置和发件人信誉。SPF(Sender Policy Framework)、DKIM(DomainKeys Identified Mail)、DMARC(Domain-based Message Authentication, Reporting & Conformance)这三项必须配齐,否则 Gmail、Outlook 会直接把邮件扔进垃圾箱。
事务邮件(注册验证、密码重置、账单通知)的优先级高于营销邮件。我早期用 SendGrid 的共享 IP 发事务邮件,送达率只有 70% 左右。后来切换到专用 IP + 独立子域名(mail.example.com),送达率提升到 98%。
# ❌ 错误做法:只配了 SPF,没配 DKIM 和 DMARC
# example.com DNS 记录
example.com. TXT "v=spf1 include:sendgrid.net ~all"
# ✅ 正确做法:SPF + DKIM + DMARC 三件套
# SPF 记录
example.com. TXT "v=spf1 include:sendgrid.net ~all"
mail.example.com. TXT "v=spf1 include:sendgrid.net ~all"
# DKIM 记录(SendGrid 提供)
s1._domainkey.example.com. CNAME s1.domainkey.u1234567.wl.sendgrid.net
s2._domainkey.example.com. CNAME s2.domainkey.u1234567.wl.sendgrid.net
# DMARC 记录(先从 none 开始监控,逐步过渡到 quarantine/reject)
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:[email protected]; pct=100"DMARC 的 p 策略建议分阶段实施:先设 p=none 收集报告,分析 30 天的数据后改成 p=quarantine(可疑邮件进垃圾箱),再 30 天后改成 p=reject(直接拒绝伪造邮件)。
监控与日志:错误必须能被及时发现
监控分三层:基础设施监控(CPU、内存、磁盘)、应用监控(请求延迟、错误率、吞吐量)、业务监控(支付成功率、邮件送达率、用户注册转化率)。独立开发者不需要一开始就部署完整的可观测性平台,但关键指标必须有告警。
我用的是组合方案:Vercel Analytics 看页面性能,Sentry 看应用错误,Better Stack(原 Better Uptime)看可用性,Stripe Dashboard 看支付指标。日志用 Axiom 或 Logtail,按用户 ID 和请求 ID 索引,方便追踪单个用户的完整链路。
// ❌ 错误做法:日志没有结构化,没有请求 ID,难以追踪
console.log('User login failed')
console.log('Payment webhook received')
console.log('Database query slow')
// ✅ 正确做法:结构化日志 + 请求 ID 串联
import pino from 'pino'
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label }),
},
})
// 中间件:为每个请求生成唯一 ID
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'] || crypto.randomUUID()
req.requestId = requestId
res.setHeader('x-request-id', requestId)
next()
})
// 业务逻辑:携带上下文的结构化日志
app.post('/api/checkout', async (req, res) => {
const { userId, amount } = req.body
const requestId = req.requestId
logger.info({
event: 'checkout_started',
requestId,
userId,
amount,
currency: 'USD',
})
try {
const session = await stripe.checkout.sessions.create({
// ...
})
logger.info({
event: 'checkout_session_created',
requestId,
userId,
sessionId: session.id,
})
res.json({ sessionId: session.id })
} catch (err) {
logger.error({
event: 'checkout_failed',
requestId,
userId,
error: err.message,
stack: err.stack,
})
res.status(500).json({ error: 'Checkout failed' })
}
})日志的关键是可追踪性。每个请求有唯一 ID,所有相关日志都带上这个 ID 和用户 ID,就能在日志系统中快速过滤出单个用户的完整操作链路。结构化日志(JSON 格式)也方便后续用日志分析工具查询和告警。
数据驻留与合规:GDPR 不是可选项
如果产品面向欧盟用户,GDPR(General Data Protection Regulation)是必须遵守的法律。GDPR 要求个人数据的存储和处理必须在欧盟境内,或者存储在被认为有「充分保护水平」的国家。违反 GDPR 的罚款最高可达全球年营业额的 4% 或 2000 万欧元。
数据驻留(Data Residency)的技术实现有两种方案:一是单区域部署在欧盟(比如 AWS EU-West-1 爱尔兰),所有用户数据都存储在那里;二是多区域部署,欧盟用户的数据路由到欧盟区域的数据库,其他区域的用户路由到各自的区域。
// ❌ 错误做法:所有用户数据都存储在同一个区域,违反 GDPR 数据驻留要求
// 假设数据库部署在 US-East-1,但服务欧盟用户
const dbConfig = {
host: 'db-us-east-1.example.com', // 所有欧盟用户数据都在美国
port: 5432,
database: 'my_saas',
}
// ✅ 正确做法:根据用户所在地路由到对应区域的数据库
import { Client } from 'pg'
const dbConfigs = {
eu: {
host: 'db-eu-west-1.example.com', // 欧盟用户数据存储在欧盟
port: 5432,
database: 'my_saas_eu',
},
us: {
host: 'db-us-east-1.example.com',
port: 5432,
database: 'my_saas_us',
},
}
async function getDbClient(userRegion: 'eu' | 'us') {
return new Client(dbConfigs[userRegion])
}
// 用户注册时,根据其所在地选择数据库
app.post('/api/register', async (req, res) => {
const { email, country } = req.body
// 根据 IP 或用户声明确定区域
const region = isEUCountry(country) ? 'eu' : 'us'
const client = await getDbClient(region)
await client.connect()
try {
await client.query(
'INSERT INTO users (email, country, region) VALUES ($1, $2, $3)',
[email, country, region]
)
res.json({ success: true })
} finally {
await client.end()
}
})除了数据驻留,GDPR 还要求提供数据导出(Data Portability)和数据删除(Right to be Forgotten)功能。产品需要有 API 或界面让用户导出自己的数据,以及申请删除账号和所有相关数据。
基础设施方案对比
| 维度 | Vercel | Fly.io | AWS (独立部署) | Railway |
|---|---|---|---|---|
| 部署模型 | Serverless / Edge Functions | Docker 容器(全球边缘) | 全面自主(EC2/ECS/Lambda) | 容器(单区域) |
| 多区域支持 | 需 Edge Functions | 原生多区域(fly-replay) | 完全自主控制 | 有限(单区域为主) |
| 数据库 | 外部(PlanetScale/Neon/Supabase) | 托管 Postgres + 卷 | 完全自主(RDS/Aurora) | 托管 Postgres/MySQL |
| CDN | 内置(全球边缘) | 需自建或配合 Cloudflare | 需配置 CloudFront | 需配合 Cloudflare |
| 适用阶段 | 早期快速验证 | 中期需要多区域控制 | 大规模自定义 | 快速原型 |
| 月成本估算 | $0-20(Hobby/Pro) | $0-30(按用量) | $50-500+(复杂计费) | $5-50(按用量) |
| CDN / 对象存储方案 | Cloudflare CDN + R2 | Vercel CDN + Blob | AWS CloudFront + S3 |
|---|---|---|---|
| 全球节点覆盖 | 300+ 城市 | 依赖 Vercel 边缘网络 | 450+ 边缘节点 |
| 存储定价 | $0.015/GB/月 | $0.025/GB/月(Blob) | $0.023/GB/月(S3 Standard) |
| 出口流量 | $0(零出口费) | 包含在 Vercel 套餐 | $0.09/GB(首 10TB) |
| 图片优化 | Polish + Image Resizing | 内置 next/image | Lambda@Edge 或 CloudFront Functions |
| 中国访问 | 企业版有中国节点 | 无优化 | 需中国境内 CDN 备案 |
| 10TB/月出口成本 | $0 | 包含在套餐 | ~$891 |
| 多区域灾备策略 | RTO | RPO | 月成本 | 适用场景 |
|---|---|---|---|---|
| Backup & Restore | 小时级 | 小时级 | $ | 非关键业务,数据可丢失 |
| Pilot Light | 十分钟级 | 分钟级 | $$ | 核心服务需快速恢复 |
| Warm Standby | 十分钟级 | 秒级 | $$$ | 高可用要求,快速扩容 |
| Multi-Site Active/Active | 秒级 | 秒级或零 | $$$$ | 零停机要求,全球用户 |
| 合规框架 | 适用范围 | 核心要求 | 违规处罚 |
|---|---|---|---|
| GDPR | 欧盟用户数据 | 数据驻留、用户权利、DPO、DPIA | 全球营业额 4% 或 €2000 万 |
| SOC 2 | B2B SaaS(美国客户) | 安全、可用性、保密性控制 | 客户合同违约 |
| CCPA | 加州居民数据 | 数据披露、删除权、退出权 | $2,500-7,500/次违规 |
| HIPAA | 医疗健康数据 | 数据加密、访问控制、审计日志 | $100-50,000/次违规 |
出海基础设施检查清单
以下清单按上线阶段分组,每项都可执行。
阶段一:上线前(MVP)
- 域名与 DNS:主域名、API 域名、静态资源域名已配置;DNS 启用 Geo 解析;关键记录 TTL 设为 300 秒
- SSL 证书:所有域名启用 HTTPS;证书自动续期(Let's Encrypt 或平台自带)
- CDN 加速:静态资源通过 CDN 分发;图片启用自动压缩和格式转换(WebP/AVIF)
- 对象存储:用户上传文件存储在 R2/S3,不占用应用服务器磁盘;生成预签名 URL 直传
- 支付集成:Stripe/Paddle 已接入;沙箱环境测试通过订阅、取消、退款流程;Webhook 签名验证已启用
- 邮件配置:SPF、DKIM、DMARC 三件套已配置;事务邮件使用独立子域名;送达率监控已启用
- 错误追踪:Sentry 或类似工具已接入;未捕获异常自动上报;Source Map 已上传
- 基础监控:可用性监控已配置(Better Stack / UptimeRobot);API 错误率告警阈值已设定
阶段二:增长期(有真实用户后)
- 结构化日志:所有日志使用 JSON 格式;每个请求携带
requestId和用户 ID;日志保留 30 天 - Webhook 可靠性:Webhook 处理器异步化;幂等性检查已实现;死信队列(DLQ)已配置
- 数据库备份:每日自动备份;跨区域备份已启用;恢复流程已测试
- 速率限制:API 接口启用速率限制(如 100 次/分钟/IP);支付接口更严格(如 10 次/分钟)
- 安全加固:CSP(Content Security Policy)头已配置;CORS 白名单已设定;敏感环境变量加密存储
阶段三:合规与扩展(面向欧盟或 B2B)
- GDPR 合规:欧盟用户数据存储在欧盟区域;数据导出 API 已实现;账号删除功能已实现
- 隐私政策:隐私政策页面已上线;Cookie 同意横幅已配置(面向欧盟用户)
- SOC 2 准备:访问控制审计日志已启用;员工权限最小化;供应商安全评估已完成
- 灾备方案:根据业务需求选择 Pilot Light / Warm Standby / Active-Active;故障切换演练已执行
验收方式
基础设施配置完成后,用以下方式验收:
- 访问速度:用海外代理或 WebPageTest 测试全球多个地区的页面加载时间,目标 TTFB < 500ms
- 支付流程:用 Stripe 测试卡完成订阅、取消、退款全流程;检查 Webhook 是否正确触发和处理
- 邮件送达:用 Gmail、Outlook、Yahoo 邮箱测试注册、登录、账单邮件是否到达收件箱
- 监控告警:手动触发一个错误(比如访问不存在的页面),检查 Sentry 和告警是否在 5 分钟内触发
- 日志追踪:执行一个完整操作(注册 → 登录 → 创建资源),在日志系统中按用户 ID 查询,确认所有相关日志都能被检索到
总结
全球化 SaaS 的基础设施不需要一开始就做到极致。域名拆分、CDN 加速、支付可靠性、邮件送达率、基础监控——这五项是 MVP 阶段必须可靠的。多区域部署、数据驻留、SOC 2 合规可以等到有真实用户后再优化。
关键原则是:支付和邮件比页面动效更重要。用户不能付款或收不到登录邮件,增长流量会被直接浪费。先把关键链路做可靠,再追求极致的性能和合规。
参考资料
- AWS. Disaster Recovery of Workloads on AWS. AWS Whitepaper
- AWS Architecture Blog. Disaster Recovery (DR) Architecture on AWS, Part IV: Multi-site Active-Active. AWS Blog
- Stripe. Receive Stripe events in your webhook endpoint. Stripe Docs
- Stripe Dev Blog. Building resilient webhook handlers in AWS: Implementing DLQs for Stripe Events. Stripe Dev
- Cloudflare. Egress-Free Object Storage — Cloudflare R2. Cloudflare
- Railway Blog. The Best PaaS for Multi-Region Deployments in 2026. Railway
- Fly.io. Multi-region databases and fly-replay. Fly.io Docs
- Alation. Data Residency for SaaS: How to Build Compliant Architecture. Alation Blog
- Sprinto. SOC 2 vs GDPR Explained (2026): Map Once, Comply Twice. Sprinto Blog
- Middleware.io. 10 Observability Best Practices Every DevOps Should Implement. Middleware Blog