2536c937e3
翻译自英文原版 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/ 构建缓存
17 KiB
17 KiB
系统设计基础
系统设计是构建可在大规模下可靠运行的软件的方法。本文件涵盖客户端-服务器架构、网络协议、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文件中定义消息模式:
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,并收集使用指标。
负载均衡
- 当你拥有多台服务器时,负载均衡器将传入请求分布到它们之间。
- 算法:
- 轮询:按顺序发送请求到服务器(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路由到嵌入服务器。较慢但更灵活。
缓存
- 缓存将频繁访问的数据存储在快速存储层(内存)中,以避免重新计算或重新获取。
- 缓存模式:
- 缓存旁路(惰性加载):应用程序先检查缓存。未命中时,从数据库获取、存储到缓存并返回。最常见的模式。
- 直写:每次写入同时写入缓存和数据库。确保缓存始终是最新的,但会减慢写入速度。
- 回写:写入只进入缓存;缓存异步刷新到数据库。写入最快,但若缓存刷新前崩溃则有数据丢失风险。
- 驱逐策略(当缓存满时):
- 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定理
- 在分布式数据库中,最多只能满足三个属性中的两个:
- 一致性:每次读取都返回最新的写入。
- 可用性:每个请求都会收到响应(即使某些节点宕机)。
- 分区容忍性:系统在网络分区(节点无法通信)时仍能继续运行。
- 由于分布式系统中网络分区不可避免,真正的选择是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被弃用但在流量下降之前不会删除。 - 错误响应:返回结构化的错误信息:
{
"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不是(支付两次很糟糕)。使操作幂等可确保重试是安全的。