面灵AI→

字节跳动火山引擎 Agent 开发秋招一面

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

  1. 算法题:LRU 缓存(LeetCode Hot 100)。
  2. 场景题:设计一个简单的异步系统(不使用 MQ 等中间件),实现数据的存取,说明从发起请求到数据存储和获取的全过程如何设计。
  3. 项目拷打:说明实习的 Agent 项目中你主要参与的部分,包括方案选型、Skills 设计、接口设计、从请求到处理的全链路;你认为 Agent 开发和传统前后端开发有哪些区别?
  4. 进程和线程的区别是什么?
  5. 进程和线程创建、切换上下文的开销有哪些区别?
  6. 进程间的常用通信方式(IPC)有哪些?在终端输入 ls 指令属于哪一种?
  7. 操作系统的内核态和用户态有何区别?
  8. 说一下 TCP 的三次握手,并分析为什么不用两次或四次?
  9. 说一下 TCP 的拥塞控制机制。
  10. 说明 TCP 和 UDP 的区别;如果想用 UDP 实现可靠传输该怎么处理?
  11. HTTP 的常见状态码有哪些?了解 429 是什么吗?
  12. HTTP 接口的 POST、PUT、PATCH 有何区别?
  13. POST 接口本身不幂等,如果业务需要幂等处理,怎么设计接口?
  14. 了解 Go 语言和其中的协程吗?
  15. Redis 是单线程还是多线程的?为什么?
  16. I/O 多路复用中的 epoll 是什么?
  17. 了解过零拷贝吗?
  18. Redis 的数据持久化机制有哪些?
  19. 既然 Redis 有数据持久化机制,为什么还要用数据库,不直接用 Redis 存储数据?
  20. 除了它是 NoSQL 数据库外,为什么即使有些场景需要 NoSQL 数据库,也不选择 Redis?
  21. MQ 的消息积压如何处理?
  22. 在后端设计时,如何保证能监测到 MQ 积压问题?

《参考解析》

LRU 缓存的及格线是「哈希表 + 双向链表」,两个结构配合才能同时拿到 O(1) 的查询与更新。哈希表存 key 到链表节点的映射,双向链表按访问时间排序:头部是最近使用、尾部是最久未使用。get(key) 命中后把节点从原位摘下来插到头部;put(key, value) 若 key 存在则更新值并移到头部,若不存在则新建节点插头部,再判断容量是否超限,超了就删掉尾节点并同步删掉哈希表里的键。写代码时要注意几个细节:用哨兵头尾节点可以消掉一堆空指针判断;put 更新已有 key 时不要增加容量计数;Java 里可以直接 LinkedHashMap 重写 removeEldestEntry,但面试通常要求手写,主动提一句「工程上可以用 LinkedHashMap,手写是为了看链表操作」比较得体。常见追问是 LFU(需要额外的频次桶或最小频次指针)和「为什么不用数组/时间戳排序」(移动元素 O(n))。

不用 MQ 设计简单异步系统,思路是「本地持久化队列 + 工作线程池 + 状态机」。请求进来先把任务落库(状态 PENDING)并返回任务 id,这一步保证不丢;再用线程池或协程池从库里拉取待处理任务执行,成功后把状态改成 SUCCESS、失败改成 FAILED 并记录重试次数;查询接口按任务 id 返回状态,前端轮询或走 SSE/WebSocket 推送。要点有四个:① 拉取任务时用「SELECT ... FOR UPDATE SKIP LOCKED + 限流批量」避免多实例抢同一条;② 幂等靠任务 id 唯一约束或状态机的乐观锁(WHERE status='PENDING')保证重复消费无害;③ 失败要有退避重试和最大次数,超过就进死信表并告警;④ 可观测性要有积压量(待处理条数)、处理耗时、失败率三个指标。要主动说出这个方案的边界:单库轮询有 QPS 上限、延迟高于 MQ 推送、任务量大时会打库,所以适合「量不大但要简单可靠」的场景,量上来就该换 MQ——面试官问这题通常就是想听你承认取舍,而不是硬说本地队列能替代 MQ。

进程与线程的答法要落到「资源分配」与「调度执行」两个词的区分上:进程是资源分配的基本单位,有独立地址空间、文件描述符表、信号处理等;线程是 CPU 调度的基本单位,同一进程内的线程共享地址空间和文件描述符,只独占栈、寄存器、程序计数器。开销差别在于:创建进程要分配页表、拷贝父进程资源(fork 的写时复制只是延后了拷贝),创建线程只需分配栈和少量内核结构;切换进程要切换页表(进而刷新 TLB,代价最大),切换线程只切换寄存器和栈指针。通信方式上,进程间有管道/命名管道、消息队列、共享内存(最快,但要自己做同步)、信号量、信号、socket(可跨主机);线程间直接读写共享内存即可,靠互斥锁和条件变量同步。ls 这类命令执行时,shell 先 fork 出子进程再 exec 替换映像,父子之间用管道传输出——所以是同主机进程间通信里的管道。

TCP 三次握手的目的是双方都确认「自己的发送能力和对方的接收能力正常」,并同步初始序列号。两次不行是因为服务端发出 SYN-ACK 后无法确认客户端是否收到:如果客户端的连接请求在网络里滞留后重发,旧请求可能在新连接建立后才到达服务端,服务端会误以为客户端又要建连,白白分配资源——第三次握手让服务端能从客户端是否回 ACK 判断这条连接是否真的有效。四次也不必:服务端的 SYN 和 ACK 可以合并成一个报文,本来就是同一次交互里的两件事。拥塞控制四段式:慢启动(cwnd 从 1 个 MSS 指数增长到 ssthresh)、拥塞避免(超过阈值后改为每 RTT 加一,线性增长)、快重传(收到三个重复 ACK 立刻重传丢失报文,不等超时)、快恢复(ssthresh 减半、cwnd 设为新 ssthresh,进入拥塞避免)。现代实现通常是 CUBIC 或 BBR,可以把「基于丢包」和「基于带宽时延估计」的区别补一句。

UDP 上做可靠传输等于把 TCP 的机制在应用层重造一遍:给每个报文编号并在接收端按序重组、为每个报文设置超时重传、用滑动窗口做流量控制、用类似拥塞窗口的策略做拥塞控制、加校验和检测损坏。同时可以针对性优化 TCP 的痛点:不需要严格按序交付的场景(音视频)就允许后到的包先处理,避免 TCP 的队头阻塞;连接建立可以省去握手。现实中的参照是 QUIC(跑在 UDP 上,把 TLS、多路复用、可靠传输做进用户态),HTTP/3 就是它,答这题时能提一句 QUIC 会显得答案有落点。

HTTP 状态码按五类记:1xx 信息(101 协议切换)、2xx 成功(200、201 创建、204 无内容)、3xx 重定向(301 永久、302/307 临时、304 协商缓存命中)、4xx 客户端错误(400、401 未认证、403 无权限、404、409 冲突、429 请求过多)、5xx 服务端错误(500、502 网关错误、503 不可用、504 网关超时)。429 是限流响应,通常带 Retry-After 头告诉客户端多久后重试,客户端应做退避重试而不是硬打。POST/PUT/PATCH 的区别在语义:POST 创建资源(非幂等,重复调用可能产生多条)、PUT 全量替换指定资源(幂等,重复调用结果一致)、PATCH 局部更新(不要求幂等,取决于补丁语义)。接口幂等设计的主流做法是客户端生成唯一请求 id(幂等键)随请求带上,服务端用唯一索引或 Redis SETNX 做去重,命中则直接返回首次结果;此外还有状态机约束(如「订单只在待支付状态可支付」)、乐观锁版本号、以及数据库唯一约束兜底。要强调「幂等键要落库而不是只放 Redis」,否则 Redis 失效后会重复执行。

Redis 与线程模型:Redis 处理命令的主线程是单线程的,所以单条命令天然原子、没有锁竞争,这也是它能直接支撑 INCR、SETNX 这类原子操作的原因。但严格说不是「全单线程」:4.0 起有后台线程做 UNLINK、FLUSHALL ASYNC 这类惰性删除,6.0 起有 I/O 多线程只用于读写 socket 和协议解析(命令执行仍在主线程),还有 bgsave/bgrewriteaof 的 fork 子进程。epoll 是 Linux 上的 I/O 多路复用机制:单个线程用一个 epoll 实例同时监听大量 fd,内核用红黑树管理注册的 fd、用就绪链表返回已触发的事件,避免 select/poll 每次调用都要把整个 fd 集合拷进内核并线性扫描,这也是 Redis、Nginx 能单线程扛高并发连接的基础。零拷贝指减少数据在内核态与用户态之间的复制次数:传统 read + write 要四次拷贝两次系统调用,sendfile 让数据直接从页缓存进网卡(DMA),mmap 把文件映射进用户空间省掉一次拷贝,splice 可以走管道零拷贝。Redis 的持久化:RDB 是某一时刻的全量快照(fork 子进程写临时文件再原子替换,文件小、恢复快,但可能丢最后一次快照后的数据),AOF 是追加写命令日志(appendfsync 可选 always/everysec/no,everysec 是默认,丢了最多 1 秒),AOF 重写把冗余命令压缩;4.0 起支持混合持久化,AOF 文件里前半段放 RDB 快照、后半段放增量命令,兼顾恢复速度和数据完整度。

**「为什么不直接用 Redis 存数据」**要从持久化与一致性两个方向答。Redis 的持久化是异步的:RDB 有快照间隔、AOF everysec 也可能丢最后一秒,机器断电或进程被杀就可能丢数据;而数据库靠 WAL(redo log)和事务保证提交即持久。此外还有:内存成本远高于磁盘,全量数据放内存不经济;Redis 缺少完整的事务、复杂的多条件查询、二级索引、外键约束和强一致的读己之写;主从复制默认异步,故障切换可能丢数据。所以常规架构是「数据库为准、Redis 做缓存或高性能场景的加速层」,配合缓存一致性策略(先更新库再删缓存 + 延迟双删或订阅 binlog 失效)。至于「有些场景需要 NoSQL 也不选 Redis」:文档型数据(嵌套结构、按字段查询)更适合 MongoDB,海量宽列写入适合 HBase/Cassandra,检索与聚合分析适合 Elasticsearch,图关系适合 Neo4j;Redis 的定位是内存数据结构服务,持久化和查询能力都不是它的强项,硬扛会付出数据安全和运维上的代价。

MQ 消息积压的处理要按「先止损、再定位、后根治」讲。止损阶段:临时扩容消费者实例(前提是分区/队列数够,否则要先扩分区)、把消费逻辑里耗时的下游调用降级或异步化、必要时先批量转存到临时队列再慢慢补。定位阶段要看是生产端突增(活动流量、上游重试风暴)还是消费端变慢(下游 RT 升高、单条处理变慢、消费线程被阻塞、锁竞争),靠消费者的 lag 曲线和单条处理耗时分布区分。根治方向:提高并行度(分区数 ≥ 消费者数)、批量消费、把串行逻辑改并行、给下游加缓存或限流、把非核心动作(发通知、写日志)移出主消费链路。可观测性要常备三个指标:消费 lag(堆积量)、消费速率与生产速率之比、消息从生产到消费的端到端延迟;告警要按 lag 的增长斜率而不是绝对阈值,否则业务量正常增长时会一直误报。另外要提前设计死信队列和重试上限,避免一条毒消息把整个队列堵死。