Files
maths-cs-ai-compendium-zh/chapter 18: ML systems design/01. systems design fundamentals.md
T
flykhan 2536c937e3 feat: 完整中文翻译 maths-cs-ai-compendium(数学·计算机科学·AI 知识大全)
翻译自英文原版 maths-cs-ai-compendium,共 20 章全部完成。

第01章 向量 | 第02章 矩阵 | 第03章 微积分
第04章 统计学 | 第05章 概率论 | 第06章 机器学习
第07章 计算语言学 | 第08章 计算机视觉 | 第09章 音频与语音
第10章 多模态学习 | 第11章 自主系统 | 第12章 图神经网络
第13章 计算与操作系统 | 第14章 数据结构与算法
第15章 生产级软件工程 | 第16章 SIMD与GPU编程
第17章 AI推理 | 第18章 ML系统设计
第19章 应用人工智能 | 第20章 前沿人工智能

翻译说明:
- 所有数学公式 $...$ / $$...$$、代码块、图片引用完整保留
- mkdocs.yml 配置中文导航 + language: zh
- README.md 已翻译为中文(兼 docs/index.md)
- docs/ 目录包含指向各章文件的 symlink
- 约 29,000 行中文内容,排除 .cache/ 构建缓存
2026-05-03 10:23:20 +08:00

177 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 系统设计基础
*系统设计是构建可在大规模下可靠运行的软件的方法。本文件涵盖客户端-服务器架构、网络协议、DNS、代理、负载均衡、缓存、数据库、消息队列、一致性模型和弹性模式*
- 生产环境中的每一个ML系统都是一个分布式系统。推荐引擎不仅仅是模型——它是一个API服务器、一个特征存储、一个模型注册表、一个缓存层、一个消息队列和一个监控栈,所有这些通过网络进行通信。理解系统设计是区分"我训练了一个模型"和"我构建了一个产品"的关键。
- 顶级科技公司(Google、Meta、Amazon、OpenAI)的系统设计面试会测试你是否能设计这些系统。本章为你提供基础构建模块(本文件)、云基础设施(文件02)、扩展模式(文件03)、ML特定设计(文件04)和实操示例(文件05)。
## 客户端-服务器架构
- 基本模式:**客户端**发送请求,**服务器**处理请求并返回响应。你的浏览器(客户端)向google.com(服务器)发送HTTP请求,服务器返回HTML。
- **请求-响应模式**:同步。客户端等待响应。简单但会产生瓶颈:客户端在等待时空闲,服务器必须在处理完当前请求后才能继续。
- **无状态服务器**:服务器不记住先前的请求。每个请求包含处理它所需的所有信息。这使得扩展变得容易:任何服务器都可以处理任何请求,因此你可以在负载均衡器后面添加更多服务器。
- **有状态服务器**:服务器在请求之间维护状态(例如,用户会话)。扩展更困难,因为来自同一用户的请求必须发送到同一台服务器(会话亲和性)。现代系统通过将状态存储在数据库或缓存(Redis)中来避免服务器端状态。
## 网络协议
- 我们在第13章(TCP/IP层、套接字)中介绍了网络知识。这里我们关注系统设计中使用的应用层协议:
- **HTTP/HTTPS**Web和大多数API的协议。请求方法:GET(读取)、POST(创建/预测)、PUT(更新)、DELETE(删除)。HTTPS增加了TLS加密(第13章安全部分)。REST API(第15章文件03)基于HTTP构建。
- **WebSocket**:客户端和服务器之间的持久双向连接。与HTTP(请求→响应→连接关闭)不同,WebSocket保持连接打开,用于实时流式传输。用于:LLM令牌流式传输(生成时发送令牌)、实时仪表盘、聊天应用。
- **gRPC**Google的RPC框架。使用Protocol Buffers(二进制序列化,比JSON小约10倍且更快),基于HTTP/2。支持流式传输(服务端、客户端、双向)。用于注重性能的内部服务间通信。Triton推理服务器(第15章)和TensorFlow Serving使用gRPC。
- **Protocol Buffers**:在`.proto`文件中定义消息模式:
```protobuf
message PredictRequest {
repeated float features = 1;
string model_version = 2;
}
message PredictResponse {
float prediction = 1;
float confidence = 2;
}
service ModelService {
rpc Predict(PredictRequest) returns (PredictResponse);
}
```
- 该模式被编译成任何语言(Python、C++、Go、Java)的客户端和服务端代码。类型安全、向后兼容性和高性能都自然具备。
## DNS
- **DNS**(域名系统)将人类可读的名称转换为IP地址(第13章)。对于系统设计,DNS还提供:
- **通过DNS的负载均衡**:为同一域名返回不同的IP地址,将流量分布到多个服务器。简单但粒度较粗(DNS结果会被缓存数分钟到数小时,因此流量不会快速重新平衡)。
- **地理路由**:根据客户端位置返回最近数据中心的IP。东京的用户获得日本数据中心;伦敦的用户获得欧洲数据中心。
- **故障转移**:如果服务器宕机,DNS停止返回其IP。新客户端连接到健康的服务器。但缓存的DNS条目意味着某些客户端会继续访问已宕机的服务器持续数分钟(TTL问题)。
## 代理
- **代理**是客户端和服务器之间的中介:
- **反向代理**(在服务器前面):客户端连接到代理,代理将请求转发给后端服务器。客户端不知道哪个服务器处理了请求。**Nginx**和**HAProxy**是标准的反向代理。它们提供:负载均衡(分发请求)、SSL终止(在代理处解密HTTPS,向后端发送明文HTTP)、缓存、速率限制和压缩。
- **API网关**:一种专门用于API的反向代理。处理身份验证、速率限制、请求路由(不同路径→不同服务)和API版本管理。**Kong**、**AWS API Gateway**和**Envoy**是常见选择。
- 对于ML服务:API网关位于模型服务器前面。它验证API密钥、对免费用户进行速率限制、将`/v1/predict`路由到模型服务器A、将`/v2/predict`路由到模型服务器B,并收集使用指标。
## 负载均衡
- 当你拥有多台服务器时,**负载均衡器**将传入请求分布到它们之间。
![负载均衡器将传入请求分布到多个后端服务器](../images/load_balancer.svg)
- **算法**
- **轮询**:按顺序发送请求到服务器(1, 2, 3, 1, 2, 3...)。简单、公平,但不考虑服务器负载。
- **最少连接**:发送到活动连接最少的服务器。适用于处理时间可变的请求(有些LLM请求生成10个令牌,有些生成1000个)。
- **加权轮询**:容量更大的服务器获得更多请求。拥有80 GB GPU内存的服务器处理的请求量是40 GB的两倍。
- **一致性哈希**:对请求键进行哈希运算,映射到特定服务器。相同的键始终发送到相同的服务器。适用于:缓存(同一用户的请求命中同一缓存)、会话亲和性和前缀缓存(第17章:具有相同系统提示词的请求发送到具有该提示词KV缓存的服务器)。
- **L4 vs L7负载均衡**
- **L4**(传输层):基于IP和端口路由。快速但无法检查请求内容。
- **L7**(应用层):基于HTTP路径、标头或正文内容路由。可以将`/api/chat`路由到聊天服务器,将`/api/embed`路由到嵌入服务器。较慢但更灵活。
## 缓存
- **缓存**将频繁访问的数据存储在快速存储层(内存)中,以避免重新计算或重新获取。
![缓存旁路模式:先检查缓存,未命中时从数据库获取并存储到缓存中以备下次使用](../images/cache_aside_pattern.svg)
- **缓存模式**
- **缓存旁路**(惰性加载):应用程序先检查缓存。未命中时,从数据库获取、存储到缓存并返回。最常见的模式。
- **直写**:每次写入同时写入缓存和数据库。确保缓存始终是最新的,但会减慢写入速度。
- **回写**:写入只进入缓存;缓存异步刷新到数据库。写入最快,但若缓存刷新前崩溃则有数据丢失风险。
- **驱逐策略**(当缓存满时):
- **LRU**(最近最少使用):驱逐最长时间未被访问的条目。最常见的策略。
- **LFU**(最不频繁使用):驱逐访问次数最少的条目。当某些条目持续受欢迎时效果更好。
- **TTL**(生存时间):条目在固定时长后过期。用于会过时的数据(模型预测缓存5分钟,特征值缓存1小时)。
- **CDN**(内容分发网络):用于静态内容(图片、JavaScript、CSS)的全球分布式缓存。遍布100多个地点的服务器从离用户最近的位置提供缓存内容。对于ML:模型权重可以缓存在CDN上以实现快速下载。
- **Redis**:标准的键值内存缓存/数据库。支持字符串、列表、集合、有序集合、哈希和流。亚毫秒级延迟。用于:缓存模型预测、存储会话数据、速率限制(统计每个用户每分钟的请求数)和实时特征服务。
- 对于ML服务:缓存重复输入的预测结果。如果很多用户问"法国的首都是什么?",计算一次答案然后提供缓存结果。对于聊天机器人工作负载,缓存命中率通常为20-40%,按比例降低GPU成本。
## 数据库
### SQL(关系型)
- **SQL数据库**PostgreSQL、MySQL)以包含行和列的形式存储数据。表之间的关系通过外键表示。查询使用SQL。**ACID**保证:
- **原子性**:事务要么完全完成,要么完全回滚。没有部分更新。
- **一致性**:数据库从一个有效状态转换到另一个有效状态。约束条件(唯一键、外键)始终得到满足。
- **隔离性**:并发事务不互相干扰。
- **持久性**:已提交的数据在崩溃后仍然存在(在确认前写入磁盘)。
- SQL数据库擅长:具有关系的有结构数据、复杂查询(联接、聚合)、严格的一致性要求和数据完整性。
### NoSQL
- **NoSQL数据库**为了可扩展性和灵活性而牺牲了一些ACID保证:
- **键值存储**Redis、DynamoDB):最简单的模型。按键快速查找。用于缓存、会话存储和特征存储。
- **文档存储**MongoDB、Firestore):存储类似JSON的文档。灵活的模式(每个文档可以有不同字段)。用于用户资料、产品目录和配置。
- **列族存储**Cassandra、HBase):针对写入密集型工作负载和时间序列数据进行了优化。用于事件日志、指标和分析。
- **图数据库**(Neo4j):存储节点和边。针对遍历查询进行了优化。用于社交网络、知识图谱和推荐系统。
- **向量数据库**Pinecone、Milvus、Weaviate、FAISS):存储高维嵌入并支持近似最近邻(ANN)搜索。对于语义搜索、RAG(检索增强生成)和推荐系统至关重要。
### CAP定理
- 在分布式数据库中,最多只能满足三个属性中的两个:
- **一致性**:每次读取都返回最新的写入。
- **可用性**:每个请求都会收到响应(即使某些节点宕机)。
- **分区容忍性**:系统在网络分区(节点无法通信)时仍能继续运行。
![CAP定理:由于网络分区不可避免,需要在CP(一致)或AP(可用)之间选择](../images/cap_theorem.svg)
- 由于分布式系统中网络分区不可避免,真正的选择是**CP**(一致但在分区期间可能不可用——如PostgreSQL vs **AP**(可用但在分区期间可能返回过期数据——如Cassandra、DynamoDB)。
- 对于ML:特征存储通常选择AP(稍微过期的特征值也比无法预测要好)。模型注册表选择CP(提供错误的模型版本是灾难性的)。
### 分片
- **分片**将数据库拆分到多台机器上。每个分片持有数据的一个子集。
- **哈希分片**:对键进行哈希运算以确定分片。`shard = hash(user_id) % num_shards`。分布均匀但不支持范围查询。
- **范围分片**:每个分片持有一个键范围(用户A-G在分片1,H-N在分片2)。支持范围查询但可能产生热点(如果很多用户名字以"S"开头)。
- **重新分片问题**:添加分片会使哈希映射失效。**一致性哈希**最小化数据移动:添加第n个分片时,只有约1/n的键需要移动。
### 数据库索引
- **索引**是一种加速查询的数据结构,代价是额外的存储空间和较慢的写入速度。没有索引时,查询会扫描每一行($O(n)$)。有索引时,可以在$O(\log n)$时间内找到目标。
- **B树索引**(默认):一种平衡树(第13章、第14章),其中每个节点包含多个键和指针。B树对缓存友好(宽节点适合缓存行)并支持范围查询(`WHERE age BETWEEN 20 AND 30`)。大多数SQL数据库使用B树。
- **哈希索引**:使用哈希函数将键映射到行位置。$O(1)$查找但不支持范围查询。用于精确匹配查找(`WHERE id = 12345`)。
- **复合索引**:对多个列的索引。`CREATE INDEX ON users(country, city)` 加速按国家或按国家+城市筛选的查询,但不能加速仅按城市的查询(最左边的列必须在查询中)。
- **权衡**:每个索引都会加速读取但减慢写入(每次插入/更新/删除都必须更新索引)并占用存储空间(每个索引约占表大小的10-30%)。不要索引所有内容——只索引你经常查询的列。
- **对于ML系统**:特征存储的在线数据库需要在实体键(user_id、item_id)上建立索引以实现快速特征查找。实验跟踪数据库需要在(experiment_id、metric_name)上建立索引以实现仪表盘查询。
### API设计
- 系统通过API进行通信。良好的API设计使系统可用、可进化和可调试:
- **REST约定**:使用名词表示资源(`/users``/models`),HTTP方法表示操作(GET=读取、POST=创建、PUT=更新、DELETE=删除),状态码表示结果(200=OK、201=已创建、400=错误请求、404=未找到、429=被限流、500=服务器错误)。
- **分页**:对于返回列表的端点,永远不要一次返回所有结果。使用基于游标的分页(`GET /items?cursor=abc&limit=50`)或基于偏移量的分页(`GET /items?offset=100&limit=50`)。对于大数据集,基于游标的分页更高效(基于偏移量的分页需要跳过行)。
- **版本管理**:在API路径前加上版本前缀(`/v1/predict``/v2/predict`)。这样可以在不破坏现有客户端的情况下演进API。客户端按照自己的节奏迁移到v2;v1被弃用但在流量下降之前不会删除。
- **错误响应**:返回结构化的错误信息:
```json
{
"error": {
"code": "INVALID_INPUT",
"message": "特征'user_age'必须为正整数",
"details": {"field": "user_age", "value": -5}
}
}
```
## 消息队列
- **消息队列**将生产者(生成工作的服务)与消费者(处理工作的服务)解耦。生产者将消息发送到队列;消费者在就绪时拉取消息。
- **队列为什么重要**:没有队列时,如果消费者慢或宕机,生产者会被阻塞。有了队列,生产者发送后就无需等待;队列缓冲消息,直到消费者准备好。
- **Apache Kafka**:一个分布式、持久化、高吞吐量的消息队列。消息存储在**主题**中,每个主题跨多个代理分区。消费者从分区读取,跟踪其位置(**偏移量**)。Kafka保证分区内的顺序,并可重播消息(日志是持久化的)。
- **发布/订阅**:发布者将消息发送到主题;该主题的所有订阅者都会收到一份副本。用于事件驱动架构:"新模型已部署"触发监控服务、A/B测试服务和日志服务同时响应。
- 对于ML:预测请求通过HTTP到达,放入Kafka队列,由GPU工作线程处理,结果通过回调或WebSocket返回。队列缓冲突发的流量,并确保即使GPU工作线程崩溃也不会丢失请求。
## 一致性模型
- 在分布式系统中,不同节点可能对数据有不同的视图。**一致性模型**定义了系统提供的保证:
- **强一致性**:写操作之后,所有后续读取(从任何节点)都能看到新值。易于推理但速度慢(需要在节点之间协调)。
- **最终一致性**:写操作之后,读取可能在某段时间内看到过期数据,但最终会看到新值。速度快(无需协调)但需要应用程序处理过期读取。
- **因果一致性**:如果操作A因果上先于B(例如,"写入X然后读取X"),系统保证B能看到A的结果。但不相关的操作可能以任何顺序被看到。
- **读写一致性**:用户始终能立即看到自己的写入,即使其他用户看到的是过期数据。大多数应用程序所需的最小一致性。
## 弹性模式
- **速率限制**:限制每个用户在时间窗口内的请求数。防止滥用并确保公平访问。使用Redis中的令牌桶或滑动窗口计数器实现。
- **断路器**:如果下游服务开始失败(错误率超过阈值),断路器"断开"并停止向其发送请求(立即返回回退响应)。超时后,"半开"并发送测试请求。如果测试成功,则"闭合"(恢复正常操作)。这防止了级联故障:如果特征存储宕机,模型服务器返回无特征的预测,而不是每次请求都超时。
- **背压**:当系统过载时,它向上游发出信号要求减速。与其接受请求然后失败,不如尽早拒绝多余的请求(返回429或503状态码)。客户端以指数退避重试。
- **指数退避重试**:如果请求失败,等待1秒后重试。如果再次失败,等待2秒。然后是4秒、8秒,依此类推。加入随机抖动以防止所有客户端同时重试(惊群问题)。
- **幂等性**:如果执行两次的效果与执行一次相同,则该操作是幂等的。`PUT /user/123 {"name": "Alice"}`是幂等的(将名称设置为"Alice"两次没问题)。`POST /payments`不是(支付两次很糟糕)。使操作幂等可确保重试是安全的。