07. 规模化架构:LB、CDN、K8s、Kafka、Nacos
读完这篇你能:
- 看懂从 1 万日活到百万日活的架构演进
- 知道负载均衡、CDN、读写分离、分片分别解决什么问题
- 看懂 K8s 的核心概念(Pod、Deployment、Service)
- 知道 ZooKeeper / Nacos / Kafka / gRPC 是干嘛的、什么时候用
对应架构层:接入层 + 协调层 + 部署层(进阶)
前置知识:01-06 篇
预计用时:3 小时(主要理解,操作可选)
0. 为什么算法工程师要学这个
你的应用在 Cloud Run 跑得好好的,日活 1000。突然老板说:
"我们下周要在超级碗投广告,预计带来 50 万用户。"
你看了一眼 Cloud Run 配置:max-instances=10。每实例 80 并发,总共 800 并发——50 万用户同时来,会挂。
怎么扩?直接调 max-instances=100?成本炸了。怎么办?
更要命的问题:
- 数据库一台机器扛不住 50 万用户读,怎么分担?
- 单个 Cloud Run 区域在亚洲,美国用户延迟 300ms+,怎么加速?
- LLM 调用慢,队列堆积怎么处理?
- 几百台机器,怎么管理?怎么发现彼此?
这一篇回答这些问题。注意:这是"理解型"教程,不是"动手型"——这些技术都需要团队 / 运维支持,算法工程师主要是看懂 + 能讨论,不是亲手搭。
1. 架构演进:从 1 到百万日活
1.1 阶段 1:单机(< 1000 日活)
用户 → Cloud Run(1 个实例)→ Postgres
↓
Redis / S3
- 一个 Docker 容器,Cloud Run 自动扩缩
- 一个 Postgres 实例(2 CPU, 4GB)
- 一个 Redis 实例
- 月成本:$50-200
瓶颈:单个 Cloud Run 实例的并发上限。
1.2 阶段 2:小规模扩展(1k - 1 万日活)
用户 → Cloud Run(多实例,自动扩)→ 主 Postgres(读写都它)
↑ ↓
CDN Redis(缓存)
- Cloud Run 多实例 + 自动扩缩
- Postgres 升级到 4 CPU 16GB
- 加 CDN 加速静态资源
- 月成本:$200-2000
瓶颈:
- Postgres 写入压力
- LLM 调用排队(单个用户等很久)
1.3 阶段 3:读写分离 + 缓存(1万 - 10 万日活)
用户 → LB → 多个 App 实例 → Postgres 主(写)
↘ Postgres 从(读)× 2
↘ Redis 集群
↘ Kafka(异步任务)
↓
消费者(后台 worker)
- 加 LB(负载均衡,Cloud Run 自带,自建需 Nginx / ALB)
- Postgres 读写分离:1 主 2 从
- Redis 集群(分片)
- Kafka / RabbitMQ 做异步任务(LLM 调用、批量处理)
- 多个 worker 消费异步任务
- 月成本:$2000-20000
瓶颈:
- 单库写入(写不能像读那样加从库)
- 协调问题(服务发现、配置同步)
1.4 阶段 4:微服务 + K8s(10 万 - 100 万日活)
用户 → CDN → Global LB → 多区域 K8s 集群
├── 用户服务(微服务)
├── LLM 服务(微服务)
├── 计费服务
└── 通知服务
每个微服务:
→ 自己的 DB(分库)
→ 通过 gRPC / 消息队列通信
→ 通过 Nacos / etcd 注册发现
- K8s 管几百个容器
- 服务网格(Istio)管服务间通信
- 数据库分片(按 user_id hash 切到不同库)
- 多区域部署(美东、亚太、欧洲)
- 月成本:$20000-200000+
瓶颈:
- 复杂度爆炸
- 团队 / 组织问题(康威定律)
1.5 经验法则:不要过早优化
日活 1k → Cloud Run + 单 Postgres 够了
日活 1万 → 加 Redis 缓存、CDN
日活 10万 → 加读写分离、异步任务
日活 100万 → 才考虑 K8s、分片、多区域
90% 的 LLM 应用到不了 100 万日活。如果你是创业公司,先把 1000 日活跑通,再谈扩展。
2. 接入层进阶
2.1 负载均衡(Load Balancing)
作用:把进入的流量分发到多台后端机器。
算法:
| 算法 | 怎么工作 | 用途 |
|---|---|---|
| 轮询(Round Robin) | A→B→C→A→B→C | 默认,机器配置相同 |
| 最少连接(Least Connections) | 找当前连接最少的 | 长连接、LLM 应用 |
| IP Hash | 同 IP 固定去某机器 | 需要 session sticky |
| 加权(Weighted) | 按 CPU/内存权重 | 机器配置不同 |
LLM 应用用最少连接——因为每个请求时长差异大(LLM 调用 1-30 秒),轮询会让某些机器被慢请求卡满。
主流 LB:
| 工具 | 特点 |
|---|---|
| Nginx | 老牌,配置式,反向代理 + LB |
| HAProxy | 专业 LB,性能强 |
| Cloud LB(AWS ALB、GCP LB) | 托管,全球分布 |
| Cloud Run(自带) | 你不用管,Cloud Run 自动 LB |
| Envoy / Istio | 服务网格的 LB,微服务间 |
Nginx 配置例子:
upstream llm_backend {
least_conn; # 最少连接算法
server app1:8000 weight=3;
server app2:8000 weight=2;
server app3:8000 weight=1;
keepalive 32; # 保持长连接
}
server {
listen 443 ssl http2;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
location /api/ {
proxy_pass http://llm_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off; # ✅ SSE 流式必须关
}
}
2.2 CDN(内容分发网络)
作用:把静态资源(JS、CSS、图片、视频)缓存在全球各地的节点,用户就近访问。
没有 CDN:
澳大利亚用户 → 美国服务器(200ms 延迟)→ 加载 5 秒
有 CDN:
澳大利亚用户 → 悉尼 CDN 节点(5ms 延迟)→ 加载 1 秒
主流 CDN:
| 服务 | 特点 |
|---|---|
| Cloudflare | 免费,边缘计算,防护强 |
| AWS CloudFront | 深度集成 S3 |
| Akamai | 老牌,企业级 |
| 阿里云 CDN / 腾讯云 CDN | 国内 |
LLM 应用怎么用 CDN:
- 前端静态资源(HTML/CSS/JS)→ 必须走 CDN
- API 响应 → 不缓存(动态),但可以用 CDN 做边缘路由
- 文件下载(S3 上的 PDF)→ 通过 CDN 加速
Cloudflare 配置:
- 域名 DNS 切到 Cloudflare
- 开启 "Proxies"(橙色云朵)
- 配 Page Rules:
/static/*全缓存,/api/*不缓存
2.3 DNS 和全球路由
DNS:把 example.com 翻译成 IP。
智能 DNS(Route53、Cloudflare)能根据用户地理位置返回不同 IP:
美国用户查 example.com → 1.2.3.4(美东机房)
亚洲用户查 example.com → 5.6.7.8(亚太机房)
实现"地理就近接入"。
Anycast:同一个 IP 在全球多个机房宣告,BGP 自动路由到最近的。Cloudflare、Cloud LB 都用这个技术。
3. 数据层进阶
3.1 读写分离
原理:
- 所有写操作 → 主库(Master / Primary)
- 所有读操作 → 从库(Replica / Standby)
- 主库的修改异步同步到从库
App → [写] → Master Postgres ─── 同步 ───→ Replica 1
└── 同步 ──→ Replica 2
App → [读] → LB → Replica 1 / Replica 2
代码:
from sqlalchemy import create_engine
write_engine = create_engine("postgresql://user:pass@master:5432/db")
read_engine = create_engine("postgresql://user:pass@replica-lb:5432/db")
# 写
with Session(write_engine) as s:
s.add(user)
s.commit()
# 读
with Session(read_engine) as s:
users = s.exec(select(User)).all()
注意:
- 复制延迟:主库写后,从库可能晚几十毫秒到几秒才同步
- 写完立刻读,可能读到旧数据——用"读主库"或"读写绑定同一会话"
3.2 分片(Sharding)
当读写分离不够:单库扛不住写入(每秒 10 万写),或单表行数到亿级。
分片:把数据水平切到多个库,每个库只存一部分。
用户 ID 1-100万 → DB Shard 1
用户 ID 100万-200万 → DB Shard 2
用户 ID 200万-300万 → DB Shard 3
分片键(Shard Key)选择很关键:
| 策略 | 例子 | 优缺点 |
|---|---|---|
| 按用户 ID hash | shard = hash(user_id) % N | 均匀,但跨片查询难 |
| 按时间 | 2026 年数据在 A,2025 年在 B | 时序数据好用,但热点(最新片) |
| 按地域 | 美国用户在美东库,亚洲用户在亚太库 | 就近访问,但跨区查询慢 |
| 按业务 | 订单库、用户库分 | 业务边界清晰 |
警告:分片是不可逆的复杂化。一旦分了,JOIN、事务都变难。不到万不得已不要分片。
3.3 冷热数据隔离
热数据(近 30 天,频繁访问)→ Redis + Postgres SSD
温数据(近 1 年,偶尔访问)→ Postgres HDD
冷数据(1 年前,几乎不访问)→ S3 Glacier
Postgres 表分区:
CREATE TABLE queries (
id BIGSERIAL,
user_id INT,
created_at TIMESTAMP,
...
) PARTITION BY RANGE (created_at);
CREATE TABLE queries_2026_06 PARTITION OF queries
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
CREATE TABLE queries_2026_05 PARTITION OF queries
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
查询时 Postgres 自动只扫相关分区。
3.4 缓存集群
单 Redis 扛不住时:
Redis Cluster(官方分片):
- 数据自动分片到多个节点
- 主从复制,自动 failover
- 客户端用
redis-py集群模式
Redis 主从 + Sentinel:
- 一主多从,读分摊
- Sentinel 监控主,挂了自动选新主
from redis.cluster import RedisCluster
client = RedisCluster(host="redis-cluster", port=6379)
4. 协调层:服务发现和配置中心
4.1 为什么需要协调层
单机时代:函数调用,代码里写死 localhost:8000。
多机时代:
- 服务 A 要调服务 B,但 B 有 10 个实例,调哪个?
- B 的实例 IP 会变(扩容、重启、迁移)
- 配置改了(限流从 10/min 调成 100/min),不想重启
协调层 解决这些:
| 问题 | 解决方案 | 技术 |
|---|---|---|
| 服务发现 | 服务注册 + 健康检查 + 客户端发现 | Nacos、Consul、etcd、ZooKeeper |
| 动态配置 | 配置中心,改配置不重启 | Nacos、Apollo |
| 异步通信 | 消息队列,解耦 | Kafka、RabbitMQ、RocketMQ |
| 分布式锁 | 多实例操作同一资源防冲突 | Redis RedLock、ZooKeeper |
4.2 ZooKeeper / etcd / Consul / Nacos 对比
| 技术 | 来源 | 特点 | 主用场景 |
|---|---|---|---|
| ZooKeeper | Yahoo, 2008 | 老牌,复杂,Java | Kafka 早期依赖,Hadoop 生态 |
| etcd | CoreOS, 2013 | 简单,Go,强一致(Raft) | K8s 底层用 |
| Consul | HashiCorp, 2014 | 服务发现 + 健康 + KV | 微服务架构 |
| Nacos | 阿里, 2018 | 注册中心 + 配置中心一体 | 国内微服务标配 |
简单理解:
- K8s 自带 etcd,你不直接用,但要知道 K8s 就靠它做服务发现
- 自建微服务(Java/Dubbo 体系)用 Nacos 或 Consul
- ZooKeeper 在新项目里用得越来越少,但老系统(Kafka、Hadoop)还在用
4.3 Nacos 工作原理(代表)
注册中心:
服务启动 → 向 Nacos 注册(IP + 端口 + 健康检查 URL)
服务调用方 → 从 Nacos 拉服务列表(本地缓存)
健康检查失败 → Nacos 摘除该实例 → 通知调用方更新
配置中心:
配置存 Nacos → 服务启动时拉取 + 长轮询监听变更
配置改了 → Nacos 推送到所有订阅的服务 → 服务热更新(不重启)
Python 接入 Nacos(虽然 Python 用 Nacos 不多,Java/Dubbo 主流):
import nacos
client = nacos.NacosClient("nacos-server:8848", namespace="prod")
# 拉配置
config = client.get_config("my-app", "DEFAULT_GROUP")
# 监听配置变更
def on_change(args):
print("Config updated:", args["content"])
client.add_config_watcher("my-app", "DEFAULT_GROUP", on_change)
LLM 应用真的需要 Nacos 吗?
大多数情况:不需要。Cloud Run / K8s 自带服务发现,配置用环境变量 + Secret Manager。Nacos 是 Java 微服务生态的产物,Python 圈用得少。
你只需要知道:如果有人跟你说"我们用 Nacos",你知道它在协调层,做注册 + 配置。
4.4 Kafka:高吞吐消息队列
作用:
- 生产者把消息写到 topic
- 消费者订阅 topic 异步处理
- 解耦 + 削峰
典型用法:
用户行为日志 → Kafka → 实时分析(Flink)
↘ → 数据仓库(Snowflake)
↘ → 监控告警
LLM 调用请求 → Kafka → Worker 池消费 → 调 LLM → 结果回写
Kafka 核心概念:
| 概念 | 类比 |
|---|---|
| Topic | 队列名(分类) |
| Partition | 一个 topic 切成多个,并行 |
| Producer | 写消息的 |
| Consumer | 读消息的 |
| Consumer Group | 一组消费者平分 partition |
| Offset | 消费位置(类似文件指针) |
vs RabbitMQ:
| 维度 | Kafka | RabbitMQ |
|---|---|---|
| 吞吐 | 极高(百万/秒) | 中(万/秒) |
| 延迟 | 毫秒 | 毫秒 |
| 持久化 | 持久 + 可回放 | 持久但消费后删 |
| 模型 | 日志流 | 队列 |
| 用途 | 大数据、事件流 | 任务队列、RPC |
LLM 应用:大多数用 Redis Streams 或 RabbitMQ 就够了,Kafka 是大数据/日志场景才需要。
4.5 gRPC:高性能 RPC
REST + JSON 简单但慢(JSON 序列化开销大)。gRPC 用 Protobuf(二进制),速度快 10 倍。
// hello.proto
syntax = "proto3";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
# 服务端
class Greeter(greeter_pb2_grpc.GreeterServicer):
def SayHello(self, request, context):
return greeter_pb2.HelloReply(message=f"Hello {request.name}")
server = grpc.aio.server()
greeter_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.add_insecure_port("[::]:50051")
await server.start()
# 客户端
async with grpc.aio.insecure_channel("localhost:50051") as channel:
stub = greeter_pb2_grpc.GreeterStub(channel)
resp = await stub.SayHello(greeter_pb2.HelloRequest(name="sanbu"))
什么时候用 gRPC:
- 内部服务间调用(对性能敏感)
- 跨语言(Java + Python + Go 都能调)
- 流式通信(gRPC 双向流)
对外 API 还是 REST——因为浏览器、curl 对 gRPC 支持差。
5. K8s 基础(看懂 yaml)
5.1 为什么需要 K8s
Cloud Run / Fargate 简单,但:
- 锁定云厂商
- 灵活度有限(网络、存储、调度策略)
- 大规模(几百个服务)成本高
Kubernetes 是"容器的操作系统"——管几百个容器的部署、扩缩容、网络、存储、故障恢复。
5.2 K8s 核心概念
| 概念 | 是什么 | 类比 |
|---|---|---|
| Pod | 最小调度单位(1+ 容器) | 一个进程组 |
| Deployment | 管 Pod 的副本数 + 滚动更新 | systemd |
| Service | 一组 Pod 的稳定网络入口 | 内部 DNS + LB |
| Ingress | 外部流量入口 | Nginx / Cloud LB |
| ConfigMap | 配置 | /etc/config |
| Secret | 密钥 | 加密的 ConfigMap |
| Volume | 持久化 | 磁盘挂载 |
| Namespace | 资源隔离 | 文件夹 |
5.3 一个完整的 Deployment YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-llm-app
labels:
app: my-llm-app
spec:
replicas: 3 # 跑 3 个副本
selector:
matchLabels:
app: my-llm-app
template:
metadata:
labels:
app: my-llm-app
spec:
containers:
- name: app
image: my-registry/my-app:v1.2.3
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
resources:
requests: # 保底资源
cpu: "500m"
memory: "512Mi"
limits: # 上限
cpu: "1000m"
memory: "1Gi"
readinessProbe: # 就绪探针(能接流量了吗)
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe: # 存活探针(要不要重启)
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: my-llm-app
spec:
selector:
app: my-llm-app
ports:
- port: 80
targetPort: 8000
type: ClusterIP # 集群内访问
部署:
kubectl apply -f deployment.yaml
kubectl get pods
kubectl logs my-llm-app-xxx
kubectl exec -it my-llm-app-xxx -- bash
kubectl scale deployment my-llm-app --replicas=5
5.4 自动扩缩(HPA)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-llm-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-llm-app
minReplicas: 3
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
CPU 超 70% → 自动扩;空闲 → 自动缩。
LLM 应用特殊:CPU 不是好指标(LLM 调用等 IO,CPU 不高但应用很忙)。用自定义指标(请求队列长度、并发数)。
5.5 Helm:K8s 包管理
手写 YAML 太繁琐。Helm 是 K8s 的"pip / npm":
helm install my-release bitnami/postgresql
helm install my-release bitnami/redis
社区有几千个现成的 chart。
5.6 微服务 vs 单体
单体(Monolithic):
- 一个代码库,一个进程
- 简单,迭代快
- 适合 < 50 万日活
微服务(Microservices):
- 拆成多个独立服务,独立部署
- 团队并行迭代
- 但复杂度爆炸(网络、事务、调试)
康威定律(Conway's Law):软件架构会映射组织架构。10 个人的团队硬上微服务 = 灾难。
经验:
- 0-50 万日活:单体(或者 Cloud Run 几个独立服务)
- 50 万-500 万日活:有限度的微服务(按业务域拆 3-5 个)
- 500 万+:微服务
6. 成本估算
百万日活 LLM 应用大致成本(月):
| 组件 | 配置 | 月成本 |
|---|---|---|
| 应用集群 | 20 台 4C8G | $2,000 |
| Postgres(主+2从) | 16C 64GB | $1,500 |
| Redis 集群 | 6 节点 | $500 |
| 对象存储 S3 | 5TB | $150 |
| CDN | 10TB 流量 | $500 |
| LLM API(OpenAI) | 100M tokens/月 | $3,000-15,000 |
| 可观测(Grafana Cloud) | Pro | $300 |
| K8s 集群(自管) | 3 master | $1,000 |
| 合计 | $9,000-21,000/月 |
LLM API 是最大头。控制成本的关键在 09. LLM 工程 讲的成本治理。
7. 常见踩坑
坑 1:过早微服务化
10 人团队硬上微服务 → 服务间通信、调试、事务都变难 → 效率反而下降。
先单体,真有痛点再拆。
坑 2:无脑加 Redis 缓存
数据本来强一致要求高,加缓存后出现脏读 → 用户看到旧余额 → 投诉。
不是所有数据都适合缓存。
坑 3:K8s 上 Single-Point Postgres
K8s 里跑 Postgres,挂了没自动备份 → 数据全丢。
数据库用托管服务(Cloud SQL、RDS),不要自管。
坑 4:读写分离后立即读
写完主库,立刻读从库,但同步还没到 → 读到旧数据。
解决:
- 写后强制读主
- 用"读己写"一致性(SQLAlchemy 的 session 复用)
坑 5:gRPC 对外暴露
浏览器调不了 gRPC(需要 grpc-web 转换)。
对外 API 用 REST/gateway,内部用 gRPC。
坑 6:Kafka 当数据库用
消费后消息还在 Kafka,觉得"反正有日志"。某天磁盘满了,Kafka 挂了,数据丢。
Kafka 是消息队列不是数据库。消费完该入库的还是要入库。
8. 检查清单
- 我能解释负载均衡的轮询、最少连接、IP hash 算法
- 我知道 CDN 解决什么问题,LLM 应用怎么用
- 我能解释读写分离、分片、冷热隔离
- 我知道为什么不要过早分片
- 我能解释 ZooKeeper / etcd / Nacos / Consul 是干嘛的
- 我知道 Kafka 和 RabbitMQ 的区别
- 我知道 gRPC 和 REST 的区别,各自什么时候用
- 我能看懂 K8s 的 Deployment + Service YAML
- 我知道 HPA 自动扩缩容的原理
- 我能画一张 100 万日活的 LLM 应用架构图
- 我知道单体和微服务的边界(康威定律)
9. 下一步
架构理解到位了!下一站可靠性:
- 08. 可靠性与护栏:Guardrails、人审、回退
让系统在 LLM 乱说、模型故障、突发流量下,还能稳定运行。
进阶学习资源
- 《Designing Data-Intensive Applications》:Martin Kleppmann(本系列理论基础)
- 《System Design Interview》:Alex Xu,图多易读
- ByteByteGo Newsletter:https://blog.bytebytego.com/
- K8s 官方教程:https://kubernetes.io/docs/tutorials/
- Google SRE Book(免费):https://sre.google/sre-book/table-of-contents/
- 高并发架构案例:https://github.com/donnemartin/system-design-primer