拼多多服务端开发二面:Go 并发调度、知识库架构与 Agent 平台的不足
- 轮次
- 二面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 自我介绍。
- 详细介绍实习项目的业务目标。
- 这里提到并发,有什么特殊设计吗?
- 为什么下游 QPS 受限?你觉得是因为什么原因?
- Go 并发原理,或者说底层调度原理是什么?
go funcA 调用go funcB,它们在同一个队列吗?怎么判断?- 在同一队列有什么好处?
- 知识库怎么搭建的,什么架构?
- 知识库的调用链路是怎样的?
- 你觉得你们的 Agent 平台相比较于市场比较成熟的产品,有什么不足?
- 算法:数组中的数能否被等分成四组。
《参考解析》
Go 的并发调度要讲成 GMP 的分工,而不是「协程很轻」这一句。 G 是 goroutine(用户态任务)、M 是内核线程、P 是处理器上下文(本地可运行队列加调度所需的资源),同一时刻只有拿到了 P 的 M 才能真正执行 G;新创建的 goroutine 优先放进当前 P 的本地队列(runnext 槽位最优先,然后才是本地队列尾部),本地队列满了会批量挪一半到全局队列;P 空闲时先从全局队列取,再向其它 P 偷(work stealing);遇到系统调用、channel 阻塞、time.Sleep 这类会让出执行权的操作时,G 被挂起并放回队列,M 与 P 解绑或另找一个 M 继续跑,网络 IO 则由 netpoller 配合调度器唤醒,这就是「几万个 goroutine 只对应少量线程」能成立的原因。要顺带讲清 GOMAXPROCS 决定 P 的数量(默认等于 CPU 核数),它影响的是并行度而不是并发度。
「两个 goroutine 在不在同一个队列」是道很刁的追问,答法要落在可观测性上。 严格的答案是:这由运行时的调度决策决定,从代码层面无法保证,因为 goroutine 创建时虽然默认入当前 P 的本地队列,但随时可能被偷走、被 gopark 挂起后重新入队、或者因为 GOMAXPROCS 变化被重新分配。面试官想听的是判断手段:用 runtime/trace(go tool trace)看每个 goroutine 在哪个 P 上运行和迁移、用 GODEBUG=schedtrace=1000,scheddetail=1 打调度器快照、或者用 pprof 的 goroutine 视图观察堆积;也能从源码侧推理(newproc 把新 G 放到当前 P 的 runnext,所以「刚创建且当前 P 还很空」时大概率同队列)。如果被问「怎么强行让它们同队列」,可以答:把两者放进同一个 P 是通过绑定 runtime.LockOSThread 或串行化执行来实现的,但真正想控制执行顺序应该用 channel、sync.WaitGroup 这类同步原语,而不是依赖调度顺序。最后回答「同队列的好处」:访问本地队列不需要加全局锁,减少跨 P 迁移与调度窃取带来的开销,缓存亲和性更好(同一个 P 上连续运行的 G 更容易命中 L1/L2),在高频创建短任务时能明显降低调度延迟;反过来代价是负载可能不均,需要靠 work stealing 与全局队列兜底。
下游 QPS 受限这类归因题,先分层再下结论。 不要直接说「下游不行」,而要给出可验证的排查顺序:一是自己的出流量是否超了对方给的配额(有没有被限流、有没有重试风暴把有效 QPS 放大)、二是自身调用链的瓶颈(连接池大小、线程或 goroutine 数、超时设置、序列化开销、串行调用未并行化)、三是下游的真实容量与依赖深度(下游背后还有更慢的依赖、慢查询、锁竞争)、四是网络与中间件(DNS、TLS 握手、跨可用区延迟、代理层排队)。定位手段上给证据:看超时与错误码分布、看 RT 分位(P99 与均值差多少)、看连接池等待时间与拒绝数、看是否集中在某台实例或某个 key 上。优化方向对应地说:加大连接池与并发度、批量合并请求、加缓存或本地缓存、异步化非关键路径、用熔断与降级把不可用下游隔离。面试官真正想确认的是你有没有能力把「受限」拆成可量化的原因,而不是抱怨下游。
并发上的「特殊设计」要主动交代取舍。 如果项目里为了扛并发做了设计,按这四类组织回答最容易讲清:缓存(本地 + 分布式多级、热点打散、过期时间打散、空值缓存)、异步与批处理(把非关键路径丢进 MQ、批量合并写入、合并小请求减少下游压力)、限流与隔离(接口级与用户级限流、线程池与信号量隔离、熔断降级)、幂等与一致性(唯一键、状态机、乐观锁或分布式锁,以及最终一致性的对账补偿)。每一类都要带上「为什么这里需要它」和「放弃了什么」——例如异步换来吞吐但对实时性有损,缓存换来性能但要处理一致性,这种取舍说明才是加分的部分。
知识库的架构与调用链路按数据流讲。 搭建侧:资料收集与清洗(去重、去噪、格式统一、权限与生效时间标注)、切分(按标题层级或语义切,带重叠,块大小按问答类型调)、向量化(选 embedding 模型,注意中文效果、维度与成本)、入库(向量库加元数据,元数据用于过滤与鉴权)、以及版本与增量更新(内容哈希去重,只重嵌入变化的部分)。调用侧:接收问题 → 查询改写或扩展 → 多路召回(向量管语义、关键词或 BM25 管专有名词)→ 融合与去重 → rerank 精排 → 按 token 预算拼接上下文(带来源标记)→ 交给模型生成并要求引用 → 无命中时明确说不知道。可观测性也要提:每步留 trace,记录召回了哪些块、得分多少、最终是否被引用,这样效果差的时候才能区分是「没召回」还是「召回对了但生成错」。
「你们的 Agent 平台比成熟产品差在哪」是道考察自省与产品感的题。 别急着护短也别自贬,按维度对照着说:一是可观测与可归因(成熟产品有完整的 trace 与评测看板,能定位到是哪一步、哪个工具、哪个 prompt 版本出问题);二是评测体系(有没有版本化评测集、回归门禁、在线抽样与人工校准,还是靠感觉调);三是工具与集成的生态(工具治理、权限与审计、MCP 这类标准协议的适配广度);四是稳定性与成本(重试与降级策略、上下文与缓存优化、模型分级路由、token 成本可见性);五是多租户与安全(隔离、配额、敏感操作审批)。说不足时一定要给出「我们已经在补哪一步」,并挑一个自己实际推动过的点展开细节——面试官要的是你能判断差距并动手,而不是把差距说成客观条件。回答结尾可以补一句场景差异:成熟产品面向通用场景,我们的优势在业务数据与场景沉淀,取舍上就不必追求功能对齐。
等分成四组:从「回溯 + 剪枝」讲到「状态压缩」。 先做两个前置判断:总和必须是 4 的倍数,且最大值不能超过 target = sum / 4,否则直接返回 false。回溯做法是把数组按从大到小排序(大数先放能更快触发剪枝),开四个桶,依次尝试把当前数放进任一个不超过 target 的桶里,放完递归下一位;剪枝有四种:跳过装不下的桶、跳过与上一个失败桶数值相同的桶(同一层等价剪枝)、跳过空桶(四个桶等价,放空桶失败就没必要试其它空桶)、以及一旦找到解就立即返回。更快的做法是位运算 DP:用整数位掩码表示已选的子集,记录当前正在凑的那一组已经凑到的和,枚举「能继续放进当前组的下标」做状态转移,复杂度是指数级但常数小于回溯。回答时把「前置判断 → 搜索顺序(降序)→ 剪枝条件 → 终止条件」讲全,面试官看的就是你会不会主动剪枝,写完还要能说清最坏复杂度与为什么会退化。