你写了一个 `async def fetch_all(urls)`,里面用 `requests.get(url)` 循环请求 100 个 URL,然后 `await asyncio.gather(*[fetch_one(u) for u in urls])`。压测发现 QPS 和串行差不多,且一个慢请求会拖住其他所有请求。请解释原因,并给出两种改法;如果某个同步库没有异步替代,怎么放才不阻塞事件循环?
面试官在考什么
判断是否理解事件循环、阻塞调用与异步并发的本质,以及能否在不能替换同步库时正确卸载。
回答框架 · 前两步
- `requests.get` 是同步阻塞调用,在事件循环线程执行会阻塞整个 loop,导致其他任务无法调度,表现为串行。
- 改法一:用异步 HTTP 客户端如 aiohttp/httpx.AsyncClient,真正让出事件循环。
还有 3 条答案要点、 3 层追问、 4 个常见坑, 以及针对你简历的追问路径,在定制报告里。
同一能力域的其他题
- 你负责一个批量任务模块,函数写成 `def add_task(task, queue=[])`,内部 `queue.append(task); return queue`。测试时多次调用后发现 queue 里累积了之前所有 task,即使没有传 queue。请解释根因,并给出至少两种修复方式,说明各自适用场景。
- 一个常驻服务启动时把一份 100 万行的 CSV 一次性载入内存,每行解析成 dict 缓存起来(8 个字段:3 个短字符串、2 个 int、1 个 float、1 个 bool、1 个时间戳字符串)。实测进程 RSS 约 800MB,而机器只给了 1GB,跑一段时间后被 OOM Killer 干掉。你会怎么把常驻内存压下来?请说出你会选的具体方案、预估能降到多少、以及这个方案让你牺牲了什么。
- 一个基于 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 利用层缓存;如果还要求镜像尽量小且构建可复现,基础镜像、依赖安装和启动用户分别怎么设计?