跳转到内容

Module 17: 基础设施与云服务

📖 深度参考手册 — 本模块属于理论参考,非主线必读。 主线学习路径见 README.md。 当你在项目实战中遇到相关问题时,回来查阅。

现代应用不需要自己搭建所有东西。了解基础设施组件和云服务选型,能让你用最少的运维成本上线产品。本模块覆盖从域名注册到支付集成的完整链路,帮你建立”从零到上线”的基础设施认知。


定义:域名是互联网上的”门牌号”(如 hotel-booking.com),DNS(Domain Name System)是将域名翻译成服务器 IP 地址的系统。域名注册需要通过注册商(Namecheap、Cloudflare、阿里云万网等)。DNS 记录类型包括:A 记录(域名 → IPv4 地址,如 hotel-booking.com → 1.2.3.4)、AAAA 记录(域名 → IPv6 地址)、CNAME 记录(域名别名,如 www.hotel-booking.com → hotel-booking.com)、MX 记录(邮件服务器,指定谁来收 @hotel-booking.com 的邮件)、TXT 记录(文本信息,常用于域名验证、SPF/DKIM 邮件反垃圾认证)。

为什么重要:域名是用户访问你的产品的入口,DNS 配置是否正确直接影响用户能否打开你的网站。错误的 DNS 配置可能导致:网站无法访问、邮件发不出去或被标记为垃圾邮件(没配 SPF/DKIM)、SSL 证书申请失败(域名验证不通过)。DNS 生效还有传播延迟(几分钟到 48 小时),出问题时排查困难,所以一定要在正式上线前提前配好。

案例URL Shortener — 自定义短域名是核心资产,DNS 配置是第一步。

URL Shortener 的 DNS 配置:
1. 购买短域名: sho.rt(域名越短越好,这是核心产品体验)
2. DNS 记录配置:
sho.rt A → 203.0.113.10 # 主域名指向服务器
sho.rt AAAA → 2001:db8::1 # IPv6支持
www.sho.rt CNAME → sho.rt # www跳转到根域名
sho.rt MX → mx.sendgrid.net # 邮件服务
sho.rt TXT → "v=spf1 include:sendgrid.net ~all" # 邮件认证
3. 等待 DNS 生效:
$ dig sho.rt +short
203.0.113.10 ← DNS 已生效
4. 用户访问 sho.rt/abc123:
浏览器 → DNS查询sho.rt → 得到203.0.113.10 → 请求服务器 → 302重定向到目标URL

实操步骤(以 Cloudflare 为例):

1. 在 Cloudflare 注册并添加域名
2. 修改域名注册商的 NS(Name Server)记录指向 Cloudflare:
ns1.cloudflare.com
ns2.cloudflare.com
3. 在 Cloudflare DNS 面板添加记录:
Type | Name | Content | Proxy | TTL
A | @ | 203.0.113.10 | ✅ | Auto
CNAME | www | @ | ✅ | Auto
4. 开启 Cloudflare 代理(橙色云朵):
- 自动获得 DDoS 防护
- 自动获得 CDN 加速
- 隐藏源站 IP(安全)

先想一想 🤔 URL Shortener 的短域名如果 DNS 解析出了问题(比如 DNS 服务商宕机),所有短链接都打不开了。如何提高 DNS 的可用性?

点击查看解析

DNS 高可用方案:

  1. 使用多个 DNS 服务商:比如同时用 Cloudflare 和 AWS Route 53 作为 DNS 解析。域名注册商处配置两组 NS 记录。任何一家宕机,另一家继续服务。

  2. 降低 TTL:正常情况下 TTL 设为 300 秒(5分钟),这样 DNS 变更可以快速生效。但如果 DNS 服务商宕机,客户端缓存的 DNS 记录在 TTL 过期后就无法刷新了——所以也不要把 TTL 设得太短。

  3. 使用 Anycast DNS:Cloudflare 和 Route 53 都使用 Anycast 技术——同一个 DNS 服务器 IP 在全球多个位置有实例。用户的 DNS 查询会自动路由到最近的实例。某个节点宕机,自动路由到其他节点。

对于 URL Shortener 这种域名是核心资产的服务,DNS 的可靠性就是业务的生命线,双 DNS 服务商是必须的。


定义:反向代理是部署在服务器端的代理服务器,代替后端应用接收客户端请求,然后转发给后端处理。核心功能包括:请求转发(前端静态文件和 API 请求分流到不同后端)、SSL 终止(统一处理 HTTPS 加密解密,后端只需处理 HTTP)、负载均衡(将请求分发到多个后端实例)、静态文件服务(直接返回 HTML/CSS/JS,无需经过应用服务器)、请求限流(防止恶意请求打爆后端)。两个主流选择:Nginx(性能极强,配置灵活但语法复杂)和 Caddy(自动 HTTPS,配置极简,适合中小项目)。

为什么重要:没有反向代理,你的应用直接暴露在互联网上,有多个问题:无法轻松添加 HTTPS、无法做负载均衡、静态文件也要经过应用处理(浪费资源)、无法做请求限流(容易被攻击)。反向代理是生产部署的标配架构层。

案例所有系统 — 前端静态文件由 Nginx 直接服务,API 请求反向代理到 Go 后端。

Nginx 配置示例(Hotel Reservation):

/etc/nginx/conf.d/hotel.conf
upstream api_backend {
server 127.0.0.1:8080; # Go 应用实例 1
server 127.0.0.1:8081; # Go 应用实例 2(负载均衡)
}
server {
listen 80;
server_name hotel-booking.com www.hotel-booking.com;
return 301 https://$server_name$request_uri; # HTTP → HTTPS 重定向
}
server {
listen 443 ssl http2;
server_name hotel-booking.com;
# SSL 证书
ssl_certificate /etc/letsencrypt/live/hotel-booking.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/hotel-booking.com/privkey.pem;
# 前端静态文件(React 构建产物)
location / {
root /var/www/hotel/dist;
try_files $uri $uri/ /index.html; # SPA 路由支持
expires 1y; # 静态资源长缓存
add_header Cache-Control "public, immutable";
}
# API 请求转发到 Go 后端
location /api/ {
proxy_pass http://api_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 请求限流: 每个IP每秒最多10个请求
limit_req zone=api burst=20 nodelay;
}
# WebSocket 支持(实时通知)
location /ws/ {
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
# 限流区域定义
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

Caddy 配置示例(同样的功能,但简洁得多):

# Caddyfile
hotel-booking.com {
# 自动HTTPS(Caddy自动申请和续期Let's Encrypt证书)
# 前端静态文件
handle {
root * /var/www/hotel/dist
try_files {path} /index.html
file_server
}
# API 反向代理
handle /api/* {
reverse_proxy localhost:8080 localhost:8081 {
lb_policy round_robin
}
}
# WebSocket
handle /ws/* {
reverse_proxy localhost:8080
}
# 请求限流
rate_limit {remote.ip} 10r/s
}

先想一想 🤔 为什么 Nginx 配置中 location /try_files $uri $uri/ /index.html 这行?如果去掉会怎样?

点击查看解析

这行配置是 SPA(Single Page Application)路由支持 的关键。

React/Vue/Angular 等前端框架使用客户端路由——URL 变化时不请求服务器,而是由 JavaScript 在浏览器端渲染对应页面。比如用户访问 hotel-booking.com/reservations/123,这个路径在服务器上不存在真正的文件。

try_files 的工作逻辑:

  1. 先尝试找 $uri 对应的文件(如 /assets/style.css
  2. 再尝试找 $uri/ 对应的目录
  3. 都找不到 → 返回 /index.html(SPA 的入口文件)
  4. 然后由 React Router 在浏览器端根据 URL 渲染正确的页面

如果去掉这行:

  • 直接访问 hotel-booking.com → 正常(index.html 存在)
  • 点击导航到 /reservations/123 → 正常(JavaScript 路由处理)
  • 刷新 /reservations/123 页面 → 404 Not Found!(服务器找不到这个文件)
  • /reservations/123 分享给别人,对方打开 → 404!

这是新手部署 SPA 最常见的坑之一。


定义:HTTPS 是 HTTP 的安全版本,通过 TLS(Transport Layer Security)协议对通信进行加密,防止数据在传输过程中被窃听或篡改。HTTPS 需要 SSL/TLS 证书来证明服务器身份。Let’s Encrypt 是一个免费、自动化的证书颁发机构(CA),任何人都可以免费获取 HTTPS 证书。证书有效期为 90 天,必须自动续期。证书的验证级别分为三种:DV(Domain Validation)——只验证你拥有这个域名(Let’s Encrypt 提供的就是这种,免费);OV(Organization Validation)——验证组织身份;EV(Extended Validation)——最严格的验证,地址栏显示公司名称。三种级别的加密强度完全相同,差别仅在于身份验证的严格程度。

为什么重要:没有 HTTPS 的网站:浏览器会显示”不安全”警告(直接劝退用户)、Google 搜索排名会降低、无法使用 HTTP/2 和许多现代 Web API(如 Service Worker、Geolocation)、用户的密码和支付信息在网络上明文传输(极度危险)。2024 年之后,HTTPS 已经不是可选项,而是必需项。

案例所有系统 — 用 Caddy 一行配置搞定自动 HTTPS。

三种获取证书的方式:
方式一: Caddy(最简单,推荐)
# Caddyfile 中只需要写域名,Caddy 自动:
# 1. 向 Let's Encrypt 申请证书
# 2. 完成域名验证(HTTP-01 challenge)
# 3. 安装证书并配置 TLS
# 4. 证书到期前自动续期
hotel-booking.com {
reverse_proxy localhost:8080
}
# 就这么多。没了。真的就一行。
方式二: Certbot + Nginx
# 安装 certbot
sudo apt install certbot python3-certbot-nginx
# 自动申请证书并配置 Nginx
sudo certbot --nginx -d hotel-booking.com -d www.hotel-booking.com
# 设置自动续期(certbot 安装时会自动配置 cron/systemd timer)
sudo certbot renew --dry-run # 测试续期是否正常
方式三: 云服务商(如 AWS ACM)
# AWS Certificate Manager 提供免费证书
# 但只能在 AWS 服务(CloudFront、ALB)上使用
# 不能下载到自己的服务器

常见问题排查:

问题: 证书申请失败
→ 检查DNS A记录是否正确指向服务器IP
→ 检查80端口是否对外开放(Let's Encrypt 需要通过 HTTP-01 验证)
→ 检查防火墙规则
问题: 证书过期
→ Let's Encrypt 证书有效期90天
→ Caddy: 自动续期,无需操心
→ Certbot: 检查 systemd timer 是否正常运行
$ systemctl status certbot.timer
→ 设置监控: 证书有效期 < 14天 时告警
问题: 混合内容 (Mixed Content)
→ 页面是 HTTPS,但加载了 HTTP 的图片/脚本
→ 浏览器会阻止加载或显示警告
→ 解决: 所有资源URL使用 // 或 https://

先想一想 🤔 Let’s Encrypt 的免费证书和 DigiCert 的付费证书($300/年)在安全性上有区别吗?什么情况下需要付费证书?

点击查看解析

加密强度完全相同。Let’s Encrypt 的 DV 证书和 DigiCert 的 DV 证书使用相同的加密算法(如 RSA 2048 位或 ECDSA P-256),浏览器对两者的信任级别也一样。用户的数据在传输中受到的保护是完全相同的。

需要付费证书的场景:

  1. EV 证书:金融、支付等行业可能要求 EV(Extended Validation)证书,地址栏会显示公司名称,增加用户信任感。但实际上大多数用户并不注意这个细节,Chrome 甚至已经不再特别显示 EV 信息了。

  2. 通配符证书管理:Let’s Encrypt 也支持通配符证书(*.hotel-booking.com),但需要 DNS-01 验证(需要自动化 DNS API)。如果你的 DNS 服务商不支持 API,付费证书的人工验证更简单。

  3. 企业合规要求:某些行业合规标准(如 PCI DSS)可能要求使用特定 CA 的证书。

对于绝大多数项目(包括商业项目),Let’s Encrypt 完全够用。把省下的钱花在其他安全措施上(WAF、安全审计)更有价值。


定义:对象存储是一种专门用于存储非结构化数据(文件、图片、视频、文档)的存储服务。与文件系统不同,对象存储是扁平结构(没有目录层级,虽然可以用 / 模拟),每个对象由唯一的 Key 标识。核心操作只有四个:PUT(上传)、GET(下载)、DELETE(删除)、LIST(列举)。主流服务:AWS S3(行业标准 API,几乎所有对象存储都兼容 S3 协议)、MinIO(开源自托管,S3 完全兼容)、Cloudflare R2(无出站流量费用,适合高流量场景)。关键原则:文件(图片/视频/文档)不要存数据库,存对象存储。数据库只存元数据和文件路径。

为什么重要:把文件存在数据库中(如 PostgreSQL 的 BYTEA 类型)会导致:数据库体积膨胀、备份极慢、查询性能下降、无法使用 CDN 加速。对象存储天生为文件存储设计——按量付费、自动冗余备份、支持 CDN 加速、支持预签名 URL(安全地让前端直传文件)。

案例YouTube — 视频文件存 S3/MinIO,数据库只存元数据和文件路径。

YouTube 的视频存储架构:
用户上传视频:
1. 前端向后端请求"预签名上传URL"
POST /api/videos/upload-url
→ 后端生成 S3 预签名URL(有效期15分钟)
→ 返回: { uploadUrl: "https://s3.../videos/raw/abc123.mp4?签名..." }
2. 前端直接上传到 S3(不经过后端,节省带宽)
PUT https://s3.../videos/raw/abc123.mp4
→ 视频文件直接传到对象存储
3. S3 触发事件通知 → 后端收到通知 → 启动转码流水线
原始文件: videos/raw/abc123.mp4 (1080p, 2GB)
→ 转码为多个分辨率:
videos/transcoded/abc123/1080p.mp4
videos/transcoded/abc123/720p.mp4
videos/transcoded/abc123/480p.mp4
videos/transcoded/abc123/360p.mp4
4. 数据库只存元数据:
videos 表:
| id | title | user_id | s3_key | duration | status |
| abc123 | 我的视频 | user_1 | videos/raw/abc123.. | 300 | ready |
用户观看视频:
→ CDN缓存命中 → 直接返回(不到达S3和后端)
→ CDN缓存未命中 → 回源到S3 → 返回并缓存

预签名 URL 代码示例(Go + AWS SDK):

// 生成预签名上传URL(前端拿到这个URL可以直接上传到S3)
func GenerateUploadURL(bucket, key string) (string, error) {
client := s3.NewPresignClient(s3Client)
req, err := client.PresignPutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
ContentType: aws.String("video/mp4"),
}, s3.WithPresignExpires(15*time.Minute))
if err != nil {
return "", err
}
return req.URL, nil
}
// 生成预签名下载URL(用户点击时生成临时可访问的链接)
func GenerateDownloadURL(bucket, key string) (string, error) {
client := s3.NewPresignClient(s3Client)
req, err := client.PresignGetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
}, s3.WithPresignExpires(1*time.Hour))
if err != nil {
return "", err
}
return req.URL, nil
}

先想一想 🤔 为什么 YouTube 让前端直接上传到 S3(通过预签名 URL),而不是让前端先上传到后端,再由后端转存到 S3?

点击查看解析

性能和成本两方面的考虑:

  1. 带宽:一个 1GB 的视频文件,如果经过后端中转,后端需要先接收 1GB(入站),再上传 1GB 到 S3(出站),总共消耗 2GB 带宽。直传只消耗 1GB(前端直接到 S3)。

  2. 服务器资源:视频上传是 I/O 密集型操作,中转时后端需要大量内存缓冲、大量 CPU 处理网络 I/O。如果 100 个用户同时上传 1GB 的视频,后端需要 100GB 的内存/磁盘缓冲。直传完全不占后端资源。

  3. 超时和断点续传:大文件上传容易超时或中断。直传时,前端可以利用 S3 的分片上传(Multipart Upload)能力,断点续传。如果经过后端中转,需要自己实现这些复杂逻辑。

  4. 安全性不受影响:预签名 URL 有时效限制(比如 15 分钟),只能上传到指定的 Key,无法访问其他文件。后端完全控制”谁可以上传什么”。

这个模式叫做 Direct-to-S3 Upload,几乎所有涉及文件上传的系统都应该使用这个模式。


定义:云服务是由云平台提供的即开即用的基础设施和平台服务,分为三层:IaaS(Infrastructure as a Service)——提供虚拟机、网络、存储等基础设施(如 EC2、ECS);PaaS(Platform as a Service)——提供运行环境,不用管服务器(如 Heroku、Google App Engine);SaaS(Software as a Service)——提供完整的软件服务(如 Stripe、SendGrid)。选型需要考虑:地域(国内业务用阿里云/腾讯云)、生态(已用某家就继续,减少集成成本)、价格(不同服务差异巨大)。

为什么重要:选错云服务会导致成本暴增、性能不达标、迁移困难。比如:用了 AWS 的 DynamoDB(私有协议),以后想迁到 GCP 就要重写数据层;国内业务用了 AWS,但 AWS 在中国大陆需要通过光环新网运营,服务受限且贵;小项目用了 Kubernetes,运维复杂度远超业务复杂度。

案例所有系统 — 核心云服务对照表。

服务类型AWSGCPAzure阿里云
计算(虚拟机)EC2Compute EngineVirtual MachinesECS
容器编排ECS / EKSGKEAKSACK
ServerlessLambdaCloud FunctionsAzure Functions函数计算 FC
对象存储S3Cloud StorageBlob StorageOSS
关系数据库RDSCloud SQLAzure DatabaseRDS
文档数据库DynamoDBFirestoreCosmos DB表格存储
缓存ElastiCacheMemorystoreAzure CacheRedis 版
消息队列SQS + SNSPub/SubService BusMNS / RocketMQ
CDNCloudFrontCloud CDNAzure CDNCDN
DNSRoute 53Cloud DNSAzure DNS云解析 DNS
容器镜像仓库ECRArtifact RegistryACRACR
日志/监控CloudWatchCloud MonitoringMonitor日志服务 SLS

选型决策树:

你的业务主要面向哪里?
├─ 中国大陆 → 阿里云/腾讯云(合规、速度、支付集成)
├─ 全球 → AWS(市场份额最大、服务最全)
├─ 已有Google生态 → GCP(K8s原生体验最好、AI/ML服务强)
└─ 已有Microsoft生态 → Azure(AD集成、Office365联动)
你的团队规模和运维能力?
├─ 1-3人团队 → PaaS优先(Vercel/Railway/Fly.io)
├─ 3-10人团队 → 托管服务优先(RDS/ElastiCache,而非自建)
└─ 10+人团队 → 可以考虑更灵活的IaaS组合
预算敏感度?
├─ 极度敏感 → Hetzner/DigitalOcean(VPS)+ 自建服务
├─ 中等 → AWS/阿里云(按需付费、预留实例折扣)
└─ 不太敏感 → 尽量用托管服务(省人力成本比省服务器成本更划算)

先想一想 🤔 一个面向中国大陆用户的 Hotel Reservation 系统,如果选了 AWS 作为云服务商,会遇到什么问题?

点击查看解析

会遇到多个严重问题:

  1. 合规问题:中国法律要求用户数据存储在中国境内。AWS 中国区(由光环新网/西云数据运营)和 AWS 全球区是完全隔离的,很多服务在中国区不可用或功能受限。

  2. ICP 备案:在中国大陆提供互联网服务需要 ICP 备案,备案必须绑定国内云服务商的服务器。用 AWS 全球区的服务器无法完成备案,网站可能被封。

  3. 网络延迟:如果服务器在海外(如东京、新加坡),中国用户访问延迟 100-300ms,体验很差。而且跨境网络不稳定,经常丢包。

  4. 支付集成:微信支付、支付宝的接入文档和 SDK 都以国内云服务商为主,AWS 上的资料和支持较少。

  5. 技术支持:AWS 中国区的技术支持远不如全球区,中文文档也不如阿里云完善。

结论:面向中国大陆用户的业务,优先选择阿里云或腾讯云。如果公司全球化,海外用 AWS/GCP,中国大陆用阿里云/腾讯云,中间打通。


定义:Serverless(无服务器)是一种云计算执行模型,开发者只编写业务逻辑函数,不需要管理服务器。最核心的形态是 FaaS(Function as a Service,函数即服务)——代码以函数为单位部署,由事件触发执行(HTTP 请求、定时任务、消息队列消息等)。主流平台:AWS Lambda、Google Cloud Functions、Vercel Serverless Functions、Cloudflare Workers。特点:不管服务器(无需配置、维护、扩容)、按调用次数计费(没有请求时不花钱)、自动伸缩(1 个请求和 10000 个并发请求,平台自动处理)。

为什么重要:对于流量不可预测或低频的服务,传统服务器(7×24 小时运行)大部分时间在空跑,浪费钱。Serverless 的”用多少付多少”模型对这类场景极度友好——一个月只有 1000 次调用的 API,成本可能只有几分钱。但 Serverless 也有明显限制:冷启动延迟(函数长时间未调用后首次执行会慢几百毫秒到几秒)、执行时间限制(Lambda 最长 15 分钟)、无法保持本地状态(每次调用可能在不同的实例上运行)。

案例URL Shortener — 短链接生成(低频写入)可以用 Serverless;重定向(高频读)不适合。

URL Shortener 的 Serverless 分析:
适合 Serverless 的部分(低频、突发):
✅ 创建短链接 API
- 调用频率低(每天几千次)
- 偶尔有突发(营销活动时批量创建)
- 冷启动延迟可接受(创建操作对延迟不敏感)
✅ 统计报告生成
- 每天定时生成一次(cron触发Lambda)
- 运行时间几分钟
- 不需要常驻服务器
✅ Webhook 处理
- 接收第三方回调(如支付确认)
- 频率不可预测,可能很久没有请求
不适合 Serverless 的部分(高频、低延迟):
❌ 短链接重定向 (GET /abc123 → 302 Location: https://...)
- 每秒可能数万次请求
- 要求极低延迟(用户点击后要立即跳转)
- 冷启动会导致首次访问慢几百毫秒(用户体验差)
- 高频调用下,Serverless 的按次计费反而比常驻服务器贵
❌ 实时点击流收集
- 高频写入,需要常驻的流处理服务
最优方案: 混合架构
创建短链接 → Serverless Function (Lambda/Vercel)
短链接重定向 → 常驻服务器 (Go/Nginx) + CDN缓存
统计报告 → Serverless Function (定时触发)

Vercel Serverless Function 示例:

// api/shorten.js(Vercel Serverless Function)
import { nanoid } from 'nanoid'
import { db } from '../lib/db'
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
const { url } = req.body
const shortId = nanoid(8)
await db.insert('urls', { short_id: shortId, original_url: url })
return res.status(201).json({
shortUrl: `https://sho.rt/${shortId}`,
originalUrl: url,
})
}

先想一想 🤔 Chat System 的消息推送能用 Serverless 实现吗?

点击查看解析

不能,至少不能直接用。原因:

  1. WebSocket 长连接:Chat System 需要 WebSocket 保持客户端和服务器的持久连接,以便实时推送消息。但 Serverless 函数是短生命周期的——执行完就销毁,无法维持长连接。AWS Lambda 最长只能运行 15 分钟。

  2. 有状态:Chat System 需要知道”用户 A 当前连在哪个实例上”,以便把消息推送到正确的连接。Serverless 是无状态的,每次调用可能在不同实例上运行。

替代方案:

  • 使用专门的实时通信服务:如 AWS AppSync(托管 WebSocket)、Pusher、Ably、Socket.io Cloud。这些服务帮你管理 WebSocket 连接,你只需通过 API 发送消息。
  • 混合架构:WebSocket 网关用常驻服务器(或 AWS API Gateway WebSocket API),消息处理逻辑用 Serverless。网关负责维持连接,Lambda 负责处理消息路由和存储。
  • Cloudflare Durable Objects:一种有状态的 Serverless,可以维持 WebSocket 连接,适合 Chat 场景。但这是 Cloudflare 私有技术,有锁定风险。

定义:BaaS(Backend as a Service)是提供开箱即用的后端功能的云服务,开发者无需从零搭建后端。核心功能通常包括:数据库(可直接从前端查询)、身份认证(注册/登录/OAuth)、文件存储、实时数据订阅(数据变化自动推送到客户端)、Edge Functions(服务端逻辑)。主流平台:Supabase(开源 Firebase 替代,基于 PostgreSQL + GoTrue + PostgREST + Realtime)、Firebase(Google 生态,Firestore + Auth + Hosting + Cloud Functions)、Appwrite(开源自托管 BaaS)。

为什么重要:从零搭建一个完整的后端(用户认证、数据库设计、API 开发、文件存储、实时通信)可能需要 2-4 周。BaaS 让这些成为开箱即用的功能,几小时就能搭出一个功能完整的 MVP。但 BaaS 也有局限:深度定制困难(复杂的业务逻辑难以在 BaaS 中表达)、供应商锁定(迁移成本高)、大规模下成本可能高于自建。选型原则:快速验证想法 → BaaS;产品验证成功需要深度定制 → 自建后端。

案例适合 MVP 阶段的所有系统 — 用 Supabase 1 小时搭出 Hotel Reservation 的 MVP。

用 Supabase 快速搭建 Hotel Reservation MVP:
1. 创建 Supabase 项目(2分钟)
→ 自动获得 PostgreSQL 数据库 + REST API + 实时订阅 + 认证 + 存储
2. 设计数据库表(10分钟)
→ 在 Supabase Dashboard 的 SQL 编辑器中:
CREATE TABLE hotels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
location TEXT,
price_per_night DECIMAL(10,2),
available_rooms INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE reservations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
hotel_id UUID REFERENCES hotels(id),
user_id UUID REFERENCES auth.users(id), -- Supabase内置用户表
check_in DATE NOT NULL,
check_out DATE NOT NULL,
status TEXT DEFAULT 'confirmed',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 开启 Row Level Security(用户只能看自己的预订)
ALTER TABLE reservations ENABLE ROW LEVEL SECURITY;
CREATE POLICY "用户只能查看自己的预订"
ON reservations FOR SELECT
USING (auth.uid() = user_id);
3. 前端直接调用 Supabase SDK(20分钟)
// 注册/登录
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123'
})
// 查询可用酒店
const { data: hotels } = await supabase
.from('hotels')
.select('*')
.gt('available_rooms', 0)
.order('price_per_night', { ascending: true })
// 创建预订
const { data: reservation } = await supabase
.from('reservations')
.insert({
hotel_id: selectedHotel.id,
check_in: '2024-06-01',
check_out: '2024-06-03',
})
.select()
.single()
// 实时监听预订状态变化
supabase
.channel('my-reservations')
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'reservations',
filter: `user_id=eq.${userId}`,
}, (payload) => {
console.log('预订状态更新:', payload.new.status)
})
.subscribe()
4. 部署前端到 Vercel(5分钟)
→ vercel deploy
→ 完成!一个有认证、预订、实时通知的 MVP
总耗时: ~1小时(vs 自建后端 ~2-4周)

BaaS vs 自建后端对比:

维度BaaS (Supabase)自建后端 (Go+Gin)
开发速度极快(小时级)慢(周级)
定制能力受限(复杂逻辑靠 Edge Functions)完全自由
成本(初期)免费/极低服务器+人力成本
成本(大规模)可能很高可控
数据掌控在云端(但 Supabase 可以自托管)完全自控
迁移难度高(强依赖SDK)低(标准API)

先想一想 🤔 如果用 Supabase 搭建的 Hotel Reservation MVP 验证成功了,决定扩展为正式产品。你会继续用 Supabase 还是迁移到自建后端?判断标准是什么?

点击查看解析

判断标准:

继续用 Supabase 如果

  • 业务逻辑相对简单(CRUD 为主,少量复杂逻辑用 Edge Functions 能搞定)
  • 团队规模小(1-3 人),没有专职后端开发
  • 流量在 Supabase 的免费/Pro 计划能覆盖(月活用户 < 10 万)
  • 实时功能(预订状态推送)是核心需求(Supabase Realtime 开箱即用)

迁移到自建后端如果

  • 需要复杂的业务逻辑:如房态日历管理、多人并发预订同一间房的冲突处理、复杂的定价策略(动态定价、促销叠加)、订单状态机等——这些在 Supabase 中实现起来很别扭
  • 需要集成多个外部系统:支付网关、渠道管理(OTA 对接)、PMS(酒店管理系统)
  • 对性能有特殊要求:如需要在应用层做复杂缓存策略
  • 团队扩大到 5+ 人:自建后端的代码更容易多人协作和测试

渐进式迁移方案:不需要一次性全部迁移。可以保留 Supabase 的 Auth 和 Realtime,把复杂业务逻辑抽到自建的 Go 后端,两者共用同一个 PostgreSQL 数据库(Supabase 提供直连字符串)。


定义:邮件和通知服务用于向用户发送系统消息,包括三种渠道:邮件(注册确认、密码重置、订单通知等事务性邮件,以及营销邮件)、短信(验证码、紧急通知)、推送通知(App 推送、Web 推送)。主流邮件服务:SendGrid(Twilio 旗下,功能全面)、Resend(API 设计优雅,开发者友好)、AWS SES(最便宜,但功能基础)。短信服务:Twilio(国际短信)、阿里云短信(国内短信)。推送通知:FCM(Firebase Cloud Messaging,Android + Web)、APNs(Apple Push Notification Service,iOS)。

为什么重要:自己搭建 SMTP 邮件服务器发送邮件几乎必定进垃圾箱——因为没有 IP 信誉、没有正确的 SPF/DKIM/DMARC 配置。专业的邮件服务商已经解决了送达率问题:高信誉 IP 池、自动配置邮件认证、退信处理、投诉处理。事务性邮件(如预订确认)的送达率直接影响用户体验和业务——用户收不到确认邮件,就会认为预订失败。

案例Hotel Reservation — 预订确认邮件、入住提醒、取消通知。

Hotel Reservation 的通知策略:
预订成功:
├─ 📧 邮件(立即): 预订确认 + 订单详情 + 取消链接
├─ 📱 App推送(立即): "预订成功!查看详情"
└─ 💬 短信(立即): "您已成功预订XX酒店,入住日期X月X日"
入住前提醒:
├─ 📧 邮件(入住前3天): 入住指南 + 交通信息 + 天气预报
└─ 📱 App推送(入住前1天): "明天入住XX酒店,别忘了带身份证"
取消/变更:
├─ 📧 邮件(立即): 取消确认 + 退款信息
└─ 💬 短信(立即): 取消确认(简短)
通知优先级策略:
关键信息(预订确认、取消) → 邮件 + 短信 + 推送(三重保障)
提醒信息(入住提醒) → 邮件 + 推送
营销信息(促销活动) → 仅邮件(需用户同意)

使用 Resend 发送邮件的代码示例(Go):

package notification
import (
"bytes"
"encoding/json"
"net/http"
)
type Email struct {
From string `json:"from"`
To []string `json:"to"`
Subject string `json:"subject"`
HTML string `json:"html"`
}
func SendReservationConfirmation(userEmail, hotelName, checkIn, checkOut string) error {
email := Email{
From: "Hotel Booking <noreply@hotel-booking.com>",
To: []string{userEmail},
Subject: "预订确认 - " + hotelName,
HTML: `
<h1>预订成功!</h1>
<p>您已成功预订 <strong>` + hotelName + `</strong></p>
<ul>
<li>入住日期: ` + checkIn + `</li>
<li>退房日期: ` + checkOut + `</li>
</ul>
<p><a href="https://hotel-booking.com/reservations">查看预订详情</a></p>
`,
}
body, _ := json.Marshal(email)
req, _ := http.NewRequest("POST", "https://api.resend.com/emails", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+resendAPIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}

先想一想 🤔 Hotel Reservation 系统需要在用户预订成功后发送确认邮件。如果邮件发送失败(SendGrid 暂时不可用),应该怎么处理?让预订也失败吗?

点击查看解析

绝对不能让预订因为邮件发送失败而失败。邮件是”辅助功能”,预订是”核心业务”——不能让非核心功能的故障影响核心业务。

正确做法:

  1. 异步发送:预订成功后,把”发送确认邮件”作为一个异步任务放入消息队列(如 Kafka/Redis Queue),立即返回”预订成功”给用户。邮件服务异步消费队列并发送。

  2. 重试机制:邮件发送失败后自动重试,采用指数退避策略(1分钟后重试 → 5分钟后重试 → 30分钟后重试),最多重试 5 次。

  3. 备用渠道:如果邮件重试 5 次都失败了,通过短信或 App 推送发送简要的预订确认信息。

  4. 补发机制:提供”重新发送确认邮件”按钮,用户可以在预订详情页手动触发。

  5. 监控告警:邮件发送失败率超过阈值时告警,运维人员介入排查。

这体现了一个重要的系统设计原则:核心流程不应该依赖非核心服务。发送邮件、记录日志、发送统计事件等辅助操作都应该是异步的,不阻塞核心业务。


定义:支付集成是将第三方支付服务接入应用系统的过程。主流支付平台:Stripe(国际支付,API 设计被公认为行业标杆)、PayPal(全球覆盖广,但 API 设计较老)、微信支付/支付宝(中国大陆必备)。典型集成流程:前端创建支付意图(Payment Intent)→ 跳转/弹出支付界面 → 用户完成支付 → 支付平台通过 Webhook 回调通知后端 → 后端验证并更新订单状态。关键概念:幂等性(同一个支付可能 Webhook 回调多次,处理逻辑必须保证执行一次和执行多次效果相同)、支付状态机(待支付 → 已支付 → 已退款,每个状态转换都有严格的校验)。

为什么重要:支付是最敏感的业务环节——多收费、少收费、重复收费、钱收了但订单没更新,任何一个问题都是严重事故。支付集成的核心挑战不是”调通 API”(文档都很清楚),而是处理各种异常情况:网络超时、Webhook 重复投递、并发支付冲突、退款部分成功等。这些边缘场景处理不好就会出财务问题。

案例Hotel Reservation — Stripe 集成流程详解。

Hotel Reservation 的 Stripe 支付流程:
1. 用户选择酒店和日期,点击"支付"
前端 → POST /api/reservations/create-payment
后端:
a. 创建预订记录(status: "pending_payment")
b. 调用 Stripe API 创建 PaymentIntent
c. 返回 client_secret 给前端
2. 前端展示 Stripe 支付表单
→ Stripe.js 渲染信用卡输入框(PCI合规,卡号不经过你的服务器)
→ 用户输入卡号,点击确认
→ Stripe.js 直接与 Stripe 通信完成支付
3. Stripe 通过 Webhook 通知你的后端
POST https://hotel-booking.com/api/webhooks/stripe
事件: payment_intent.succeeded
数据: { payment_intent_id: "pi_xxx", amount: 50000, ... }
4. 后端处理 Webhook
a. 验证 Webhook 签名(防伪造)
b. 查找对应的预订记录
c. 更新预订状态: "pending_payment" → "confirmed"
d. 扣减可用房间数
e. 发送确认邮件
5. 异常处理:
├─ 支付失败 → 预订保持 "pending_payment",30分钟后自动取消
├─ Webhook 未收到 → 定时任务查询 Stripe 确认支付状态
├─ Webhook 重复投递 → 幂等处理(检查预订是否已确认,已确认则跳过)
└─ 退款 → Stripe API 发起退款 → Webhook通知 → 更新状态为 "refunded"

Webhook 处理的关键代码(Go):

func HandleStripeWebhook(c *gin.Context) {
// 1. 读取请求体
payload, _ := io.ReadAll(c.Request.Body)
// 2. 验证 Webhook 签名(防止伪造请求)
event, err := webhook.ConstructEvent(
payload,
c.GetHeader("Stripe-Signature"),
webhookSecret,
)
if err != nil {
c.JSON(400, gin.H{"error": "签名验证失败"})
return
}
// 3. 根据事件类型处理
switch event.Type {
case "payment_intent.succeeded":
var pi stripe.PaymentIntent
json.Unmarshal(event.Data.Raw, &pi)
// 4. 幂等处理:检查是否已处理过
reservation, _ := db.FindReservationByPaymentIntentID(pi.ID)
if reservation.Status == "confirmed" {
// 已经处理过了,直接返回成功(幂等)
c.JSON(200, gin.H{"status": "already_processed"})
return
}
// 5. 在事务中更新状态(原子操作)
err := db.Transaction(func(tx *gorm.DB) error {
// 更新预订状态
if err := tx.Model(&reservation).
Update("status", "confirmed").Error; err != nil {
return err
}
// 扣减可用房间
if err := tx.Model(&Hotel{}).
Where("id = ? AND available_rooms > 0", reservation.HotelID).
Update("available_rooms", gorm.Expr("available_rooms - 1")).Error; err != nil {
return err // 没有可用房间,事务回滚
}
return nil
})
if err != nil {
// 需要退款(房间已被订完)
stripe.Refund(pi.ID)
c.JSON(200, gin.H{"status": "refunded"})
return
}
// 6. 异步发送确认邮件
go sendConfirmationEmail(reservation)
case "charge.refunded":
// 处理退款...
}
c.JSON(200, gin.H{"status": "ok"})
}

支付状态机:

支付成功
pending_payment ──────────→ confirmed
│ │
│ 超时(30min) │ 用户申请退款
▼ ▼
cancelled refunded

先想一想 🤔 为什么 Stripe Webhook 可能会重复投递同一个事件?如果不做幂等处理会怎样?

点击查看解析

Webhook 重复投递的原因:

  1. 网络问题:Stripe 发送了 Webhook,你的服务器收到并处理了,但返回 200 的响应在网络中丢失了。Stripe 认为投递失败,会重试。
  2. 超时:你的服务器处理 Webhook 耗时太长(比如发邮件很慢),Stripe 在等待超时后认为失败,重试。
  3. Stripe 内部重试机制:Stripe 对失败的 Webhook 会在接下来的几小时内多次重试(最多数十次)。

如果不做幂等处理:

  • 重复扣减房间数:可用房间从 10 → 9 → 8,但实际只有一个预订。
  • 重复发送确认邮件:用户收到多封相同的确认邮件。
  • 财务对账异常:系统记录的收入可能与 Stripe 实际金额不匹配。

幂等处理的核心:用 payment_intent_id 作为唯一标识,处理前先查询该支付是否已经处理过。如果是,直接返回成功,不执行任何业务逻辑。这也是为什么上面的代码中有 if reservation.Status == "confirmed" { return } 这个检查。


定义:CDN(Content Delivery Network,内容分发网络)是在全球各地部署的缓存服务器网络,将内容缓存到离用户最近的节点。用户请求时,由最近的 CDN 节点直接返回内容,而不需要请求远在千里之外的源站服务器。主流 CDN:Cloudflare(免费计划就很强大,附带 DDoS 防护和 WAF)、AWS CloudFront(与 AWS 生态深度集成)。核心配置要点:缓存规则(什么该缓存、缓存多久)、缓存刷新(发版后清除旧缓存)、边缘计算(在 CDN 节点运行代码逻辑,如 Cloudflare Workers、Lambda@Edge)。

为什么重要:没有 CDN 时,所有用户的请求都打到源站服务器——北京的用户访问部署在美国的服务器,网络延迟 200ms+,页面加载肉眼可见的慢。CDN 把静态内容缓存到全球各地的边缘节点,用户从最近的节点获取内容,延迟降低到 10-30ms。对于图片、视频、静态网页等内容,CDN 可以把源站的带宽压力降低 90% 以上。

案例Google Maps — 地图瓦片全球 CDN 缓存,不同 zoom 级别不同 TTL。

Google Maps 的 CDN 策略:
地图瓦片(Map Tiles)的特点:
- 数量巨大: 全球地图在每个zoom级别有数十亿个瓦片
- 更新频率不一:
zoom 1-10 (大范围): 很少更新(大陆/国家/省级边界变化极少)
zoom 11-15 (城市级): 偶尔更新(新修路、新建筑)
zoom 16-20 (街道级): 较频繁更新(商铺变化、道路施工)
CDN缓存策略:
zoom 1-8 (洲/国家/省):
Cache-Control: public, max-age=2592000 # 30天缓存
→ 这些瓦片几乎不变,长缓存
zoom 9-14 (城市/区域):
Cache-Control: public, max-age=604800 # 7天缓存
→ 偶尔有变化,一周刷新一次
zoom 15-20 (街道/建筑):
Cache-Control: public, max-age=86400 # 1天缓存
→ 变化较频繁,每天刷新
实时交通叠加层:
Cache-Control: no-cache # 不缓存
→ 交通路况必须实时,不能缓存
效果:
→ 全球缓存命中率 > 95%(大部分用户看的都是已缓存的瓦片)
→ 源站只需要处理 < 5% 的请求(新瓦片生成、实时数据)
→ 用户体验: 地图丝滑流畅(瓦片从最近CDN节点加载,延迟极低)

Cloudflare 实际配置示例:

# Cloudflare Page Rules(或 Cache Rules)
# 规则1: 静态资源(JS/CSS/图片)— 长缓存
URL: hotel-booking.com/assets/*
设置:
Cache Level: Cache Everything
Edge Cache TTL: 1 year
Browser Cache TTL: 1 year
→ 前端构建工具会给文件名加hash(如 app.a1b2c3.js)
→ 文件内容变了hash就变了,URL变了就是新资源
→ 所以可以安全地设置超长缓存
# 规则2: API请求 — 不缓存
URL: hotel-booking.com/api/*
设置:
Cache Level: Bypass
→ API返回的是动态数据,不能缓存
→ 否则用户看到的可能是别人的预订信息!
# 规则3: HTML页面 — 短缓存或不缓存
URL: hotel-booking.com/*
设置:
Cache Level: Cache Everything
Edge Cache TTL: 5 minutes
Browser Cache TTL: 0 (no-cache)
→ HTML页面可能包含用户特定内容
→ 短缓存或不缓存
# 发版后清除缓存:
# Cloudflare API 或 Dashboard 中 Purge Cache
# 通常在CI/CD流水线中自动执行:
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
-H "Authorization: Bearer {api_token}" \
-d '{"purge_everything": true}'

边缘计算示例(Cloudflare Workers):

// 在CDN边缘节点运行的代码(延迟极低)
// 用途:根据用户地理位置返回最近的酒店
export default {
async fetch(request) {
const { cf } = request // Cloudflare 自动提供地理位置信息
const country = cf.country // 如 "CN"
const city = cf.city // 如 "Shanghai"
const latitude = cf.latitude // 如 "31.2304"
const longitude = cf.longitude
// 在边缘节点直接处理,不回源
// 从边缘KV存储中获取该城市的热门酒店(预先缓存)
const hotHotels = await HOT_HOTELS_KV.get(`${country}:${city}`, 'json')
if (hotHotels) {
return new Response(JSON.stringify(hotHotels), {
headers: { 'Content-Type': 'application/json' },
})
}
// 缓存未命中,回源请求
return fetch(`https://api.hotel-booking.com/hotels/nearby?lat=${latitude}&lon=${longitude}`)
},
}

先想一想 🤔 如果 Hotel Reservation 系统的前端发了新版本(修了一个严重 bug),但用户浏览器一直用的是 CDN 缓存的旧版本 JS 文件,怎么办?

点击查看解析

这就是著名的”缓存失效”问题。解决方案:

  1. 构建工具的内容哈希(预防手段,最佳实践): Vite/Webpack 构建时会自动给文件名加上内容哈希:app.jsapp.a1b2c3.js。文件内容变了,哈希就变了,文件名就变了,浏览器会请求新的 URL,不会命中旧缓存。HTML 文件(引用了新的 JS 文件名)设置不缓存或短缓存。

  2. CDN 缓存清除(紧急手段): 在 CI/CD 流水线中,部署完成后自动调用 CDN API 清除缓存(Purge All)。Cloudflare 全球节点清除缓存通常在 30 秒内完成。

  3. Service Worker 更新(如果用了 PWA): Service Worker 可以拦截请求并返回缓存,即使 CDN 缓存清了,Service Worker 缓存也需要更新。需要在 Service Worker 中实现版本检查和自动更新逻辑。

最佳组合:内容哈希(预防) + CDN Purge(兜底) + 短 TTL 的 HTML(确保新文件名能被加载)。这样即使不手动清缓存,用户也会在 HTML 缓存过期后(通常几分钟)自动加载到新版本。


练习一:为 Hotel Reservation 搭建完整基础设施

Section titled “练习一:为 Hotel Reservation 搭建完整基础设施”

为一个新项目(Hotel Reservation)设计从零到上线的完整基础设施架构,包含以下环节:

  1. 域名和 DNS:选择域名注册商,配置 DNS 记录
  2. 反向代理:选择 Nginx 或 Caddy,写出配置文件
  3. HTTPS:选择证书获取方式
  4. 对象存储:酒店图片存储方案(选择 S3/MinIO/R2)
  5. 数据库:PostgreSQL 部署方案(自托管 vs 云托管)
  6. CDN:静态资源和图片的缓存策略

请画出完整的架构图,标注每个组件的作用和数据流向:

参考架构图:
用户浏览器
┌──────────┐
│Cloudflare│ ← DNS + CDN + DDoS防护 + 自动HTTPS
│ CDN │
└────┬─────┘
│ 缓存未命中
┌──────────┐
│ Caddy │ ← 反向代理 + SSL终止
│ 代理 │
└────┬─────┘
├─ 静态文件(/) → /var/www/hotel/dist (React构建产物)
├─ API(/api) → Go后端 (localhost:8080)
│ │
│ ├─ PostgreSQL (数据库)
│ ├─ Redis (缓存/会话)
│ └─ Resend (邮件服务)
└─ 图片(/images) → Cloudflare R2 (对象存储)

同一个 Hotel Reservation 项目,对比两种方案的成本和开发时间:

方案 A:全自建

  • VPS(Hetzner/DigitalOcean)+ Docker Compose
  • PostgreSQL 自己跑
  • Nginx + Certbot
  • MinIO 自托管

方案 B:全托管

  • Supabase(数据库 + 认证 + 实时)
  • Vercel(前端 + Serverless API)
  • Cloudflare R2(对象存储)
  • Resend(邮件)

对比维度:

  • 月度成本(100 用户 / 1000 用户 / 10000 用户)
  • 开发时间(从零到上线)
  • 运维复杂度
  • 可扩展性
  • 迁移风险