
每年校招季总有人翻出往年的真题做复盘2018七牛云校招笔试题卷二就是其中一套被反复讨论的卷子。我当年准备云存储和基础架构方向时把能找到的存储/CDN类笔试都刷了一遍这套卷二给我的印象最深选择题比编程题更让人心虚分布式系统题直接决定你能不能进下一轮。这篇文章我就结合当时的笔记和后来的工作理解把整套卷子的考点、典型题、估算思路完整梳理一遍给准备校招、想投云存储/基础架构/Go开发方向的同学做参考。这套卷子的价值不在题目本身而在它背后的命题逻辑。七牛云是做对象存储和CDN起家的技术栈以Go为主核心产品面向海量数据和高并发场景。所以卷二不像普通校招卷那样只考算法刷题它更关注你有没有分布式系统的直觉懂不懂网络和操作系统底层能不能在边界条件模糊的情况下把估算题算得合理。下面我从题型分布开始逐块拆解。1. 为什么这套“卷二”值得单独复盘先看懂七牛云在找什么人1.1 一卷和二卷的定位差异很多公司校招笔试会拆成两张卷子卷一通常是公共卷考数据结构、算法、语言基础所有岗位共用卷二则是方向卷更贴近具体业务和岗位要求。七牛云的卷一和卷二就是这么分工的卷一筛选的是“基础够不够扎实”卷二筛选的是“能不能做分布式存储/CDN相关的事”。从命题逻辑反推如果公司主要用Go语言核心产品是对象存储、CDN调度、数据处理管道那笔试就应该重点考这些领域的基本功。卷二里有不少题看起来是“常识题”比如TCP的三次握手、进程和线程的区别但真正拉开差距的是后面的分布式一致性哈希、副本策略、容量估算。它不要求你达到架构师水平但要求你能把“为什么这样做”讲清楚而不是只背结论。1.2 卷二涉及的主要模块根据我的回忆和整理这一卷的考点大概可以分成五个模块计算机网络TCP/UDP、HTTP、DNS、CDN原理操作系统进程/线程/协程、内存布局、IO模型分布式基础一致性哈希、副本、一致性模型、Raft入门算法与数据结构海量数据处理、LRU、TopKGo语言与Linux基础Goroutine、Channel、常用命令这里有个隐形的门槛如果你完全没有接触过Go看到并发编程题时可能会懵。但七牛云的笔试不是专门考Go语法它考的是并发思维。只要能讲清楚线程和协程的区别理解Goroutine的调度模型用伪代码写也没太大问题。1.3 时间分配策略我记得这套卷子题量看起来不大大概十几道题但真正做起来时间很紧。选择题和填空题看着简单一犹豫就过去好几分钟后面的估算题需要写计算过程编程题要写完整代码都是吃时间的环节。我当时的策略是先把有把握的题做完尤其是选择题和填空题保证基础分。再做编程题先写思路框架再补边界条件。最后做系统估算题这类题没有唯一答案只要步骤清晰、假设合理就能拿大部分分。单题卡住超过15分钟果断跳过不要恋战。一个很现实的点笔试成绩不只看你答对多少还看你的答题策略。如果空题太多面试官会怀疑你的时间管理能力。2. 选择题里那些“好像会但一选就错”的考点TCP、并发与内存2.1 TCP 三次握手和四次挥手没你想的那么简单TCP是网络部分雷打不动的考点。三次握手的过程大家都会背客户端发SYN服务端回SYNACK客户端再回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)k1000时几乎等于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, reverseTrue)海量数据变体会说内存装不下所有数据。解决办法是哈希分片按某种规则把数据分到多个桶每个桶分别取TopK最后再合并。这里要留意分片要均匀否则某个桶数据量过大内存还是不够。3.2 手写 LRU CacheLRULeast 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文件50GBB文件50GB内存只有8GB怎么找出两个文件里相同的字符串最容易想到的方案是哈希分片。对A和B的每一行分别计算hash按hash值取模分成多个小文件比如分成100份。然后分别对比A_0和B_0A_1和B_1以此类推。每一对小文件大小约500MB可以加载进内存做精确交集。外部排序加归并是另一个方案先对两个文件分别外部排序再用双指针做归并求交集。排序的磁盘IO比哈希分片大但哈希函数需要均匀否则分片后数据倾斜会拖慢速度。布隆过滤器也可以用来做预筛把A的hash值写入布隆过滤器然后遍历B输出可能存在的数据。但布隆过滤器有误判率会把不存在的key也放进来所以只能用来过滤掉大部分不存在的数据最后还需要精确验证。方案内存磁盘IO准确性哈希分片低高精确外部排序归并低更高精确布隆过滤器低低有误判笔试里只需写出方案和复杂度但在实际系统中哈希分片是最常用的。3.4 编程题到底怎么判分这个点很多同学不知道对外存储公司来说编程题判分不只看能不能跑通更看复杂度分析和边界条件。面试官拿到试卷后会重点看几处是否有空输入、单元素、重复元素的处理时间复杂度和空间复杂度是否写清楚代码里有没有硬编码数据结构的选用是否合理我在卷子上有个习惯写完代码后在代码块末尾用注释写上时间复杂度和空间复杂度。这既是给面试官看的也是帮自己理清思路。边界条件更要主动覆盖比如LRU的容量为0或1TopK的k大于数组长度都要考虑到。4. 分布式系统题卷二真正拉开差距的地方如果说前三部分是基础和算法那分布式部分就是七牛云这种存储公司的“主菜”。卷二在这块大概会出两三道题覆盖面主要是一致性哈希、副本与一致性、CDN场景。4.1 一致性哈希从“为什么有它”到“怎么实现”一致性哈希几乎是所有分布式缓存和存储系统的必考题。面试官通常不会只问概念而是给一个场景有N台缓存服务器数据应该怎么分布最简单的是hash(key) % N。但问题在于如果增加或减少一台服务器索引分母变成N1或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亿 × 500KB 50TB。每年数据量50TB × 365 18.25PB。3年数据量18.25PB × 3 54.75PB。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 估算题的通用套路我总结了一个四步法列出所有已知条件统一单位。先算数量级不管精确数字。加上冗余系数比如副本、预留空间、协议开销。最后把结论写成范围比如“大概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年准备笔试时我把大量时间花在纯算法题上结果卷二里操作系统和网络选择题反而失分最多。校招笔试不是比谁刷的题量更大而是比谁的错题复盘更深。这份整理与其说是“答案”不如说是一张查漏补缺的地图。沿着这些知识点逐个打勾你大概率不会在卷子上被突然冒出来的冷门题打乱节奏。