百度后端一面面经:GMP 调度、切片机制与容器隔离
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 自我介绍。
- 项目深挖(讲讲你做的项目与其中的难点)。
- 操作系统内存管理有哪些策略?
- CPU 访问内存的过程是怎样的?
- 讲一下 GMP 调度模型,P 在模型里的作用是什么?为什么一定要有 P?其中谁可以通过人为控制,P 大于、等于、小于 CPU 核心数,这三种分别适用于什么情况?
- 切片的底层机制是什么,怎么实现的?使用过程中需要注意什么问题?
- 数据库的事务隔离级别有哪些?脏读和幻读分别是什么?
- Redis 常用的数据类型有哪些?
- Redis 的主从是怎么解决数据一致性问题的,它们之间怎么同步数据?
- Docker 的 CMD 和 ENTRYPOINT 两个语法的区别?
- 讲一下 k8s 提交一个 Pod,它的创建过程会经过哪些环节?
- 创建一个容器,它依赖了 Linux 内核的哪些机制?Namespace 和 Cgroup 起什么作用?
- 算法:交换链表中的节点。
《参考解析》
内存管理与 CPU 访存:把地址翻译这条链讲通就赢了一半。操作系统内存管理的策略分几层:物理内存分配用连续分配(首次适应、最佳适应、伙伴系统)与非连续分配(分页、分段、段页式),虚拟内存用请求调页按需加载,页面置换有 FIFO、LRU、Clock 等算法,分配器层面还有 slab、buddy 和用户态的 malloc arena。CPU 访问一个虚拟地址的过程是:CPU 先查 TLB,命中直接得到物理地址;未命中则由 MMU 逐级遍历页表(x86-64 常见四级:PGD→PUD→PMD→PTE)取出物理页框号,与页内偏移拼成物理地址,并把结果回填 TLB;如果页表项显示页面不在内存或权限不符,触发缺页异常,内核负责换入页面或分配零页,再重新执行这条指令。追问通常落在页表太大(多级页表 + 大页)、TLB 抖动(工作集超过 TLB 覆盖范围)以及写时复制这几处,答的时候带上具体机制而不是只背名词。
GMP 与 P 的作用:P 不是可有可无的中间层。 G 是 goroutine,M 是内核线程,P 是逻辑处理器,它持有本地运行队列、mcache、栈分配等调度所需的资源。M 必须先绑定一个 P 才能执行 G,这是「必须有 P」的原因:一来它把可运行的 G 做了分片,减少全局队列锁竞争;二来内存分配器与调度器耦合,mcache 挂在小对象分配路径上,需要 P 提供无锁的本地缓存。P 的数量由 GOMAXPROCS 控制,可以人为设置。P 数大于 CPU 核心数:适合大量阻塞在 IO 或系统调用上的场景,能减少 M 因阻塞而导致其他 G 排队等待的情况,但上下文切换与调度开销上升,GC 扫描的栈也更多;P 数等于核心数:默认值,CPU 密集型任务吞吐最佳;P 数小于核心数:适合与其他进程争抢 CPU、或希望降低调度抖动和内存占用(每个 P 都有 mcache 和调度结构)的场景。答题时补一句工作窃取:某个 P 的本地队列空了会从全局队列或其他 P 的队列尾部偷一半任务,避免负载不均。
切片底层:它就是「指针 + 长度 + 容量」的三元组。运行时它是 SliceHeader(array、len、cap),所以切片传参只是拷贝 24 字节的头,底层数组是共享的。append 时如果容量够就原地写并返回新的头(len 加一),容量不够就申请一块更大的数组(小切片翻倍、大切片按约 1.25 倍增长),拷贝旧数据后再追加——这意味着扩容前后指向的数组可能不同。由此产生几个高频坑:函数内 append 后不返回新切片,调用方看到的还是旧长度;对同一底层数组的两个切片做修改会互相可见(s2 := s1[:2] 之后写 s2 会改到 s1 的元素);用 s1 = append(s1, x) 之后如果发生扩容,之前保存的别名切片就与它分家了。另外还有内存泄漏:从一个大切片里切出一小块长期持有,会拖住整个底层数组,解决办法是 copy 出一个独立的新切片;需要限制后续 append 覆盖原数组时可以用三索引切片 s[a:b:b]。
事务隔离与 Redis 主从:一个讲清异常,一个讲清丢失窗口。四种隔离级别的递进关系要背成因果链:读未提交会产生脏读;读已提交解决脏读,但仍可能不可重复读(同一行两次读结果不同)与幻读(同一条件两次查行数不同);可重复读解决不可重复读,MySQL 的 InnoDB 在 RR 下用 MVCC 提供一致性快照读,并用临键锁(记录锁 + 间隙锁)在「当前读」时阻止其他事务插入,从而基本消除幻读;串行化靠加锁串行执行。要分清快照读(普通 SELECT)与当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)。Redis 部分,常用数据类型是 string、list、hash、set、zset,另有 bitmap、HyperLogLog、GEO、Stream 等扩展结构。主从同步分两步:从库首次连接发起全量同步,主库 fork 出 RDB 快照加缓冲区增量数据一起发送;之后靠 repl_backlog 与复制偏移量做增量同步(PSYNC 带 offset),断线重连时若 offset 还在 backlog 内则只补差量,否则回退到全量。要注意复制默认是异步的,主库写成功不代表从库已落盘,故障切换存在丢数据的窗口,对一致性敏感的场景可以用 WAIT 命令或 Redis 集群的多数派机制,但代价是延迟。
容器与 k8s:Pod 创建是一条串行的控制链路,隔离靠内核三件套。 Pod 的创建流程可以按组件顺序说:用户 kubectl 提交 YAML → API Server 鉴权、准入控制(Admission)后把对象写入 etcd → Controller Manager 里的对应控制器补上 ReplicaSet/OwnerReference 等字段 → Scheduler 依据资源请求、亲和性、污点容忍选出节点并回写绑定信息 → 目标节点的 kubelet watch 到 Pod,调用 CRI 拉镜像、创建 Pod 沙箱(先起 pause 容器)与业务容器,调用 CNI 配网络、CSI 挂存储 → 容器启动后探针通过则更新状态。容器依赖的内核机制主要是 Namespace 与 Cgroup:Namespace 负责「看得见什么」(PID、NET、MNT、UTS、IPC、USER、CGROUP 等各自一套视图),Cgroup 负责「能用多少」(CPU 配额、内存上限与 OOM 控制、IO 带宽),再加上 capabilities、seccomp、AppArmor 做权限收敛。Docker 的 CMD 是容器启动的默认命令,会被 docker run 后面的参数整体覆盖;ENTRYPOINT 是固定入口不会被覆盖,CMD 此时作为它的默认参数被追加,所以常见组合是 ENTRYPOINT 写可执行程序、CMD 写默认参数。算法题「交换链表中的节点」用哑节点加三指针原地改指向即可,注意头节点被交换、相邻节点、只剩一个节点这几类边界。