一个常驻服务启动时把一份 100 万行的 CSV 一次性载入内存,每行解析成 dict 缓存起来(8 个字段:3 个短字符串、2 个 int、1 个 float、1 个 bool、1 个时间戳字符串)。实测进程 RSS 约 800MB,而机器只给了 1GB,跑一段时间后被 OOM Killer 干掉。你会怎么把常驻内存压下来?请说出你会选的具体方案、预估能降到多少、以及这个方案让你牺牲了什么。
面试官在考什么
判断他是否真的理解 CPython 对象模型与内存开销的来源,还是只会背 __slots__ / namedtuple 的名词;以及在机器规格这种成本约束下能否做出可落地的取舍。
回答框架 · 前两步
- 先定位开销来源:开销大头是「每行一个容器对象 + 每行一个 __dict__ + 每行若干个独立 str 对象」,而不是数据本身;一个装了 8 个 key 的 dict 加上它的 __dict__ 与 8 个字段对象,量级在几百字节/行。
- 方案一:换成 dataclass(slots=True) 或 namedtuple,去掉每行的 __dict__ 与哈希表,通常能省 30%~50%。
还有 3 条答案要点、 3 层追问、 4 个常见坑, 以及针对你简历的追问路径,在定制报告里。
同一能力域的其他题
- 你负责一个批量任务模块,函数写成 `def add_task(task, queue=[])`,内部 `queue.append(task); return queue`。测试时多次调用后发现 queue 里累积了之前所有 task,即使没有传 queue。请解释根因,并给出至少两种修复方式,说明各自适用场景。
- 你写了一个 `async def fetch_all(urls)`,里面用 `requests.get(url)` 循环请求 100 个 URL,然后 `await asyncio.gather(*[fetch_one(u) for u in urls])`。压测发现 QPS 和串行差不多,且一个慢请求会拖住其他所有请求。请解释原因,并给出两种改法;如果某个同步库没有异步替代,怎么放才不阻塞事件循环?
- 一个基于 asyncio 的 HTTP 服务,单进程单事件循环,正常 P99 是 20ms。接入一个新的下游 SDK 后,该 SDK 只有同步阻塞接口(单次调用约 50ms,内部是阻塞 socket 读)。上线后 P99 涨到 500ms,QPS 也上不去。你会怎么定位和改造?请说清你选哪种方案、线程/进程开多少、以及并发量上来之后会先崩在哪里。
- 你在 feature 分支上有 3 个本地提交,期间 main 已被同事合并了多个提交。现在要提 PR,团队要求历史整洁且不能覆盖别人的提交。你会选 rebase 还是 merge?如果 rebase 到第 2 个提交时冲突,具体怎么处理?如果这个 feature 分支已经 push 且有人基于它开了新分支,还能不能 rebase?
- 一个 10 人团队,main 分支每天合并约 30 个 PR,长期使用 squash merge。现在线上出现一个回归,怀疑是过去 3 周内约 400 个提交中的某一个引入的,需要用 git bisect 定位。你负责这次排查。请说明你现在会怎么做、这次排查会遇到什么麻烦、以及排查完你是否要改团队的分支/合并策略,依据是什么。
- 一个 Python 服务的 Dockerfile 目前是 `FROM python:3.11`、`COPY . .`、`RUN pip install -r requirements.txt`。每次只改一行业务代码,`docker build` 也要重新下载全部依赖,约 5 分钟。请调整 Dockerfile 利用层缓存;如果还要求镜像尽量小且构建可复现,基础镜像、依赖安装和启动用户分别怎么设计?