6.8 KiB
Go后端开发-字节一面
1.自我介绍
好的。 我目前有大概 6 年后端开发经验,前几年在一家中型电商公司做业务后端,后来转到一家做 SaaS 的公司担任 Tech Lead,带过 5 人左右的小团队。 技术栈主要是 Go,早期也写过一段时间 Java。做过的系统包括高并发订单、Feed 流、消息推送这类,QPS 峰值在几万级别,算不上超大规模但该踩的坑基本都踩过。 最近一年多在做 AI 工程化方向,主要是 RAG 系统落地和 Agent 框架搭建,也做过一些推理侧的优化——主要是降延迟和控成本,用过 vLLM,对 KV Cache、量化这些有实践经验,不算特别深但都是真实上线过的。 对字节感兴趣主要是两点:一是规模,字节的系统体量能接触到我现在遇不到的问题;二是 AI 基础设施这块,字节在这方面投入很大,我想在更大的平台上深耕这个方向。 有什么想深入了解的可以直接问。
2.如果让你编译一个golang的程序,在linux下执行包含哪些系统调用
编译阶段(编译器本身运行时)
go build main.go
编译器进程本身会用到:
execve:启动编译器进程open/read:读取源文件、依赖包write:写出目标文件mmap:映射文件到内存fork/clone:编译器可能启动子进程(汇编器、链接器) 程序启动阶段(./main 执行时) Go runtime 初始化涉及:execve:启动进程mmap:申请虚拟地址空间(堆、栈)mprotect:设置内存页权限clone:创建系统线程(M)sigaction:注册信号处理(GC、抢占调度依赖信号)futex:线程同步 运行阶段mmap/munmap:堆内存扩缩read/write:IO 操作epoll_create/epoll_wait:netpoller 底层nanosleep:调度器 sleeprt_sigreturn:信号处理返回 验证方式
strace ./main 2>&1 | head -50
实际跑一下一目了然,Go 程序启动时 mmap 调用特别多,VSZ 看起来大就是这个原因。这块我是结合 strace 实际观察过,但内核源码层面没深入读过。
3.一个http请求的整个路由过程
从网络到业务代码的完整路径
1. 网卡 → 内核网络栈
网卡收包 → DMA 写入 Ring Buffer
→ 硬中断通知 CPU → 软中断(NET_RX)
→ TCP/IP 协议栈处理 → 数据写入 socket 接收缓冲区
2. epoll 唤醒 Go netpoller 基于 epoll:
epoll_wait 检测到 fd 可读
→ 唤醒对应 goroutine
→ 从 socket buffer 读取数据
3. HTTP 解析
读取原始字节流
→ 解析请求行(Method、Path、Version)
→ 解析 Header
→ 读取 Body
4. 路由匹配 以 gin 为例:
注册时构建路由树(Radix Tree)
请求到来 → 按 Method 找对应树
→ 前缀匹配找到 handler
→ 提取路径参数(:id、*path)
5. 中间件链执行
middleware1 → middleware2 → handler → middleware2 → middleware1
洋葱模型,gin 用 index 递增控制:
c.Next() // 推进到下一个handler
c.Abort() // 中断链路
6. 响应写回
handler 写 ResponseWriter
→ 序列化响应体
→ 写入 socket 发送缓冲区
→ 内核发送
踩过的坑:路由注册顺序影响匹配,/user/profile 和 /user/:id 同时注册时要注意静态路由优先级高于参数路由,gin 的 radix tree 处理了这个但自己实现路由容易踩。
4.拷打项目(拷打的非常多)
5.http2和http3的区别,http3不可靠的udp怎么实现可靠
HTTP/2 vs HTTP/3 核心区别
| HTTP/2 | HTTP/3 | |
|---|---|---|
| 传输层 | TCP | QUIC(基于 UDP) |
| 队头阻塞 | 解决了 HTTP 层,TCP 层还有 | 彻底解决 |
| 握手 | TLS 1~2 RTT | 0~1 RTT |
| 连接迁移 | 不支持 | 支持(换网络不断连) |
HTTP/2 的遗留问题
多路复用解决了 HTTP 层队头阻塞,但 TCP 是字节流,一个包丢失整条连接都要等重传——TCP 层队头阻塞依然存在,弱网环境下反而比 HTTP/1.1 更差。
HTTP/3 = HTTP over QUIC
QUIC 在 UDP 之上自己实现了可靠传输:
可靠性保障机制
序列号:每个 QUIC 包有唯一 Packet Number,单调递增
ACK:接收方确认,发送方维护发送窗口
重传:超时或收到 NACK 触发重传
流量控制:连接级 + 流级别双重控制
彻底解决队头阻塞
Stream A: [pkt1] [pkt2丢失] [pkt3]
Stream B: [pkt4] [pkt5]
Stream A 丢包只阻塞 Stream A,Stream B 不受影响——因为 QUIC 在用户态管理每个 Stream 的状态。
0-RTT 握手
首次连接:1-RTT(比TLS1.3 over TCP 少一次)
再次连接:0-RTT,直接带上之前的会话票据
连接迁移 TCP 连接由四元组标识,换 IP 就断了。QUIC 用 Connection ID 标识连接,切换网络(WiFi → 4G)连接不中断。
这块我了解原理,生产环境还没大规模用过 HTTP/3,主要是服务端支持和中间件兼容性还不够成熟。
6.手撕代码:二叉树的右视图
7.mysql给一个比较长的读取操作,例如对全表进行读取,再读取的同时来了一个写入操作会发生什么;快照会把所有的数据都copy一份吗;每一次读取都需要把undo log回放一遍吗
全表读 + 并发写入会发生什么
MySQL InnoDB 默认 RR 隔离级别,普通 SELECT 走 MVCC 一致性读:
全表扫描开始 → 创建 ReadView(记录当前活跃事务列表)
→ 读取每行时,根据 ReadView 判断该版本是否可见
→ 期间写入操作正常进行,不阻塞读
→ 读到的始终是事务开始时的快照版本
写入不会被读阻塞,读也不会被写阻塞——这是 MVCC 的核心价值。
快照会 copy 所有数据吗
不会,这是个常见误解。
ReadView 只是一个轻量结构:
// 伪代码
ReadView {
creator_trx_id // 创建者事务ID
trx_ids // 创建时活跃事务列表
up_limit_id // 活跃事务最小ID
low_limit_id // 下一个事务ID
}
数据还在原地,版本链挂在行记录上,通过 undo log 串起来。读取时按需回溯,不是提前 copy。
每次读都要回放整个 undo log 吗
不是回放,是版本链查找,而且不一定每次都要遍历:
行记录 → trx_id 可见?直接读
→ 不可见 → 找 roll_pointer → 上一版本
→ 再判断可见性 → 直到找到可见版本
代价取决于版本链长度——长事务 + 高频更新的行,版本链可能很长,读性能会下降。
踩过的坑:长事务不提交导致 undo log 无法清理,版本链越来越长,后来读这些热点行明显变慢。监控里加了 INFORMATION_SCHEMA.INNODB_TRX 告警,发现长事务及时干掉。