为社交网络设计数据结构 注释:为了避免重复,这篇文章的链接直接关联到 系统设计主题 的相关章节。为一讨论要点、折中方案和可选方案做参考。 第 1 步:用例和约束概要 收集需求并调查问题。 通过提问清晰用例和约束。 讨论假设。 如果没有面试官提出明确的问题,我们将自己定义一些用例和约束条件。 用例 我们就处理以下用例审视这一问题 用户 寻找某人并显示与被寻人之间的最短路径 服务 高可用 约束和假设 状态假设 流量分布不均 某些搜索比别的更热门,同时某些搜索仅执行一次 图数据不适用单一机器 图的边没有权重 1 千万用户 每个用户平均有 50 个朋友 每月 10 亿次朋友搜索 训练使用更传统的系统 - 别用图特有的解决方案例如 GraphQL 或图数据库如 Neo4j。
注释:为了避免重复,这篇文章的链接直接关联到 系统设计主题 的相关章节。为一讨论要点、折中方案和可选方案做参考。
收集需求并调查问题。
通过提问清晰用例和约束。
讨论假设。
如果没有面试官提出明确的问题,我们将自己定义一些用例和约束条件。
训练使用更传统的系统 - 别用图特有的解决方案例如 GraphQL 或图数据库如 Neo4j。
向你的面试官厘清你是否应该做粗略的使用计算
便捷的转换指南:
用所有重要组件概述高水平设计

深入每个核心组件的细节。
和你的面试官说清你期望的代码量
没有百万用户(点)的和十亿朋友关系(边)的限制,我们能够用一般 BFS 方法解决无权重最短路径任务:
class Graph(Graph): def shortest_path(self, source, dest): if source is None or dest is None: return None if source is dest: return [source.key] prev_node_keys = self._shortest_path(source, dest) if prev_node_keys is None: return None else: path_ids = [dest.key] prev_node_key = prev_node_keys[dest.key] while prev_node_key is not None: path_ids.append(prev_node_key) prev_node_key = prev_node_keys[prev_node_key] return path_ids[::-1] def _shortest_path(self, source, dest): queue = deque() queue.append(source) prev_node_keys = {source.key: None} source.visit_state = State.visited while queue: node = queue.popleft() if node is dest: return prev_node_keys prev_node = node for adj_node in node.adj_nodes.values(): if adj_node.visit_state == State.unvisited: queue.append(adj_node) prev_node_keys[adj_node.key] = prev_node.key adj_node.visit_state = State.visited return None
我们不能在同一台机器上满足所有用户,我们需要通过 人员服务器 拆分 用户并且通过 查询服务 访问。
friend_ids 列表source 运行 BFS 搜索算法同时 当前用户的 friend_ids 作为每个 adjacent_node 的 idsadjacent_node:
adjacent_node 的 人员服务器(有待优化)和你的面试官说清你应该写的代码量
注释:简易版错误处理执行如下。询问你是否需要编写适当的错误处理方法。
查询服务 实现:
class LookupService(object): def __init__(self): self.lookup = self._init_lookup() # key: person_id, value: person_server def _init_lookup(self): ... def lookup_person_server(self, person_id): return self.lookup[person_id]
人员服务器 实现:
class PersonServer(object): def __init__(self): self.people = {} # key: person_id, value: person def add_person(self, person): ... def people(self, ids): results = [] for id in ids: if id in self.people: results.append(self.people[id]) return results
用户 实现:
class Person(object): def __init__(self, id, name, friend_ids): self.id = id self.name = name self.friend_ids = friend_ids
用户图服务 实现:
class UserGraphService(object): def __init__(self, lookup_service): self.lookup_service = lookup_service def person(self, person_id): person_server = self.lookup_service.lookup_person_server(person_id) return person_server.people([person_id]) def shortest_path(self, source_key, dest_key): if source_key is None or dest_key is None: return None if source_key is dest_key: return [source_key] prev_node_keys = self._shortest_path(source_key, dest_key) if prev_node_keys is None: return None else: # Iterate through the path_ids backwards, starting at dest_key path_ids = [dest_key] prev_node_key = prev_node_keys[dest_key] while prev_node_key is not None: path_ids.append(prev_node_key) prev_node_key = prev_node_keys[prev_node_key] # Reverse the list since we iterated backwards return path_ids[::-1] def _shortest_path(self, source_key, dest_key, path): # Use the id to get the Person source = self.person(source_key) # Update our bfs queue queue = deque() queue.append(source) # prev_node_keys keeps track of each hop from # the source_key to the dest_key prev_node_keys = {source_key: None} # We'll use visited_ids to keep track of which nodes we've # visited, which can be different from a typical bfs where # this can be stored in the node itself visited_ids = set() visited_ids.add(source.id) while queue: node = queue.popleft() if node.key is dest_key: return prev_node_keys prev_node = node for friend_id in node.friend_ids: if friend_id not in visited_ids: friend_node = self.person(friend_id) queue.append(friend_node) prev_node_keys[friend_id] = prev_node.key visited_ids.add(friend_id) return None
我们用的是公共的 REST API:
$ curl https://social.com/api/v1/friend_search?person_id=1234
响应:
{ "person_id": "100", "name": "foo", "link": "https://social.com/foo", }, { "person_id": "53", "name": "bar", "link": "https://social.com/bar", }, { "person_id": "1234", "name": "baz", "link": "https://social.com/baz", },
内部通信使用 远端过程调用。
在给定约束条件下,定义和确认瓶颈。

重要:别简化从最初设计到最终设计的过程!
你将要做的是:1) 基准/负载 测试, 2) 瓶颈 概述, 3) 当评估可选和折中方案时定位瓶颈,4) 重复。以 在 AWS 上设计支持百万级到千万级用户的系统 为参考迭代地扩展最初设计。
讨论最初设计可能遇到的瓶颈和处理方法十分重要。例如,什么问题可以通过添加多台 Web 服务器 作为 负载均衡 解决?CDN?主从副本?每个问题都有哪些替代和 折中 方案?
我们即将介绍一些组件来完成设计和解决扩展性问题。内部负载均衡不显示以减少混乱。
避免重复讨论,以下网址链接到 系统设计主题 相关的主流方案、折中方案和替代方案。
解决 平均 每秒 400 次请求的限制(峰值),人员数据可以存在例如 Redis 或 Memcached 这样的 内存 中以减少响应次数和下游流量通信服务。这尤其在用户执行多次连续查询和查询哪些广泛连接的人时十分有用。从内存中读取 1MB 数据大约要 250 微秒,从 SSD 中读取同样大小的数据时间要长 4 倍,从硬盘要长 80 倍。1
以下是进一步优化方案:
根据问题的范围和剩余时间,还需要深入讨论其他问题。
参考 安全章节
查阅 每个程序员必懂的延迟数字
Note: This document links directly to relevant areas found in the system design topics to avoid duplication. Refer to the linked content for general talking points, tradeoffs, and alternatives.
Gather requirements and scope the problem.
Ask questions to clarify use cases and constraints.
Discuss assumptions.
Without an interviewer to address clarifying questions, we'll define some use cases and constraints.
Exercise the use of more traditional systems - don't use graph-specific solutions such as GraphQL or a graph database like Neo4j
Clarify with your interviewer if you should run back-of-the-envelope usage calculations.
Handy conversion guide:
Outline a high level design with all important components.

Dive into details for each core component.
Clarify with your interviewer how much code you are expected to write.
Without the constraint of millions of users (vertices) and billions of friend relationships (edges), we could solve this unweighted shortest path task with a general BFS approach:
class Graph(Graph): def shortest_path(self, source, dest): if source is None or dest is None: return None if source is dest: return [source.key] prev_node_keys = self._shortest_path(source, dest) if prev_node_keys is None: return None else: path_ids = [dest.key] prev_node_key = prev_node_keys[dest.key] while prev_node_key is not None: path_ids.append(prev_node_key) prev_node_key = prev_node_keys[prev_node_key] return path_ids[::-1] def _shortest_path(self, source, dest): queue = deque() queue.append(source) prev_node_keys = {source.key: None} source.visit_state = State.visited while queue: node = queue.popleft() if node is dest: return prev_node_keys prev_node = node for adj_node in node.adj_nodes.values(): if adj_node.visit_state == State.unvisited: queue.append(adj_node) prev_node_keys[adj_node.key] = prev_node.key adj_node.visit_state = State.visited return None
We won't be able to fit all users on the same machine, we'll need to shard users across Person Servers and access them with a Lookup Service.
friend_idssource and the current user's friend_ids as the ids for each adjacent_nodeadjacent_node from a given id:
adjacent_node matching the given id (potential for optimization)Clarify with your interviewer how much code you should be writing.
Note: Error handling is excluded below for simplicity. Ask if you should code proper error handing.
Lookup Service implementation:
class LookupService(object): def __init__(self): self.lookup = self._init_lookup() # key: person_id, value: person_server def _init_lookup(self): ... def lookup_person_server(self, person_id): return self.lookup[person_id]
Person Server implementation:
class PersonServer(object): def __init__(self): self.people = {} # key: person_id, value: person def add_person(self, person): ... def people(self, ids): results = [] for id in ids: if id in self.people: results.append(self.people[id]) return results
Person implementation:
class Person(object): def __init__(self, id, name, friend_ids): self.id = id self.name = name self.friend_ids = friend_ids
User Graph Service implementation:
class UserGraphService(object): def __init__(self, lookup_service): self.lookup_service = lookup_service def person(self, person_id): person_server = self.lookup_service.lookup_person_server(person_id) return person_server.people([person_id]) def shortest_path(self, source_key, dest_key): if source_key is None or dest_key is None: return None if source_key is dest_key: return [source_key] prev_node_keys = self._shortest_path(source_key, dest_key) if prev_node_keys is None: return None else: # Iterate through the path_ids backwards, starting at dest_key path_ids = [dest_key] prev_node_key = prev_node_keys[dest_key] while prev_node_key is not None: path_ids.append(prev_node_key) prev_node_key = prev_node_keys[prev_node_key] # Reverse the list since we iterated backwards return path_ids[::-1] def _shortest_path(self, source_key, dest_key, path): # Use the id to get the Person source = self.person(source_key) # Update our bfs queue queue = deque() queue.append(source) # prev_node_keys keeps track of each hop from # the source_key to the dest_key prev_node_keys = {source_key: None} # We'll use visited_ids to keep track of which nodes we've # visited, which can be different from a typical bfs where # this can be stored in the node itself visited_ids = set() visited_ids.add(source.id) while queue: node = queue.popleft() if node.key is dest_key: return prev_node_keys prev_node = node for friend_id in node.friend_ids: if friend_id not in visited_ids: friend_node = self.person(friend_id) queue.append(friend_node) prev_node_keys[friend_id] = prev_node.key visited_ids.add(friend_id) return None
We'll use a public REST API:
$ curl https://social.com/api/v1/friend_search?person_id=1234
Response:
{ "person_id": "100", "name": "foo", "link": "https://social.com/foo", }, { "person_id": "53", "name": "bar", "link": "https://social.com/bar", }, { "person_id": "1234", "name": "baz", "link": "https://social.com/baz", },
For internal communications, we could use Remote Procedure Calls.
Identify and address bottlenecks, given the constraints.

Important: Do not simply jump right into the final design from the initial design!
State you would 1) Benchmark/Load Test, 2) Profile for bottlenecks 3) address bottlenecks while evaluating alternatives and trade-offs, and 4) repeat. See Design a system that scales to millions of users on AWS as a sample on how to iteratively scale the initial design.
It's important to discuss what bottlenecks you might encounter with the initial design and how you might address each of them. For example, what issues are addressed by adding a Load Balancer with multiple Web Servers? CDN? Master-Slave Replicas? What are the alternatives and Trade-Offs for each?
We'll introduce some components to complete the design and to address scalability issues. Internal load balancers are not shown to reduce clutter.
To avoid repeating discussions, refer to the following system design topics for main talking points, tradeoffs, and alternatives:
To address the constraint of 400 average read requests per second (higher at peak), person data can be served from a Memory Cache such as Redis or Memcached to reduce response times and to reduce traffic to downstream services. This could be especially useful for people who do multiple searches in succession and for people who are well-connected. Reading 1 MB sequentially from memory takes about 250 microseconds, while reading from SSD takes 4x and from disk takes 80x longer.1
Below are further optimizations:
Additional topics to dive into, depending on the problem scope and time remaining.
Refer to the security section.
See Latency numbers every programmer should know.