585-系统设计面试-亚历克斯徐-Technology-2020
585-系统设计面试-亚历克斯徐-Technology-2020
Barack
September 7, 2025
《System Design Interview》,首版于2020年。系统设计面试被许多人认为是最复杂、最难的技术工作面试。本书提供了一个循序渐进的框架,指导您如何应对系统设计问题。本书包含许多真实案例,以系统化的方法和可遵循的详细步骤进行说明。
Alex Xu。曾就读于Carnegie Mellon University。曾就职于ModeIn N,Oracle,Apple,Twitter。于2019年创建公司,ByteByteGo。
Table of Contents
CHAPTER 1: SCALE FROM ZERO TO MILLIONS OF USERS
CHAPTER 2: BACK-OF-THE-ENVELOPE ESTIMATION
CHAPTER 3: A FRAMEWORK FOR SYSTEM DESIGN INTERVIEWS
CHAPTER 4: DESIGN A RATE LIMITER
CHAPTER 5: DESIGN CONSISTENT HASHING
CHAPTER 6: DESIGN A KEY-VALUE STORE
CHAPTER 7: DESIGN A UNIQUE ID GENERATOR IN DISTRIBUTED SYSTEMS
CHAPTER 8: DESIGN A URL SHORTENER
CHAPTER 9: DESIGN A WEB CRAWLER
CHAPTER 10: DESIGN A NOTIFICATION SYSTEM
CHAPTER 11: DESIGN A NEWS FEED SYSTEM
CHAPTER 12: DESIGN A CHAT SYSTEM
CHAPTER 13: DESIGN A SEARCH AUTOCOMPLETE SYSTEM
CHAPTER 14: DESIGN YOUTUBE
CHAPTER 15: DESIGN GOOGLE DRIVE
CHAPTER 16: THE LEARNING CONTINUES
这几天正好有个面试,主题是系统设计。刚好,这也是我最近在用coding agent开发视觉小说生成器时遇到的代码问题。我现在的工程文件里,把所有功能都堆在一个文件里,添加一点新东西就像往积木塔上塞砖头,摇摇欲坠,写着写着就发现可维护性越来越低。我不禁问自己:为什么同样是写程序,到了复杂的时候就一塌糊涂?答案其实很简单——缺少系统设计。如果只是自娱自乐,写个小脚本,坏了也无妨;可一旦用户增多,功能复杂,没有好的设计就像没有地基的楼,迟早要塌。那什么是好的设计呢?书里提到一些基本原则,比如保持 Web 层无状态,这意味着用户相关的数据不要放在服务器内存里,而要放到数据库里;比如考虑冗余,你不能指望单点服务器万无一失,要有主从、要有负载均衡、要有多数据库和多 CDN,甚至在全球化时要考虑多数据中心和 DNS 的就近路由。还有,把静态资源放进 CDN,利用分片减轻单一节点压力,把服务切分成独立模块,降低耦合度,再加上日志和监控,才能及时发现和修复问题。这些东西不就是常见的工程套路吗?但为什么到自己手上就容易忽视?或许正因为我常常心急,觉得能跑就行,却忘了长远。进一步想,我又意识到,系统设计不仅是工程问题,它其实也是一种思维方式。当下流行的 agent 系统更是如此。一个 agent 如果只是工具,只能被动执行指令,那它的生命力就有限;但若它有目标感、能推理、有记忆、有行动力,它就能在复杂任务中自我规划、自主选择,还能在必要时提问澄清,并且从过往经验里获得反馈。我在想,这不就像一个真正成熟的“人”吗?他既能独立决策,又能与别人的意图保持一致;他不是靠硬编码的流程,而是围绕目标不断调整路径;他必须具备交互性,能和用户实时对话,甚至能和其他 agent 协作分工,一个写作,一个编码,一个审核,还有一个像总管一样协调全局。听起来很理想化,但其实我们已经在现实里看到雏形——比如智能客服、游戏里的 NPC、健康教练类应用,背后都是类似的设计思路。这些新的 agent 系统,和传统的系统设计到底是什么关系?是断层还是延续?延续。任何新东西都要站在旧的理解上,agent 也一样。没有对传统系统设计的深刻把握,就很难真正驾驭这些新兴的复杂架构。归根到底,系统设计是一种训练:它逼着人从短期的“能跑就行”转向长期的“如何稳固”,逼着人从单点思维转向全局思维。
今天参加面试,题目是Design a chat system。尴尬的是,我在面试前只翻完了书里的第六章,却没来得及读到第十二章,而偏偏那一章正是系Design a chat system。不过尽管如此,我已经阅读的前六章,对我面试时整理思路,还是很有帮助的。我现在的项目越做越大,文件也越来越臃肿,甚至有一个web_app.py文件长到五千八百行。我不得不动手去拆分模块,让 AI agent 帮忙把大文件拆解成小块。可是一旦重构,新的 bug 接踵而至。如果系统很小,也许还能勉强凑合,可是系统一旦变复杂,糟糕的设计就会变成隐患。试想,一个文件如果上万行,不仅人难以阅读,AI 处理时也要消耗大量算力,这不正暴露出系统设计的重要性吗?哪怕我们用 AI 来写代码,它也只能帮忙执行,真正的架构思路还是要人来反复提醒、反复斟酌。那我该如何学习系统设计呢?光看别人写的优雅代码当然有帮助,但为什么常常收获不深?或许答案是:必须亲手犯错,才能真正理解。幸好现在犯错的成本比过去低了很多,以前得一行一行地手写,推倒重来会让人望而生畏,而如今有了代码生成工具,我们可以用较低的代价进行反复重构。当我们把同一个系统推翻重写两三次时,可能发现对它的理解更深了。这也让我在面试的最后,向面试官提问:在 AI 辅助编程已经普及的今天,系统设计是否也应该有一种新的方式?
其实系统设计的面试不该是一个人的独角戏,它更像是一场对话,是一个和面试官共同澄清问题、逐步推进的过程。我常常问自己:在真实工作里,当老板交代任务时,如果我连问题都没弄清楚,怎么能动手?所以第一步一定是 understand problem and establish design scope,要弄清哪些是需要解决的,哪些是暂时不必考虑的。比如说,假如题目是设计一个聊天系统,我就要先问清楚:这是一个一对一的聊天,还是支持多人群聊?它是一个只在手机端运行的 APP,还是需要同时支持网页端?规模有多大?是创业公司的原型,只需支撑一千用户,还是大公司级别,要承载上千万甚至上亿用户?群聊时单个群的成员上限是多少?消息的形态是什么?只支持文字,还是能发图片、视频和附件?消息大小有没有限制?需不需要考虑端到端加密?聊天记录要保存多久?这些问题如果不在一开始厘清,就很可能导致设计方向走偏。那接下来该做什么?就是 propose high level design and get buy-in,先画出一个粗略的架构图,让面试官认可你的思路,再逐步细化。比如我可以先说:我们有一个 sender,一个 receiver,中间有一个服务器,服务器的角色是存储消息、转发消息。然后再问自己:服务器要支持哪些接口?API 要怎么定义?转发时是否需要做队列和限流?这些细节一步步展开。这样思考的过程,就像在和面试官一起搭积木,从地基到框架,再到细节。而我反省自己,如果不这样去追问和澄清,往往就会陷入凭直觉胡乱设计的困境。
如果要设计一个聊天系统,最关键的能力是什么?我一开始想当然地以为只是“能发消息”,可仔细想才发现,它更本质的是“如何维持消息在客户端和服务器之间的稳定往返”。书里提到常用的协议是 WebSocket,它的特点是双向、持久,也就是一旦建立,就能保持一个持续不断的双工通道。那它是怎么运作的呢?起初由客户端发起一个 HTTP 握手,服务器确认后返回应答,于是双方之间的通信就能一直持续下去。这样一来,每当服务器收到消息,就能第一时间推给客户端。为什么不用传统的请求-响应模式?答案在于实时性。聊天不同于刷网页,如果掉线了,消息就会丢失,体验就彻底崩溃。于是整体架构就需要拆成几个模块。第一个是无状态服务,这类服务适合处理用户登录、资料更新、群管理等不需要实时连接的功能,它们通常做成独立的微服务,还能借助现成的集成方案。这里最核心的是服务发现,它就像给客户端一张地图,告诉它有哪些聊天服务器可用。第二个是有状态的聊天服务,它必须维持长连接,比如你用微信,如果连接断了,消息就收不到了。因为要持续保持网络通道,所以这类服务一般不会频繁切换负载,而是稳定地和某一台服务器绑定。第三个是第三方集成,比如消息推送。想象一下,当你收到新消息时,手机上跳出的通知弹窗,并不是聊天服务本身直接完成的,而是通过操作系统提供的推送平台来实现的。这样梳理下来,一个高层次的聊天系统架构就清晰了:无状态服务负责“静态”的部分,有状态服务负责“实时”的部分,第三方服务负责“触达”的部分。以前总觉得聊天系统无非就是收发消息,但现在明白,真正的难点在于如何把不同模块组合起来,既保证实时性,又保证扩展性。那是不是意味着我理解得已经足够了?恐怕还远远不够:API 怎样定义?消息队列如何处理?断线重连怎么保证?这些问题只有亲自设计过、踩过坑,才会有更深的体会。
扩展性这个问题,看似抽象,其实在实践中往往最容易被忽视。我以前创业的时候就吃过亏,我们买了服务器,最初用的人不多,一切都很顺利,可是一旦同时在线的用户多了,系统就频频崩溃。奇怪的是,后台数据显示 CPU 和内存并没有跑满,照理说机器没到极限,可为什么还是撑不住?那时我们能想到的办法只有重启,但重启之后不久又挂掉。后来才意识到真正的问题不在硬件,而在于我们对扩展性的理解不够。试想,如果所有请求都涌向同一台服务器,高并发压力很容易会让系统崩盘,只是崩溃的根源往往不是肉眼可见的指标。一个真正可扩展的聊天系统,至少要分成几个核心模块:其一是聊天服务(chat service),负责消息的收发,必须支持高并发和实时性;其二是在线状态服务(presence service),用来管理用户的上下线状态;其三是用户 API 服务,处理登录、注册、修改资料等请求;其四是通知服务(notification service),负责把新消息推送到用户的设备上;最后还有一个关键模块——键值存储(KV store),它用来保存聊天记录,以便用户离线后再次上线时能同步消息。那这些服务该如何通信?书里提醒我,像聊天和在线状态这样的实时服务适合用 WebSocket 来保持长连接,而用户资料、通知之类的功能则用传统的 HTTP 就足够了。我就自问:如果所有模块都混在一起会怎样?答案很简单——耦合太高,扩展性差,问题难以定位,最终导致整个系统脆弱不堪。反过来,如果能把不同的职责拆开,并为每个服务考虑冗余和备份,那么即使某个节点挂掉,系统整体也能继续运行。
既然要设计聊天应用,那么绕不过去的问题就是数据的存储和消息的流转。聊天产生的数据如此庞大,该怎么存放才合适?如果是用户资料、好友关系这样的结构化信息,关系型数据库自然最合适;但若是体量巨大的聊天记录,再用关系型数据库就显得笨重了,这时键值存储(Key-Value Store)这种非关系型数据库就成了更高效的选择。那么具体该如何设计数据模型呢?对于一对一聊天,一条消息至少需要包含 message ID、发送者、接收者、内容和创建时间;而在群聊里,还要额外包含群 ID 和成员 ID 等信息。解决了存储问题,下一个难题是消息如何在系统里流转。一对一聊天的流程可以想象成这样:用户 A 把消息发给自己连接的聊天服务器,服务器从 ID 生成器那里拿到一个唯一的 message ID,然后把消息写进消息队列,再存入键值存储。如果用户 B此刻在线,消息就会被转发到 B 所连接的服务器,再通过持续的 WebSocket 通道推送给 B;如果 B 不在线,则会通过通知服务推送提醒,等他上线时再同步消息。那群聊又该怎么处理?显然复杂得多。比如一个群里有三个人,A 发消息后,系统需要把这条消息复制到每个成员的消息队列里,这在人数不多时很合理,但若一个群有五千人,逐一复制就太夸张了,所以一些系统会采用不同的策略来平衡效率和复杂度。再从接收方看,每个用户其实都有一个 inbox,里面汇聚了来自不同发送者的消息队列。除此之外,还有很多细节值得深思:应用是否支持图片、视频等媒体?是否需要端到端加密?是否要在客户端缓存以减少加载延迟?如果聊天服务器宕机,消息如何重试和补发?这些问题在面试里不可能面面俱到,但基础流程和需求一定要讲清楚。我自己在准备时发现,用 draw.io 之类的工具现场画出流程图,不仅能帮我理清思路,也能让面试官更直观地理解我的设计;或者直接打开一个文本文件,把思路逐条写下来分享,也能起到类似效果。说到底,面试不只是考察答案,更是考察你如何一步步思考和表达。