
2018年秋招季我整理过一份百度核心系统工程师的笔试题。这套题放在今天看依然是后端岗笔试的“风向标”之一。核心系统工程师这个岗位在百度对应的是搜索引擎、大规模存储、广告检索、计算调度这些方向背后的基础设施团队笔试不考花哨的框架API反而把操作系统、网络、数据结构这些“压箱底”的基础翻来覆去地考。这篇文章我尽量按当年参加笔试的同学回忆和岗位常见考察方向把题目类型、典型题目和完整解题思路做一次复盘同时把每类题背后出题人真正想考察的能力点拆开讲。适合正在准备大厂后端岗、基础架构岗校招或实习笔试的人参考也适合工作了几年想回头补一补系统基础的在职工程师。你不用把它当成“真题答案”背把它当成一份“系统基础能力自查清单”更有价值。1. 这份试卷的出题逻辑核心系统工程师到底在挑什么人1.1 岗位画像既要懂业务更要懂“业务脚下踩的这层土”核心系统工程师和普通后端开发的最大区别在于工作对象。普通后端关注订单、用户、内容这些业务对象核心系统工程师关注的是支撑所有业务对象的那层基础设施——集群调度、存储引擎、缓存中间件、消息队列、负载均衡。在百度这类大厂这个岗位还会涉足检索集群的分配、索引数据的更新链路、大规模机器学习训练的资源调度等场景。这类岗位的工作性质决定了你写的每一行代码可能都运行在上万台服务器上任何一个低级错误都可能被放大成一次大规模故障。所以面试官和出题人最看重的不是你“会多少框架”而是你有没有能力在底层系统出问题时快速定位根因。这份笔试的定位本质上是“基础能力压力测试”业务知识可以入职后再补但操作系统、网络、算法这些童子功必须在笔试阶段就过关。1.2 题目的大致分布与答题重心根据还原的试卷结构整套题大致分三块题型大致占比考察内容答题侧重点选择题约35%-40%操作系统、网络、数据结构选择题概念辨析细节陷阱多问答题约35%-40%TCP状态分析、内存管理、分布式方案思路链完整有推导过程算法/手写题约20%-30%海量数据处理、数据结构设计手写核心逻辑复杂度要明确问答题是拉开差距的地方改卷时看的是推导过程而不是标准答案。举个例子同样是问“进程和线程的区别”低分答案通常是“进程是资源分配单位线程是CPU调度单位”一句话。高分答案却会展开成三条线资源隔离地址空间、文件描述符表、上下文切换成本页表切换、TLB失效、内核态陷入、通信方式进程间IPC vs 线程共享内存并且能说出“为什么线程更轻量”的底层机制。这种差异就是笔试想要筛选出的“真懂”和“背过”的区别。2. 操作系统与Linux底子扎不扎实几道题就露馅2.1 进程、线程、协程切换开销这道送分题里藏着陷阱选择题里有一道高频题下列并发模型中上下文切换开销最小的是哪个选项有进程、线程、协程、裸机中断。很多人一看“协程”就秒选这是对的但如果你只选对了答案却说不出为什么下一轮面试就会露馅。协程为什么切换开销小因为它的调度完全发生在用户态切换时只需要保存和恢复少量寄存器不需要陷入内核态也不需要切换页表。页表切换是一个开销极高的操作因为CPU的TLB缓存会跟着失效后续的地址翻译全部要走内存中的页表项这一下就是几百个周期。但协程也有一套“隐形代价”很少人提如果某个协程在内部执行了阻塞式IO它会卡住整个线程里的所有其他协程。所以生产环境下使用协程核心前提是底层IO必须是非阻塞的配合事件循环来驱动这也是Go语言运行时把网络IO做成非阻塞模型的原因。笔试里如果你能在答案结尾补一句“协程在阻塞IO场景下会拖垮整个线程”这道题基本就是满分表达了。2.2 页面置换算法一道手算题的完整推导过程操作系统另一个高频考点是页面置换算法。这里最常见的是给一串访问序列要求分别用FIFO和LRU求出缺页次数。原题大概是这么个形式访问序列1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5物理块数为3。FIFO的完整手算过程如下初始内存为空1、2、3依次装入产生3次缺页访问4内存已满且4不在淘汰最早进入的1缺页第4次内存变为4,2,3访问1缺页淘汰2内存变为4,1,3访问2缺页淘汰3内存变为4,1,2访问5缺页淘汰4内存变为5,1,2访问1、2命中访问3缺页淘汰1内存变为5,3,2访问4缺页淘汰2内存变为5,3,4访问5命中结束总计缺页次数9次。LRU按最近最久未使用淘汰1、2、3装入缺页3次访问4缺页淘汰1因为1最久没被用内存变为4,2,3访问1缺页淘汰2内存变为4,1,3访问2缺页淘汰3内存变为4,1,2访问5缺页淘汰4内存变为5,1,2访问1、2命中访问3缺页淘汰5内存变为3,1,2访问4缺页淘汰1内存变为3,4,2访问5缺页淘汰2内存变为3,4,5总计缺页次数10次。这个题目本身不难但有一个隐藏加分点FIFO有一种称为Belady异常的现象——增加物理块数后缺页次数反而增多而LRU属于栈算法不会出现Belady异常。笔试答题时只要把这句话写上阅卷人就知道你学操作系统不是只背了结论而是理解过算法性质。另一个加分点是在LRU实现层面提一句“真正的LRU代价高工业界常用CLOCK算法作为近似实现”这一句在面试里尤其好使。2.3 给你一台不健康的机器Linux排查类问题的回答模板这套笔试题还有一类典型问答题机器负载升高怎么排查这类题没有标准答案但回答的层次感就是分数。我的回答模板是“由顶层到底层由表象到根因”层次如下先用top看整体负载和CPU使用率确认是CPU繁忙还是IO等待。top输出里有load average三个数值分别对应1分钟、5分钟、15分钟的负载。如果15分钟负载很高但1分钟负载不高说明系统可能正在恢复反之说明问题刚刚发生。CPU用满的话用ps -eo pid,ppid,cmd,%cpu --sort-%cpu | head按CPU使用率排序找进程。找到进程后再用top -Hp pid查看进程内的线程因为Java或者C多线程程序出现CPU飚高通常只有一个或几个线程在疯狂占核。如果进程层面看不出异常用strace -p pid跟踪系统调用。我曾经在排查一台异常机器时发现某个线程每秒执行了几十万次无意义的futex系统调用问题立刻定位到锁竞争。内存不足的话free -g看整体内存再用cat /proc/meminfo细看Slab、PageTables这些特殊内存。PageTables很高说明进程页表本身占据了大量内存通常是进程虚拟地址空间太大造成的。磁盘IO饱和时iostat -x 1看%util和await等待时间长了基本就是磁盘硬件或IO调度队列拥堵。这套排查思路值得笔试前多练几遍。核心系统工程师的日常工作很大一部分就是“接故障、看指标、定位根因”考这个方向完全在情理之中。答题时哪怕不会看具体指标能说出“从系统整体指标出发逐层缩小范围最终定位到进程或线程”这个方法论也胜过写一堆碎命令。3. 网络与分布式系统百度这类“大规模”场景怎么考3.1 三次握手与TIME_WAIT别只背状态机要能解决线上问题网络部分的题目不会只让你画三次握手图而是会把TCP状态机放到一个具体故障里。典型问法是线上服务器出现大量TIME_WAIT连接是什么原因如何解决先明确一点进入TIME_WAIT的一定是主动关闭连接的一方。HTTP短连接场景下服务器如果主动断开连接就会积累大量TIME_WAIT。TIME_WAIT存在的两个理由一是确保最后一个ACK能到达对端如果ACK丢失对端会重发FIN此时TIME_WAIT状态还能让对方的重发得到回应二是防止旧连接的报文段出现在新连接中2MSL时间足够让网络中本连接的所有报文段自然消失。大量TIME_WAIT本身不一定会导致故障真正的问题在于TIME_WAIT数量过多时会占用大量连接表项可能导致新连接无法建立。常用的优化手段包括开启端口复用、长连接替代短连接、调整net.ipv4.tcp_max_tw_buckets上限。但这里有一个2018年时很多资料还在推荐、后来被证伪的操作——net.ipv4.tcp_tw_recycle。这个参数在NAT环境下会引发严重问题比如同一个NAT出口后面的多台内网机器连同一个服务端时时间戳校验会导致部分连接被丢弃现在的内核版本已经不支持开启了。你在笔试里如果能主动指出“recycle参数已被废弃不要用”阅卷人会立刻觉得你是真在线上环境摸爬过的人。3.2 一致性哈希为什么分布式缓存不能简单取模分布式系统题目里一致性哈希几乎是必考项。场景往往是这样有N台缓存服务器怎么决定一个key落在哪台机器上最朴素的思路是hash(key) % N。这种方式实现简单但有一个致命问题当节点数从N变成N1或者某个节点宕机时绝大多数key的取模结果都会变化这会导致缓存大面积失效请求全部穿透到数据库也就是常说的“缓存雪崩”。一致性哈希的做法是把哈希值空间组织成一个环范围和普通哈希一致服务节点和数据key都映射到环上的一个位置数据顺时针找到最近的节点存储。这样当某个节点宕机受影响的只有该节点到逆时针上一个节点之间的那段数据其余数据不受影响。但简单一致性哈希有一个问题节点少时数据分布极度不均匀可能出现一台机器扛了80%流量。解决办法是引入虚拟节点每个物理节点在环上映射出上百个虚拟位置。虚拟节点的好处不只是均匀性还能在某个物理节点故障时把它的数据比较均匀地分摊到其他节点上而不至于把压力集中打给下一个节点。笔试答题时画出环结构、说明数据定位规则、点出虚拟节点解决倾斜问题这三点齐全就是完整答案。这类题背后其实是在考察你设计分布式存储或缓存系统时有没有考虑过“节点故障后的流量冲击”这在百度的检索集群和存储集群里是每天都要面对的真实问题。3.3 缓存穿透、击穿、雪崩一套组合拳怎么打这道题在问答题里出现概率极高。推荐先给出定义再给解决方案缓存穿透是查询一个必然不存在的数据。缓存和数据库里都没有这条记录每次请求都直接打到数据库。攻击者可以利用这点对系统发起“空查询”攻击。解法有两个方向一是布隆过滤器在请求进入缓存前先过滤掉“一定不存在”的key二是缓存空值把不存在的key也写进缓存但需要设置一个较短的过期时间比如5分钟防止大量空值占满缓存。空值缓存有个新问题某个key旧数据被删除后空值缓存可能导致新写入的数据短期内不可见。所以更稳妥的做法是写操作发生时主动删除缓存中的空值。缓存击穿是某个热点key过期瞬间大量并发请求同时打到数据库。解法是互斥锁同一时刻只允许一个请求去数据库加载并重建缓存其余请求等待旧缓存过期或自旋重试。工程实现上可以用Redis的SETNX抢锁也可以在单机内用编程语言的锁原语。缓存雪崩是大量key同时过期或缓存集群整体不可用导致压力直接打到数据库。解法包括把过期时间加上随机值比如设置TTL为600秒时实际写600random(0,300)秒、走多级缓存、设置限流降级策略。2018年笔试考这套题放在现在的后端面试依然没有过时因为防雪崩本质上是一个系统设计问题不是某一个单一方案能解决的。给出的答案建议按“场景定义-危害-解决方案-方案权衡”这条线来写不要一上来就抛术语。4. 海量数据算法题不是LeetCode是“给你10亿个整数内存1GB”4.1 TopK问题分治Hash小顶堆的经典组合笔试里的算法题往往不是标准LeetCode风格而是把数据规模拉到一个“内存装不下”的级别。典型题目是给定10亿个32位整数找出其中最大的100个数。内存限制1GB。第一步要明确10亿个32位整数大约是4GB内存全局排序不可能。主流解法分三步分片用一个哈希函数hash(x) % 1000把10亿个整数均匀分散到1000个小文件中。哈希分片的意义在于相同哈希值的数字一定会进入同一个文件而且每个文件大小约4MB完全放得进内存。求解局部TopK对每个小文件维护一个大小为100的小顶堆。遍历文件中的每个数字如果数字比堆顶大就替换堆顶并调整堆。所有数字遍历完后堆里保留的就是这个文件的Top100。为什么用小顶堆而不是大顶堆因为小顶堆的堆顶是当前100个候选里的最小值判断新元素有没有资格进入Top100只需要和堆顶比较。大顶堆的堆顶是最大值无法直接判断新元素是强是弱。归并1000个小文件的Top100总共有10万个数字仍然放不进内存吗其实10万个数字只有几百KB完全可以一次性读入再建一个大小为100的大顶堆或者重复“TopK淘汰”的过程最终得到全局Top100。这个方案的复杂度是O(n log k)n是数据总量k是TopK的K空间复杂度是O(k 分片数)。笔试里写出复杂度后再加一句“这种分治方法天然适合用MapReduce表达Map阶段做哈希分片Reduce阶段做局部TopK”直接向出题人展示了系统级思维。4.2 布隆过滤器用少量内存判断“一定不在”布隆过滤器和海量数据处理经常一起考。题目通常长这样现在需要维护一个URL黑名单里面有上亿条记录要求判断一个新的URL是否在黑名单中内存只给你几百MB。哈希表放不下怎么做布隆过滤器Bloom Filter的答案是标准解用一个长度为m的位数组配合k个哈希函数。插入时对元素计算k个哈希值把位数组中对应位置的bit置1查询时同样计算k个哈希值只要其中任何一个bit为0说明元素一定不在集合中如果k个bit全部为1只能说明“可能在”存在一定误判率。误判率怎么算给定位数组长度m、哈希函数个数k、插入元素数量n误判率约为f (1 - e^{-kn/m})^k最优的哈希函数个数是k (m/n) × ln2。举例来说如果误判率容忍到1%n 10亿条URL需要的位数组长度约为9.6×10^9 bit换算成字节大约是1.2GB。如果只做黑名单这种低频写入、高频查询的场景1.2GB换来10亿条记录的“存在性判断”性价比极高。如果误判率放宽到5%内存可以压缩到约800MB左右。答题时还有个容易忽略的点布隆过滤器不支持删除元素。因为多个元素可能映射到同一个bit直接清零会误伤其他元素。如果需要支持删除要用Counting Bloom Filter——把bit扩展成计数器删除时做减1操作。笔试把这个扩展方案写出来说明你不仅知道用法还理解局限性。4.3 外部排序与MapReduce思想另一类常见题目是给定10亿条带权重的记录内存只有2GB如何按权重排序或者统计10亿个URL中每个URL的出现次数输出出现次数最多的100个URL。第一种题的解法是外部排序。思路是把大文件拆分成能装进内存的多个小文件每个小文件内部用快排排好序然后做多路归并。归并时维护k个文件的指针每次从k个文件中取出当前最小的记录输出到结果文件。这里可以用败者树或小顶堆优化k路归并过程堆的大小就是k每次取出最小值时间复杂度O(log k)。外部排序的磁盘IO访问次数取决于“原数据大小/内存大小”的轮数内存给得越大需要扫描磁盘的轮数就越少。第二种题的解法更贴近大数据处理读入每条URL通过哈希函数写到N个小文件中同一个URL一定进入同一个文件。再对每个小文件做词频统计统计时用哈希表或字典树。最后整体归并得到Top100。这其实就是MapReduce的完整思想Map阶段负责拆分和分组Shuffle阶段负责把相同key的数据汇合Reduce阶段负责局部聚合。笔试写到“按哈希拆分保证相同key落在同一分区”时出题人就知道你确实理解过大规模数据分散计算的原理而不是只会背MapReduce三个阶段的单词。5. 一道系统设计题的完整拆解短URL服务从读题到交卷5.1 题目还原与需求指标拆解笔试最后往往有一道系统设计题。这里按同类方向还原一道代表性题目设计一个短URL服务要求短码长度不超过8位写入QPS约1000读取QPS约10万要支持链接过期时间。读题后先别急着写方案按“读多写少、容量估算、可用性要求”三个维度拆需求。写入1000 QPS意味着每天新增约8640万条短链一年就是300亿条以上。这个量级说明数据库分库分表是必须的。读取10万 QPS说明数据库无法直接扛住读流量必须前置缓存。短码长度8位如果采用大小写字母加数字共62个字符8位短码的理论空间是62^8约218万亿足够使用了。5.2 短码生成方案对比为什么发号器是主流三种常见方案我逐一对比过哈希截断方案对长URL取MD5再截取前8位作为短码。致命问题是哈希冲突一旦冲突就要重算或者加盐再算而且短码没有规律无法反推出生成顺序对数据冷热分层不友好。随机串方案直接随机生成8位字符串每次生成后都要去数据库查一次是否已存在。高并发下冲突概率不低而且存在“恰好生成不了可用短码”的极端情况。发号器方案用一个全局自增ID再把十进制ID转换成62进制字符串比如ID12345转成62进制就是3D7。这是我最推荐的方案生成的短码天然全局唯一、不需要查重、长度可控还能通过ID区间倒推生成时间方便做冷热数据归档。十进制转62进制的核心代码用Python写就十几行chars 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ base len(chars) def id_to_short_code(seq: int) - str: if seq 0: return chars[0] result [] while seq 0: seq, remainder divmod(seq, base) result.append(chars[remainder]) return .join(reversed(result)).rjust(8, 0) def short_code_to_id(code: str) - int: seq 0 for ch in code: seq seq * base chars.index(ch) return seq发号器在分布式环境下的高可用是重点考察点。单机自增ID一旦机器重启要么ID重复要么直接丢号。通常做法是预先取号段发号服务每次从数据库取一个区间比如[10000, 19999]内存中分配完再取下一段。这样即使某台发号机崩溃最多丢失当前未分配完的号段不会重复。双机房部署时可以让机器A使用偶数号段、机器B使用奇数号段两套发号互不干扰。这个方案我把每一次写出给面试官时对方都会追问“取号段时数据库并发更新怎么保证不冲突”答案是UPDATE ... SET current_max current_max step WHERE id ?这种原子自增语句天然具备冲突保护。5.3 重定向、缓存与容灾设计短URL服务的核心路径是重定向用户访问短链接服务端返回302跳转到长URL。这里有一个容易被忽略的细节301和302的选择。301是永久重定向浏览器会缓存结果后续请求不再访问短链服务好处是服务端压力小坏处是你无法统计真实点击量。302是临时重定向每次点击都会到短链服务可以做点击分析和风险控制。绝大多数商业短链服务都会选择302搜索引擎收录场景另说。笔试里我建议把“301 vs 302”的回答要点写清楚因为这道题既考HTTP协议状态码语义也考业务理解。读路径设计成三级结构本地缓存如LRU Map容量设置约几十万条命中率能覆盖很大一部分热点流量Redis集群承载绝大部分读请求Key是短码Value是长URLMySQL分库分表作为持久化兜底Redis未命中时才查询写路径则采用异步组装写入时生成短码、写入数据库、更新缓存。为了防缓存击穿热点短码过期时用SETNX加锁只放一个请求去数据库加载其他请求短暂等待后重试。为了防热点数据集中过期短码对应的Redis TTL也可以加随机偏移。这套设计能不能支撑题目要求的10万 QPS做一个粗略估算Redis单实例读性能通常在8万到12万 QPS之间10万 QPS的读量用两到三个Redis分片完全够用。写入1000 QPS落到MySQL单库单表比较吃力但分库分表后每张表每秒只承担几十次插入非常轻松。最后加上一层本地缓存分流Redis的压力还能再降一个量级。答题时把数据算给阅卷人看比定性描述“要用缓存”有说服力得多。考完这套题再回头想它最值钱的地方其实是“把分散的知识点串成一条线”。操作系统、网络、算法、分布式设计每一块单独拿出来都能背难的是在有限时间里快速识别题目在考哪个维度的能力然后组织出有推导过程、有方案取舍的答案。这也是大厂校招笔试从“选拔知识量”转向“选拔系统思维”的一个明显信号。我个人准备笔试时走过的弯路是前期刷了太多LeetCode忽略了问答题。后来才发现选择题和算法题大家差距不大真正决定过不过的往往是那几道问答题——因为它是唯一能展现你遇到过真实问题、思考过方案权衡的窗口。最后分享一个小技巧备考期间可以把每一类问答题写成“背景-原因-方案-权衡”四段式答案然后对着手机录音讲一遍。能讲得清楚的问题上考场基本都能写得出来讲不清楚的恰恰就是你需要补的盲区。这套笔试考过的知识点差不多也是现在大厂基础架构岗面试还在反复追问的东西花几天时间把它吃透怎么都不亏。