Module 17: 基础设施与云服务
📖 深度参考手册 — 本模块属于理论参考,非主线必读。 主线学习路径见 README.md。 当你在项目实战中遇到相关问题时,回来查阅。
现代应用不需要自己搭建所有东西。了解基础设施组件和云服务选型,能让你用最少的运维成本上线产品。本模块覆盖从域名注册到支付集成的完整链路,帮你建立”从零到上线”的基础设施认知。
17.1 域名与 DNS 配置
Section titled “17.1 域名与 DNS 配置”定义:域名是互联网上的”门牌号”(如 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.com3. 在 Cloudflare DNS 面板添加记录: Type | Name | Content | Proxy | TTL A | @ | 203.0.113.10 | ✅ | Auto CNAME | www | @ | ✅ | Auto4. 开启 Cloudflare 代理(橙色云朵): - 自动获得 DDoS 防护 - 自动获得 CDN 加速 - 隐藏源站 IP(安全)先想一想 🤔 URL Shortener 的短域名如果 DNS 解析出了问题(比如 DNS 服务商宕机),所有短链接都打不开了。如何提高 DNS 的可用性?
点击查看解析
DNS 高可用方案:
使用多个 DNS 服务商:比如同时用 Cloudflare 和 AWS Route 53 作为 DNS 解析。域名注册商处配置两组 NS 记录。任何一家宕机,另一家继续服务。
降低 TTL:正常情况下 TTL 设为 300 秒(5分钟),这样 DNS 变更可以快速生效。但如果 DNS 服务商宕机,客户端缓存的 DNS 记录在 TTL 过期后就无法刷新了——所以也不要把 TTL 设得太短。
使用 Anycast DNS:Cloudflare 和 Route 53 都使用 Anycast 技术——同一个 DNS 服务器 IP 在全球多个位置有实例。用户的 DNS 查询会自动路由到最近的实例。某个节点宕机,自动路由到其他节点。
对于 URL Shortener 这种域名是核心资产的服务,DNS 的可靠性就是业务的生命线,双 DNS 服务商是必须的。
17.2 反向代理(Nginx/Caddy)
Section titled “17.2 反向代理(Nginx/Caddy)”定义:反向代理是部署在服务器端的代理服务器,代替后端应用接收客户端请求,然后转发给后端处理。核心功能包括:请求转发(前端静态文件和 API 请求分流到不同后端)、SSL 终止(统一处理 HTTPS 加密解密,后端只需处理 HTTP)、负载均衡(将请求分发到多个后端实例)、静态文件服务(直接返回 HTML/CSS/JS,无需经过应用服务器)、请求限流(防止恶意请求打爆后端)。两个主流选择:Nginx(性能极强,配置灵活但语法复杂)和 Caddy(自动 HTTPS,配置极简,适合中小项目)。
为什么重要:没有反向代理,你的应用直接暴露在互联网上,有多个问题:无法轻松添加 HTTPS、无法做负载均衡、静态文件也要经过应用处理(浪费资源)、无法做请求限流(容易被攻击)。反向代理是生产部署的标配架构层。
案例:所有系统 — 前端静态文件由 Nginx 直接服务,API 请求反向代理到 Go 后端。
Nginx 配置示例(Hotel Reservation):
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的工作逻辑:
- 先尝试找
$uri对应的文件(如/assets/style.css)- 再尝试找
$uri/对应的目录- 都找不到 → 返回
/index.html(SPA 的入口文件)- 然后由 React Router 在浏览器端根据 URL 渲染正确的页面
如果去掉这行:
- 直接访问
hotel-booking.com→ 正常(index.html 存在)- 点击导航到
/reservations/123→ 正常(JavaScript 路由处理)- 刷新
/reservations/123页面 → 404 Not Found!(服务器找不到这个文件)- 把
/reservations/123分享给别人,对方打开 → 404!这是新手部署 SPA 最常见的坑之一。
17.3 HTTPS 证书
Section titled “17.3 HTTPS 证书”定义: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),浏览器对两者的信任级别也一样。用户的数据在传输中受到的保护是完全相同的。
需要付费证书的场景:
EV 证书:金融、支付等行业可能要求 EV(Extended Validation)证书,地址栏会显示公司名称,增加用户信任感。但实际上大多数用户并不注意这个细节,Chrome 甚至已经不再特别显示 EV 信息了。
通配符证书管理:Let’s Encrypt 也支持通配符证书(
*.hotel-booking.com),但需要 DNS-01 验证(需要自动化 DNS API)。如果你的 DNS 服务商不支持 API,付费证书的人工验证更简单。企业合规要求:某些行业合规标准(如 PCI DSS)可能要求使用特定 CA 的证书。
对于绝大多数项目(包括商业项目),Let’s Encrypt 完全够用。把省下的钱花在其他安全措施上(WAF、安全审计)更有价值。
17.4 对象存储
Section titled “17.4 对象存储”定义:对象存储是一种专门用于存储非结构化数据(文件、图片、视频、文档)的存储服务。与文件系统不同,对象存储是扁平结构(没有目录层级,虽然可以用 / 模拟),每个对象由唯一的 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?
点击查看解析
性能和成本两方面的考虑:
带宽:一个 1GB 的视频文件,如果经过后端中转,后端需要先接收 1GB(入站),再上传 1GB 到 S3(出站),总共消耗 2GB 带宽。直传只消耗 1GB(前端直接到 S3)。
服务器资源:视频上传是 I/O 密集型操作,中转时后端需要大量内存缓冲、大量 CPU 处理网络 I/O。如果 100 个用户同时上传 1GB 的视频,后端需要 100GB 的内存/磁盘缓冲。直传完全不占后端资源。
超时和断点续传:大文件上传容易超时或中断。直传时,前端可以利用 S3 的分片上传(Multipart Upload)能力,断点续传。如果经过后端中转,需要自己实现这些复杂逻辑。
安全性不受影响:预签名 URL 有时效限制(比如 15 分钟),只能上传到指定的 Key,无法访问其他文件。后端完全控制”谁可以上传什么”。
这个模式叫做 Direct-to-S3 Upload,几乎所有涉及文件上传的系统都应该使用这个模式。
17.5 云服务选型
Section titled “17.5 云服务选型”定义:云服务是由云平台提供的即开即用的基础设施和平台服务,分为三层: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,运维复杂度远超业务复杂度。
案例:所有系统 — 核心云服务对照表。
| 服务类型 | AWS | GCP | Azure | 阿里云 |
|---|---|---|---|---|
| 计算(虚拟机) | EC2 | Compute Engine | Virtual Machines | ECS |
| 容器编排 | ECS / EKS | GKE | AKS | ACK |
| Serverless | Lambda | Cloud Functions | Azure Functions | 函数计算 FC |
| 对象存储 | S3 | Cloud Storage | Blob Storage | OSS |
| 关系数据库 | RDS | Cloud SQL | Azure Database | RDS |
| 文档数据库 | DynamoDB | Firestore | Cosmos DB | 表格存储 |
| 缓存 | ElastiCache | Memorystore | Azure Cache | Redis 版 |
| 消息队列 | SQS + SNS | Pub/Sub | Service Bus | MNS / RocketMQ |
| CDN | CloudFront | Cloud CDN | Azure CDN | CDN |
| DNS | Route 53 | Cloud DNS | Azure DNS | 云解析 DNS |
| 容器镜像仓库 | ECR | Artifact Registry | ACR | ACR |
| 日志/监控 | CloudWatch | Cloud Monitoring | Monitor | 日志服务 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 作为云服务商,会遇到什么问题?
点击查看解析
会遇到多个严重问题:
合规问题:中国法律要求用户数据存储在中国境内。AWS 中国区(由光环新网/西云数据运营)和 AWS 全球区是完全隔离的,很多服务在中国区不可用或功能受限。
ICP 备案:在中国大陆提供互联网服务需要 ICP 备案,备案必须绑定国内云服务商的服务器。用 AWS 全球区的服务器无法完成备案,网站可能被封。
网络延迟:如果服务器在海外(如东京、新加坡),中国用户访问延迟 100-300ms,体验很差。而且跨境网络不稳定,经常丢包。
支付集成:微信支付、支付宝的接入文档和 SDK 都以国内云服务商为主,AWS 上的资料和支持较少。
技术支持:AWS 中国区的技术支持远不如全球区,中文文档也不如阿里云完善。
结论:面向中国大陆用户的业务,优先选择阿里云或腾讯云。如果公司全球化,海外用 AWS/GCP,中国大陆用阿里云/腾讯云,中间打通。
17.6 Serverless
Section titled “17.6 Serverless”定义: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 实现吗?
点击查看解析
不能,至少不能直接用。原因:
WebSocket 长连接:Chat System 需要 WebSocket 保持客户端和服务器的持久连接,以便实时推送消息。但 Serverless 函数是短生命周期的——执行完就销毁,无法维持长连接。AWS Lambda 最长只能运行 15 分钟。
有状态: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 私有技术,有锁定风险。
17.7 BaaS(Backend as a Service)
Section titled “17.7 BaaS(Backend as a Service)”定义: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 提供直连字符串)。
17.8 邮件/通知服务
Section titled “17.8 邮件/通知服务”定义:邮件和通知服务用于向用户发送系统消息,包括三种渠道:邮件(注册确认、密码重置、订单通知等事务性邮件,以及营销邮件)、短信(验证码、紧急通知)、推送通知(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 暂时不可用),应该怎么处理?让预订也失败吗?
点击查看解析
绝对不能让预订因为邮件发送失败而失败。邮件是”辅助功能”,预订是”核心业务”——不能让非核心功能的故障影响核心业务。
正确做法:
异步发送:预订成功后,把”发送确认邮件”作为一个异步任务放入消息队列(如 Kafka/Redis Queue),立即返回”预订成功”给用户。邮件服务异步消费队列并发送。
重试机制:邮件发送失败后自动重试,采用指数退避策略(1分钟后重试 → 5分钟后重试 → 30分钟后重试),最多重试 5 次。
备用渠道:如果邮件重试 5 次都失败了,通过短信或 App 推送发送简要的预订确认信息。
补发机制:提供”重新发送确认邮件”按钮,用户可以在预订详情页手动触发。
监控告警:邮件发送失败率超过阈值时告警,运维人员介入排查。
这体现了一个重要的系统设计原则:核心流程不应该依赖非核心服务。发送邮件、记录日志、发送统计事件等辅助操作都应该是异步的,不阻塞核心业务。
17.9 支付集成
Section titled “17.9 支付集成”定义:支付集成是将第三方支付服务接入应用系统的过程。主流支付平台: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 重复投递的原因:
- 网络问题:Stripe 发送了 Webhook,你的服务器收到并处理了,但返回 200 的响应在网络中丢失了。Stripe 认为投递失败,会重试。
- 超时:你的服务器处理 Webhook 耗时太长(比如发邮件很慢),Stripe 在等待超时后认为失败,重试。
- Stripe 内部重试机制:Stripe 对失败的 Webhook 会在接下来的几小时内多次重试(最多数十次)。
如果不做幂等处理:
- 重复扣减房间数:可用房间从 10 → 9 → 8,但实际只有一个预订。
- 重复发送确认邮件:用户收到多封相同的确认邮件。
- 财务对账异常:系统记录的收入可能与 Stripe 实际金额不匹配。
幂等处理的核心:用
payment_intent_id作为唯一标识,处理前先查询该支付是否已经处理过。如果是,直接返回成功,不执行任何业务逻辑。这也是为什么上面的代码中有if reservation.Status == "confirmed" { return }这个检查。
17.10 CDN 配置实践
Section titled “17.10 CDN 配置实践”定义: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 文件,怎么办?
点击查看解析
这就是著名的”缓存失效”问题。解决方案:
构建工具的内容哈希(预防手段,最佳实践): Vite/Webpack 构建时会自动给文件名加上内容哈希:
app.js→app.a1b2c3.js。文件内容变了,哈希就变了,文件名就变了,浏览器会请求新的 URL,不会命中旧缓存。HTML 文件(引用了新的 JS 文件名)设置不缓存或短缓存。CDN 缓存清除(紧急手段): 在 CI/CD 流水线中,部署完成后自动调用 CDN API 清除缓存(Purge All)。Cloudflare 全球节点清除缓存通常在 30 秒内完成。
Service Worker 更新(如果用了 PWA): Service Worker 可以拦截请求并返回缓存,即使 CDN 缓存清了,Service Worker 缓存也需要更新。需要在 Service Worker 中实现版本检查和自动更新逻辑。
最佳组合:内容哈希(预防) + CDN Purge(兜底) + 短 TTL 的 HTML(确保新文件名能被加载)。这样即使不手动清缓存,用户也会在 HTML 缓存过期后(通常几分钟)自动加载到新版本。
练习一:为 Hotel Reservation 搭建完整基础设施
Section titled “练习一:为 Hotel Reservation 搭建完整基础设施”为一个新项目(Hotel Reservation)设计从零到上线的完整基础设施架构,包含以下环节:
- 域名和 DNS:选择域名注册商,配置 DNS 记录
- 反向代理:选择 Nginx 或 Caddy,写出配置文件
- HTTPS:选择证书获取方式
- 对象存储:酒店图片存储方案(选择 S3/MinIO/R2)
- 数据库:PostgreSQL 部署方案(自托管 vs 云托管)
- 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 (对象存储)练习二:成本对比分析
Section titled “练习二:成本对比分析”同一个 Hotel Reservation 项目,对比两种方案的成本和开发时间:
方案 A:全自建
- VPS(Hetzner/DigitalOcean)+ Docker Compose
- PostgreSQL 自己跑
- Nginx + Certbot
- MinIO 自托管
方案 B:全托管
- Supabase(数据库 + 认证 + 实时)
- Vercel(前端 + Serverless API)
- Cloudflare R2(对象存储)
- Resend(邮件)
对比维度:
- 月度成本(100 用户 / 1000 用户 / 10000 用户)
- 开发时间(从零到上线)
- 运维复杂度
- 可扩展性
- 迁移风险