☰
百度核心系统工程师笔试题复盘:从操作系统到海量数据
2026/10/23 22:53:40 网站建设 项目流程

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排查类问题的回答模板

这套笔试题还有一类典型问答题:机器负载升高,怎么排查?这类题没有标准答案,但回答的层次感就是分数。

我的回答模板是“由顶层到底层,由表象到根因”,层次如下:

  1. 先用top看整体负载和CPU使用率,确认是CPU繁忙还是IO等待。top输出里有load average三个数值,分别对应1分钟、5分钟、15分钟的负载。如果15分钟负载很高但1分钟负载不高,说明系统可能正在恢复;反之说明问题刚刚发生。

  2. CPU用满的话,用ps -eo pid,ppid,cmd,%cpu --sort=-%cpu | head按CPU使用率排序找进程。找到进程后,再用top -Hp <pid>查看进程内的线程,因为Java或者C++多线程程序出现CPU飚高,通常只有一个或几个线程在疯狂占核。

  3. 如果进程层面看不出异常,用strace -p <pid>跟踪系统调用。我曾经在排查一台异常机器时,发现某个线程每秒执行了几十万次无意义的futex系统调用,问题立刻定位到锁竞争。

  4. 内存不足的话,free -g看整体内存,再用cat /proc/meminfo细看Slab、PageTables这些特殊内存。PageTables很高说明进程页表本身占据了大量内存,通常是进程虚拟地址空间太大造成的。

  5. 磁盘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变成N+1,或者某个节点宕机时,绝大多数key的取模结果都会变化,这会导致缓存大面积失效,请求全部穿透到数据库,也就是常说的“缓存雪崩”。

一致性哈希的做法是把哈希值空间组织成一个环,范围和普通哈希一致,服务节点和数据key都映射到环上的一个位置,数据顺时针找到最近的节点存储。这样当某个节点宕机,受影响的只有该节点到逆时针上一个节点之间的那段数据,其余数据不受影响。但简单一致性哈希有一个问题:节点少时数据分布极度不均匀,可能出现一台机器扛了80%流量。解决办法是引入虚拟节点,每个物理节点在环上映射出上百个虚拟位置。虚拟节点的好处不只是均匀性,还能在某个物理节点故障时,把它的数据比较均匀地分摊到其他节点上,而不至于把压力集中打给下一个节点。

笔试答题时,画出环结构、说明数据定位规则、点出虚拟节点解决倾斜问题,这三点齐全就是完整答案。这类题背后其实是在考察你设计分布式存储或缓存系统时有没有考虑过“节点故障后的流量冲击”,这在百度的检索集群和存储集群里是每天都要面对的真实问题。

3.3 缓存穿透、击穿、雪崩:一套组合拳怎么打

这道题在问答题里出现概率极高。推荐先给出定义,再给解决方案:

缓存穿透是查询一个必然不存在的数据。缓存和数据库里都没有这条记录,每次请求都直接打到数据库。攻击者可以利用这点对系统发起“空查询”攻击。解法有两个方向:一是布隆过滤器,在请求进入缓存前先过滤掉“一定不存在”的key;二是缓存空值,把不存在的key也写进缓存,但需要设置一个较短的过期时间(比如5分钟),防止大量空值占满缓存。空值缓存有个新问题:某个key旧数据被删除后,空值缓存可能导致新写入的数据短期内不可见。所以更稳妥的做法是,写操作发生时主动删除缓存中的空值。

缓存击穿是某个热点key过期瞬间,大量并发请求同时打到数据库。解法是互斥锁,同一时刻只允许一个请求去数据库加载并重建缓存,其余请求等待旧缓存过期或自旋重试。工程实现上可以用Redis的SETNX抢锁,也可以在单机内用编程语言的锁原语。

缓存雪崩是大量key同时过期或缓存集群整体不可用,导致压力直接打到数据库。解法包括把过期时间加上随机值(比如设置TTL为600秒时,实际写600+random(0,300)秒)、走多级缓存、设置限流降级策略。2018年笔试考这套题,放在现在的后端面试依然没有过时,因为防雪崩本质上是一个系统设计问题,不是某一个单一方案能解决的。

给出的答案建议按“场景定义-危害-解决方案-方案权衡”这条线来写,不要一上来就抛术语。

4. 海量数据算法题:不是LeetCode,是“给你10亿个整数,内存1GB”

4.1 TopK问题:分治+Hash+小顶堆的经典组合

笔试里的算法题往往不是标准LeetCode风格,而是把数据规模拉到一个“内存装不下”的级别。典型题目是:给定10亿个32位整数,找出其中最大的100个数。内存限制1GB。

第一步要明确,10亿个32位整数大约是4GB内存,全局排序不可能。主流解法分三步:

  1. 分片:用一个哈希函数hash(x) % 1000把10亿个整数均匀分散到1000个小文件中。哈希分片的意义在于,相同哈希值的数字一定会进入同一个文件,而且每个文件大小约4MB,完全放得进内存。

  2. 求解局部TopK:对每个小文件,维护一个大小为100的小顶堆。遍历文件中的每个数字,如果数字比堆顶大,就替换堆顶并调整堆。所有数字遍历完后,堆里保留的就是这个文件的Top100。为什么用小顶堆而不是大顶堆?因为小顶堆的堆顶是当前100个候选里的最小值,判断新元素有没有资格进入Top100,只需要和堆顶比较。大顶堆的堆顶是最大值,无法直接判断新元素是强是弱。

  3. 归并: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进制字符串,比如ID=12345,转成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协议状态码语义,也考业务理解。

读路径设计成三级结构:

  1. 本地缓存(如LRU Map),容量设置约几十万条,命中率能覆盖很大一部分热点流量
  2. Redis集群,承载绝大部分读请求,Key是短码,Value是长URL
  3. MySQL分库分表,作为持久化兜底,Redis未命中时才查询

写路径则采用异步组装:写入时生成短码、写入数据库、更新缓存。为了防缓存击穿,热点短码过期时用SETNX加锁,只放一个请求去数据库加载,其他请求短暂等待后重试。为了防热点数据集中过期,短码对应的Redis TTL也可以加随机偏移。

这套设计能不能支撑题目要求的10万 QPS?做一个粗略估算:Redis单实例读性能通常在8万到12万 QPS之间,10万 QPS的读量用两到三个Redis分片完全够用。写入1000 QPS落到MySQL,单库单表比较吃力,但分库分表后每张表每秒只承担几十次插入,非常轻松。最后加上一层本地缓存分流,Redis的压力还能再降一个量级。答题时把数据算给阅卷人看,比定性描述“要用缓存”有说服力得多。

考完这套题再回头想,它最值钱的地方其实是“把分散的知识点串成一条线”。操作系统、网络、算法、分布式设计每一块单独拿出来都能背,难的是在有限时间里快速识别题目在考哪个维度的能力,然后组织出有推导过程、有方案取舍的答案。这也是大厂校招笔试从“选拔知识量”转向“选拔系统思维”的一个明显信号。

我个人准备笔试时走过的弯路是:前期刷了太多LeetCode,忽略了问答题。后来才发现,选择题和算法题大家差距不大,真正决定过不过的往往是那几道问答题——因为它是唯一能展现你遇到过真实问题、思考过方案权衡的窗口。最后分享一个小技巧:备考期间可以把每一类问答题写成“背景-原因-方案-权衡”四段式答案,然后对着手机录音讲一遍。能讲得清楚的问题,上考场基本都能写得出来;讲不清楚的,恰恰就是你需要补的盲区。这套笔试考过的知识点,差不多也是现在大厂基础架构岗面试还在反复追问的东西,花几天时间把它吃透,怎么都不亏。

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

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

立即咨询