☰
七牛云校招笔试题卷二复盘:分布式系统与Go核心考点解析
2026/10/24 7:54:32 网站建设 项目流程

每年校招季,总有人翻出往年的真题做复盘,2018七牛云校招笔试题(卷二)就是其中一套被反复讨论的卷子。我当年准备云存储和基础架构方向时,把能找到的存储/CDN类笔试都刷了一遍,这套卷二给我的印象最深:选择题比编程题更让人心虚,分布式系统题直接决定你能不能进下一轮。这篇文章我就结合当时的笔记和后来的工作理解,把整套卷子的考点、典型题、估算思路完整梳理一遍,给准备校招、想投云存储/基础架构/Go开发方向的同学做参考。

这套卷子的价值不在题目本身,而在它背后的命题逻辑。七牛云是做对象存储和CDN起家的,技术栈以Go为主,核心产品面向海量数据和高并发场景。所以卷二不像普通校招卷那样只考算法刷题,它更关注你有没有分布式系统的直觉,懂不懂网络和操作系统底层,能不能在边界条件模糊的情况下把估算题算得合理。下面我从题型分布开始,逐块拆解。

1. 为什么这套“卷二”值得单独复盘:先看懂七牛云在找什么人

1.1 一卷和二卷的定位差异

很多公司校招笔试会拆成两张卷子,卷一通常是公共卷,考数据结构、算法、语言基础,所有岗位共用;卷二则是方向卷,更贴近具体业务和岗位要求。七牛云的卷一和卷二就是这么分工的:卷一筛选的是“基础够不够扎实”,卷二筛选的是“能不能做分布式存储/CDN相关的事”。

从命题逻辑反推,如果公司主要用Go语言,核心产品是对象存储、CDN调度、数据处理管道,那笔试就应该重点考这些领域的基本功。卷二里有不少题看起来是“常识题”,比如TCP的三次握手、进程和线程的区别,但真正拉开差距的是后面的分布式一致性哈希、副本策略、容量估算。它不要求你达到架构师水平,但要求你能把“为什么这样做”讲清楚,而不是只背结论。

1.2 卷二涉及的主要模块

根据我的回忆和整理,这一卷的考点大概可以分成五个模块:

  • 计算机网络:TCP/UDP、HTTP、DNS、CDN原理
  • 操作系统:进程/线程/协程、内存布局、IO模型
  • 分布式基础:一致性哈希、副本、一致性模型、Raft入门
  • 算法与数据结构:海量数据处理、LRU、TopK
  • Go语言与Linux基础:Goroutine、Channel、常用命令

这里有个隐形的门槛:如果你完全没有接触过Go,看到并发编程题时可能会懵。但七牛云的笔试不是专门考Go语法,它考的是并发思维。只要能讲清楚线程和协程的区别,理解Goroutine的调度模型,用伪代码写也没太大问题。

1.3 时间分配策略

我记得这套卷子题量看起来不大,大概十几道题,但真正做起来时间很紧。选择题和填空题看着简单,一犹豫就过去好几分钟;后面的估算题需要写计算过程,编程题要写完整代码,都是吃时间的环节。

我当时的策略是:

  1. 先把有把握的题做完,尤其是选择题和填空题,保证基础分。
  2. 再做编程题,先写思路框架,再补边界条件。
  3. 最后做系统估算题,这类题没有唯一答案,只要步骤清晰、假设合理,就能拿大部分分。
  4. 单题卡住超过15分钟果断跳过,不要恋战。

一个很现实的点:笔试成绩不只看你答对多少,还看你的答题策略。如果空题太多,面试官会怀疑你的时间管理能力。

2. 选择题里那些“好像会,但一选就错”的考点:TCP、并发与内存

2.1 TCP 三次握手和四次挥手,没你想的那么简单

TCP是网络部分雷打不动的考点。三次握手的过程大家都会背:客户端发SYN,服务端回SYN+ACK,客户端再回ACK。但选择题往往不会直接这么考,它会在细节上做文章。

比如“为什么是三次而不是两次”这个追问。两次握手的问题在于:客户端发送的第一个连接请求如果在网络中滞留了很久,客户端已经超时重传并建立连接后,这个失效的请求才到达服务端。服务端会以为是新连接,于是分配资源等待客户端确认,造成资源浪费。三次握手让服务端确认了客户端的接收能力,这个坑就能避开。

四次挥手和TIME_WAIT也是高频陷阱。主动关闭方在发送最后一个ACK后会进入TIME_WAIT状态,持续2MSL(最长报文段寿命的两倍)。为什么要等2MSL?原因有两个:一是确保最后一个ACK能到达被动关闭方,如果丢失,被动关闭方会重发FIN,主动方需要重新ACK;二是让本次连接中所有在网络里残留的报文段都自然消失,避免污染后续连接。

有一类选择题会这样问:如果服务端收到RST报文,连接会怎么样?麻烦的地方在于,很多人会把RST和FIN搞混。FIN是正常关闭流程,RST是异常中止。收到RST后,主动方直接丢弃连接,不会进入TIME_WAIT,也不会有四次挥手。

SYN Flood是另一个常见知识点。攻击者发送大量SYN但不完成第三次握手,把服务端的半连接队列打满,导致正常连接无法建立。常见缓解手段有SYN Cookie、限制SYN速率、增大半连接队列长度。

2.2 进程、线程、协程:考概念,更考对比

进程、线程、协程的对比,几乎是所有校招笔试的必考题。很多人能背出定义,但遇到场景题就分不清了。

维度进程线程协程
资源开销独立地址空间,开销大共享进程地址空间,开销小用户态调度,开销最小
调度单位操作系统调度操作系统调度程序员控制调度
通信方式IPC,需内核介入共享内存,需加锁Channel/消息传递
适用场景隔离性强、多实例IO密集型任务高并发、大量轻量任务

易错点在于:协程虽然轻量,但不意味着没有并发问题。多个协程访问同一个共享变量,照样需要加锁或使用原子操作。Go的Channel只是提供了一种消息传递模型,并不能替代锁解决所有竞态问题。

Go的Goroutine调度模型也是这一段的加分项。简单说,GMP模型里M是操作系统线程,P是处理器,G是Goroutine。P的数量决定同一时刻真正并行执行的Goroutine数量,M负责从本地队列或全局队列中获取G执行。G发生阻塞时,M会被释放去执行其他G,这就是Goroutine支持高并发的原因。

2.3 内存和回收的细节

内存相关题目,主要考栈和堆的区别、内存对齐、内存泄漏排查思路。

栈和堆的区别比较基础:栈由编译器自动分配和释放,连续且速度快;堆需要手动管理或依赖垃圾回收,容易产生碎片。但在Go里有个额外考点:逃逸分析。如果编译器发现一个变量的作用域超越了函数,就会把它分配到堆上,否则分配到栈上。笔试里如果给出一段Go代码,问某个变量在栈上还是堆上,很多人会答错,因为没考虑逃逸。

内存对齐是个高频填空题。一个包含bool、int64、string字段的结构体,如果字段顺序不同,结构体大小会不一样。原因是CPU访问对齐内存时更高效,所以编译器会在字段之间插入填充字节。举个例子:

type A struct { a bool // 1字节 _ [7]byte // 对齐填充 b int64 // 8字节 }

如果字段a在前,b在后,A占16字节;如果先放int64再放bool,占9字节,但编译器可能还会对齐到16字节。这个点虽然琐碎,但很能拉开区分度。

内存泄漏排查,主要考察思路。Go里用pprof做heap profile,连续多次GC后观察内存是否回落;如果堆内存持续增长,说明可能存在内存泄漏。写这类题不需要背命令,讲清楚“先定位哪个函数创建了大量对象,再分析引用链”就行。

2.4 输入输出模型:选完这个,后面的网络题才有基础

IO模型这一块,存储和CDN公司基本必考,因为涉及到高并发网络服务。阻塞IO、非阻塞IO、IO多路复用、异步IO,这四个概念要理清。

select、poll、epoll的对比是常见大题。select有FD_SETSIZE限制,轮询所有fd,效率随连接数下降;poll用链表,解决了数量限制,但仍是线性扫描;epoll通过回调机制,只返回有事件发生的fd,所以在大量连接下效率远高于前两者。

做题诀窍:一旦题干里出现“百万连接”“高并发”“C10K”,基本就是在考epoll。七牛云这种做对象存储的,控制面和数据面都有大量长连接,IO模型选型直接关系性能,所以笔试不会绕过这个点。

3. 编程题不考偏题,但你的代码得“能上线”

编程题给我的感觉是:难度约等于LeetCode中等题,但比LeetCode更强调工程落地。它不要求你用很冷门的算法,而是要求你选择合适的数据结构,把复杂度写清楚,并考虑到边界条件。

3.1 Top K:海量数据下的高频考点

题目场景通常是:从一亿个整数里找出最大的1000个。如果直接排序,时间复杂度O(n log n),在一亿数据下不可接受。更合理的方案是小顶堆。

用一个小顶堆维护当前最大的1000个数,堆顶是这1000个数里的最小值。遍历数据时,如果当前数比堆顶大,就替换堆顶并调整堆。这样时间复杂度是O(n log k),k=1000时几乎等于O(n),空间复杂度O(k)。

import heapq def top_k(nums, k): min_heap = [] for num in nums: if len(min_heap) < k: heapq.heappush(min_heap, num) elif num > min_heap[0]: heapq.heapreplace(min_heap, num) return sorted(min_heap, reverse=True)

海量数据变体会说内存装不下所有数据。解决办法是哈希分片:按某种规则把数据分到多个桶,每个桶分别取TopK,最后再合并。这里要留意分片要均匀,否则某个桶数据量过大,内存还是不够。

3.2 手写 LRU Cache

LRU(Least Recently Used)出现的频率极高,因为它在缓存场景里非常实用。题目要求get和put都是O(1)时间复杂度。

O(1)的get显然需要哈希表,O(1)的put需要维护访问顺序,所以经典方案是哈希表加双向链表。哈希表的key对应链表节点,链表按访问时间从新到旧排列。每次get时,把对应节点移到链表头部;每次put时,如果容量满了,淘汰链表尾部的节点。

class LRUCache: def __init__(self, capacity): self.cap = capacity self.dict = {} self.head = Node(0, 0) self.tail = Node(0, 0) self.head.next = self.tail self.tail.prev = self.head def get(self, key): if key not in self.dict: return -1 node = self.dict[key] self._remove(node) self._add(node) return node.value def put(self, key, value): if key in self.dict: self._remove(self.dict[key]) node = Node(key, value) self.dict[key] = node self._add(node) if len(self.dict) > self.cap: last = self.tail.prev self._remove(last) del self.dict[last.key]

代码本身不难,难在选择数据结构。很多人会用数组或者Python的OrderedDict,但面试官更想看到你理解“哈希表负责查找,链表负责顺序”这个组合逻辑。

另外附带一个工程视角:单机可以用精确LRU,但分布式缓存比如Redis,实际用的是近似LRU,因为精确LRU在每个key上维护时间戳和链表代价太高。这个知识点写进卷子能加分。

3.3 两个大文件求交集

这个题目很经典:A文件50GB,B文件50GB,内存只有8GB,怎么找出两个文件里相同的字符串?

最容易想到的方案是哈希分片。对A和B的每一行分别计算hash,按hash值取模分成多个小文件,比如分成100份。然后分别对比A_0和B_0,A_1和B_1,以此类推。每一对小文件大小约500MB,可以加载进内存做精确交集。

外部排序加归并是另一个方案:先对两个文件分别外部排序,再用双指针做归并求交集。排序的磁盘IO比哈希分片大,但哈希函数需要均匀,否则分片后数据倾斜会拖慢速度。

布隆过滤器也可以用来做预筛:把A的hash值写入布隆过滤器,然后遍历B输出可能存在的数据。但布隆过滤器有误判率,会把不存在的key也放进来,所以只能用来过滤掉大部分不存在的数据,最后还需要精确验证。

方案内存磁盘IO准确性
哈希分片低高精确
外部排序+归并低更高精确
布隆过滤器低低有误判

笔试里只需写出方案和复杂度,但在实际系统中,哈希分片是最常用的。

3.4 编程题到底怎么判分

这个点很多同学不知道:对外存储公司来说,编程题判分不只看能不能跑通,更看复杂度分析和边界条件。面试官拿到试卷后,会重点看几处:

  • 是否有空输入、单元素、重复元素的处理
  • 时间复杂度和空间复杂度是否写清楚
  • 代码里有没有硬编码
  • 数据结构的选用是否合理

我在卷子上有个习惯:写完代码后,在代码块末尾用注释写上时间复杂度和空间复杂度。这既是给面试官看的,也是帮自己理清思路。边界条件更要主动覆盖,比如LRU的容量为0或1,TopK的k大于数组长度,都要考虑到。

4. 分布式系统题:卷二真正拉开差距的地方

如果说前三部分是基础和算法,那分布式部分就是七牛云这种存储公司的“主菜”。卷二在这块大概会出两三道题,覆盖面主要是一致性哈希、副本与一致性、CDN场景。

4.1 一致性哈希:从“为什么有它”到“怎么实现”

一致性哈希几乎是所有分布式缓存和存储系统的必考题。面试官通常不会只问概念,而是给一个场景:有N台缓存服务器,数据应该怎么分布?

最简单的是hash(key) % N。但问题在于,如果增加或减少一台服务器,索引分母变成N+1或N-1,几乎所有的key都会映射到不同节点,造成大量缓存失效。对于缓存场景来说,这等于瞬间把压力打给数据库。

一致性哈希的思路是:把整个哈希值空间看成一个环,节点和数据都哈希到环上。数据沿环顺时针查找,遇到的第一个节点就是存储节点。这样增删节点时,只有该节点相邻范围内的数据需要迁移,其他数据不受影响。

但物理节点哈希到环上通常分布不均匀,导致数据倾斜。解决办法是虚拟节点:每个真实节点对应几十到几百个虚拟节点,均匀分布在环上。这样不仅解决了倾斜,还让节点增删时的迁移范围更可控。

手写思路也不复杂。用有序映射存储哈希值到节点的映射,查找key时,找到哈希环上第一个大于等于key哈希值的节点,如果没有则回到环起点。虚拟节点就是同一个真实节点对应多个哈希值。

典型追问是:新增一台节点,哪些数据会受影响?答案是环上新增节点位置到它的前一个节点之间的数据会迁移到新节点。这个区间画出来就很直观。

4.2 副本与一致性:最终一致性和强一致性的取舍

有副本就必谈一致性。对象存储需要冗余副本防止数据丢失,但副本之间的同步方式决定了读到的数据是不是最新。

一致性模型有三个常见等级:

  • 强一致性:写操作完成后,任何读操作都能读到最新值。
  • 最终一致性:写操作完成后,系统在一段时间后达到一致。
  • 读己之写:用户写完后,自己能读到自己的写入,但其他用户可能要过一会儿。

quorum机制是理解副本一致性的钥匙。假设有N个副本,写操作需要W个副本确认,读操作需要读R个副本,只要W + R > N,读到的副本里至少有一个包含最新数据。这个W和R的取值就是一致性和可用性的权衡。

CDN场景里,缓存节点一般是最终一致的,因为内容通过缓存传播需要时间。对象存储则通常要求强一致,比如用户上传一个对象后立即可读,如果读不到,业务方会认为系统出错。2018年时七牛云对外宣传的就是上传后立即读取一致,所以笔试题会围绕这个来问。

Raft入门是另一个常考点。只需要理解三个核心概念:Leader选举、日志复制、多数派。过半节点确认后数据才算提交,这样即使部分节点故障,也能保证日志一致。笔试一般不会要求完整实现Raft,但会考“为什么需要多数派”或者“网络分区下怎么保证安全”,回答多数派和任期机制就能拿分。

4.3 CDN 相关题:命中率、回源、刷新

CDN是七牛云的核心业务,卷子里一定会出现相关场景题。最常见的切入点是缓存命中率。

假设源站文件平均大小1MB,边缘节点缓存命中率95%,用户侧总下载带宽100Gbps。这里回源带宽其实等于总带宽乘以未命中率,也就是100Gbps × 5% = 5Gbps。很多人会把回源带宽算成100Gbps,就是因为没理解命中率定义。

回源QPS也需要估算。总带宽100Gbps按字节算就是12.5GB/s,每个文件1MB,则用户请求QPS约12500。回源QPS就是12500 × 5% = 625。

缓存淘汰策略也常考。CDN边缘节点空间有限,需要决定淘汰哪些缓存文件。LRU适合访问局部性强的场景,LFU适合访问频率差异大的场景,FIFO实现最简单但效果不太理想。大多数CDN会结合访问频率和最近访问时间做加权。

还有一个隐藏考点:刷新和预热的区别。刷新是主动删除边缘节点的缓存,让下次请求回源;预热是提前把资源从源站同步到边缘节点。笔试里遇到“源站更新了一个文件,怎么让用户尽快看到新版本”,答案就是调用刷新接口,而不是等待缓存自然过期。

5. 估算题:我见过最多的翻车点,是单位换算和“忘了有副本”

估算题在卷二里占的分数不低,但很多同学看到题目就慌。其实这类题没有标准答案,面试官只看你的拆解逻辑和数量级是否合理。关键是敢算、会拆、别把单位搞错。

5.1 一道典型的存储容量估算

题目大概是这样的:某对象存储系统每天新增1亿张图片,平均大小500KB,保存3年。数据做2副本,每台存储节点裸容量12TB,估算一共需要多少台机器。

我的拆解步骤:

  1. 每天新增原始数据量:1亿 × 500KB = 50TB。
  2. 每年数据量:50TB × 365 = 18.25PB。
  3. 3年数据量:18.25PB × 3 = 54.75PB。
  4. 2副本后总存储需求:54.75PB × 2 = 109.5PB。

这里要注意,副本数是乘2,不是乘3。很多人在这一步翻车,把副本数理解成“加上原始数据”,结果多了一倍。

接下来算机器数量。12TB裸容量的机器,实际还要考虑文件系统预留、系统盘、元数据开销,一般可用容量按80%算比较合理,也就是每台可用约9.6TB。那么机器数量 = 109.5PB / 9.6TB ≈ 109500 / 9.6 ≈ 11400台。

严格来说,这里还有个单位换算陷阱:硬盘标称12TB是按十进制(12 × 10^12字节),操作系统里实际是10.9TiB(按2进制)。如果题目要求特别精确,需要区分TB和TiB。但一般估算题,写清楚假设后数量级对就能拿分。

5.2 回源带宽和QPS估算

CDN回源是一个标准估算题。题干通常会给:用户平均下载速度/总带宽、边缘节点命中率、对象平均大小,求回源带宽。

举例:用户总下载带宽100Gbps,命中率95%,平均对象大小1MB。

  • 回源带宽 = 100Gbps × (1 - 95%) = 5Gbps。
  • 用户总QPS = 100Gbps ÷ 8 ÷ 1MB = 12.5GB/s ÷ 1MB = 12500。
  • 回源QPS = 12500 × 5% = 625。

这里的核心坑是bit和byte的换算。带宽100Gbps是每秒100G比特,除以8才是字节。对象大小1MB是字节,两个单位不一致,直接除会差8倍。

5.3 估算题的通用套路

我总结了一个四步法:

  1. 列出所有已知条件,统一单位。
  2. 先算数量级,不管精确数字。
  3. 加上冗余系数,比如副本、预留空间、协议开销。
  4. 最后把结论写成范围,比如“大概10000到12000台”,并写明假设。

常见翻车点不外乎这几个:bit/byte混淆、忘记副本数、没算系统盘和文件系统预留、把QPS当成并发数。QPS是每秒请求数,并发数是同时处理的请求数,两者通过“平均响应时间”相关,并不相等。

我的习惯是算完之后做一个数量级检查。比如3年图片数据,每天50TB,三年下来肯定是几十PB级别。如果算出来只有几TB,一定是中间某个单位或副本数出了问题,回头检查一遍就会找到。

6. 复盘之后,我建议你这样准备这类笔试

6.1 知识点查漏清单

这套卷二覆盖的知识点很固定,我整理成一张自查表,准备笔试时逐个打勾:

模块必会知识点自查结果
网络TCP三次握手/四次挥手、TIME_WAIT、SYN Flood、HTTP缓存、DNS掌握 / 待复习
操作系统进程/线程/协程、内存对齐、栈堆、epoll、死锁四条件掌握 / 待复习
分布式一致性哈希、虚拟节点、副本一致性、quorum、Raft多数派掌握 / 待复习
算法TopK、LRU、海量数据求交集、字符串处理、复杂度分析掌握 / 待复习
Go/LinuxGoroutine调度、Channel、pprof、常用排查命令掌握 / 待复习

6.2 复习节奏与模拟做题

准备这种笔试,不建议上来就海量刷题。我先花一周把上面表格里的知识点过一遍,做到每个概念都能用自己的话讲出来。然后开始做模拟题,每周一到两套,严格限时两小时。

模拟时有个小技巧:把手机放另一个房间,桌面只留草稿纸和电脑,完全模拟考场环境。我发现很多错误不是不会,而是时间压力下看错条件,所以模拟状态很重要。

每道题做完后,我会在题号旁边记录实际用时,如果一道选择题超过10分钟,说明这块基础还不牢。错题用标签记录,比如“TCP”、“一致性哈希”、“单位换算”,过一周再把同标签的错题重做一遍。这种按知识点聚合的错题复盘,比反复刷整套卷子效率高很多。

6.3 笔试之后的衔接

如果笔试通过,面试官大概率会拿着你的试卷追问。比如“你LRU为什么用双向链表”“估算题里为什么副本数乘2”这类问题。所以交卷前我会把自己写的代码和关键计算步骤都存在本地,面试前再翻一遍。

如果面试中还出现系统设计题,比如“让你设计一个短视频对象存储系统”,其实考的就是卷二里那些估算法则和分布式知识点。笔试时练过的估算能力,这时候会派上大用场。

最后说一个我自己的教训。2018年准备笔试时,我把大量时间花在纯算法题上,结果卷二里操作系统和网络选择题反而失分最多。校招笔试不是比谁刷的题量更大,而是比谁的错题复盘更深。这份整理与其说是“答案”,不如说是一张查漏补缺的地图。沿着这些知识点逐个打勾,你大概率不会在卷子上被突然冒出来的冷门题打乱节奏。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询