7-10-腾讯-TEG-云架构平台-后台开发-一面
7-10-腾讯-TEG-云架构平台-后台开发-一面
基本信息
- 时间: 2025-07-10 19:00-20:15 (75分钟)
- 部门: 腾讯TEG云架构平台
- 职位: 后台开发
- 面试内容: 技术问答+2道算法题(约30分钟)
飞书纯文字版存档
- 讲一下Golang的GMP(背)
- Golang哪些设计你认为比较好(回答:协程,Channel,Context,依赖管理)
- 为什么有了线程还要用Goroutine(轻,开销小)
- Golang设计的缺点呢(回答:错误异常处理,泛型,内存操作)
- 讲一下Golang的GC(回答:说了下三色标记法,没答全,忘了,每次都不看GC的东西)
- 场景:分布式KV,Raft,一个新的节点加入,过程是怎么样的?(回答:实现AddNewNode,预分配节点ID,设置为Follower,配置Metadata,Leader构造confChange并提交到日志流;使用Snapshot或者AppendEntries进行日志增量同步(这里还被问到如果是全量同步应该怎么办),最后Leader收到多数派确认后把这个节点加入列表,后续参与RequestVote,同时也有负载均衡的过程)
- 那这个负载均衡具体是做什么?(回答:Region分裂,调整局部热点,冷热分离;分区路由重新分配;PD调度器之类的)
- Leader挂掉之后会发生什么,恢复的过程是什么?(回答:follower超过election timeout未收到心跳,设置为candidate,之后发起新一轮选举;恢复过程主要是,新 Leader 提交未完成日志,通过 AppendEntries 同步到多数节点,最后应用已提交的日志到状态机。)
- 这个恢复的过程时长大概是多久?(回答:大约2s?不考虑数据同步的情况,集群节点位置也有影响)
- 你认为etcd的心跳和选举的时间大概在哪个范围?(回答:心跳大约150~200ms,低了导致CPU超载,选举时间大于心跳两倍,防止误判,一般1s?)
- 为什么是这样?知道etcd默认是多少吗?(回答:几乎同上,默认值忘了,凭感觉说的150ms和500ms,实际上好像是心跳100ms,选举1s)
- 讲一下如何做的强一致读(回答:ReadIndex,向Leader查询最新commit index,等待状态机应用到该index后返回数据,相当于只需要一次RTT)
- 除了这个呢?(回答:本地读和线性写后读,一个数据可能不一致,一个效率低,ReadIndex是折中的方案)
- 加了Redis有缓存不一致吗,怎么解决的(回答:延迟双删,再进一步可以布隆过滤器加熔断)
- 讲一下PebbleDB(回答:LSM Tree,增量式MemTable刷新,不需要CGO绑定)
- 场景:分布式DB很慢,从哪些方面排查?(回答:网络,磁盘,CPU资源,连接池,
- 你觉得秒杀系统中重要的部分是什么(回答:Redis+Lua解决超卖问题,etcd分布式锁,锁的续租和状态恢复,解决惊群问题)
- (续)这个属于系统层面的,用户层面的呢?(回答:限流熔断,令牌桶算法)
- 场景:为了提升QPS,你会对静态网页资源做什么处理?(回答:CDN或者Nginx)
- (续)不用现成的呢,自己从头设计一个,有没有思路?或者说,先说一下CDN和Nginx的原理(回答:sendfile,mmap,IO多路复用,epoll,iouring)(其实应该还可以答网络层协议,QUIC,以及共享内存+LRU)
- (续)sendfile主要是为了解决什么问题? (回答:磁盘 -> 内核缓冲区 -> 用户空间 -> 内核socket缓冲区 -> 网卡,总共2次拷贝,1次系统调用;使用sendfile之后只需要一次DMA拷贝)
- (续)为什么有了epoll还要引入iouring呢?(回答:脑子宕机忘了iouring具体设计了,简单说了一下epoll)
维度 | epoll(2002) | io_uring (2019) |
---|---|---|
系统调用次数 | 2次/操作(epoll_ctl+wait) | 1次提交批量操作 |
内存拷贝 | 需要传递事件数组 | 共享环形缓冲区零拷贝 |
异步支持 | 仅通知就绪事件 | 完整异步IO生命周期管理 |
典型延迟 | 5-10μs | 1-3μs |
- 算法题1:字符串表达式加减乘除,计算表达式结果,没有括号(回答:用vector,先计算乘除,替换结果,然后算加减)(也可以用两个栈)(约20min)
- 算法题2:一组一维线段,给出左右区间,问把坐标轴分成几段(回答:类似合并区间)(追问:如果左右区间都在[0,200],有没有什么办法更快)(回答:优化排序,用201大小的桶统计)(这里应该可以用bitset继续优化空间,因为只需要记录是否被覆盖)(10min)
- 反问:主要做哪一块内容(回答:NoSQL)
整理版
Golang相关
- GMP模型
- 回答: 讲解了Golang的GMP调度模型
- 参考答案: GMP是Golang的调度模型,包含Goroutine(G)、Machine(M)和Processor(P)。M是操作系统线程,P是逻辑处理器,G是协程。P维护本地G队列,M从P获取G执行。当G阻塞时,M会与P分离,P会寻找其他M来运行剩余G。
- Golang设计优点
- 回答: 协程、Channel、Context、依赖管理
- 参考答案: 其他优点还包括: 编译速度快、部署简单、内置并发支持、丰富的标准库、跨平台支持等。
- Goroutine优势
- 回答: 相比线程更轻量级,开销更小
- 参考答案: 具体来说,Goroutine初始栈仅2KB且可动态扩展,创建和切换成本极低(约几百纳秒),而线程通常需要MB级栈空间,创建和切换成本在微秒级。
- Golang设计缺点
- 回答: 错误异常处理不够完善、泛型支持较晚、内存操作不够灵活
- 参考答案: 其他常见批评: 缺乏真正意义上的OO特性、包管理早期设计不足、调试工具不够强大、性能优化空间有限等。
- GC机制
- 回答: 三色标记法(未答全)
- 参考答案: Go使用并发三色标记清除算法。1) 初始阶段暂停所有goroutine(STW);2) 标记阶段与程序并发执行;3) 标记终止阶段再次STW;4) 并发清除。1.8+版本STW时间通常<1ms。
分布式系统
- Raft新节点加入过程
- 回答: 预分配节点ID→设置为Follower→配置Metadata→Leader构造confChange→日志流提交→日志同步(全量/增量)→多数派确认→加入节点列表
- 参考答案: 详细过程: 1) 新节点以Learner身份加入;2) Leader通过AppendEntries同步日志;3) 当日志差距过大时发送快照;4) 新节点追上进度后,Leader提交配置变更;5) 新节点获得投票权。
- 负载均衡具体工作
- 回答: Region分裂、热点调整、冷热分离、分区路由重分配、PD调度器
- 参考答案: 还包括: 副本均衡(确保每个节点承载相似数量的副本)、Leader均衡(分散Leader压力)、存储空间均衡(防止单节点磁盘写满)等。
- Leader故障恢复
- 回答: Follower超时→Candidate状态→发起选举→新Leader提交未完成日志→同步多数节点→应用到状态机
- 参考答案: 详细过程: 1) 选举超时(150-300ms随机值);2) 获得多数投票;3) 新Leader提交之前term的未提交日志;4) 覆盖不一致的Follower日志;5) 应用已提交日志到状态机。
- 恢复时间估算
- 回答: 约2秒(不考虑数据同步和节点位置影响)
- 参考答案: 实际取决于: 1) 选举超时设置(通常1-2秒);2) 网络延迟;3) 日志追赶时间。生产环境通常设计为5秒内完成故障转移。
- etcd心跳和选举时间
- 回答: 心跳150-200ms(避免CPU超载),选举时间>心跳两倍(防止误判),约1秒
- 参考答案: 默认值: 心跳间隔(HeartbeatInterval)100ms,选举超时(ElectionTimeout)1000ms。最佳实践是选举超时应是心跳间隔的10倍左右。
- etcd默认值
- 回答: 实际默认值心跳100ms,选举1秒
- 参考答案: 可通过--heartbeat-interval和--election-timeout参数调整。网络不稳定时可适当增大。
- 强一致读实现
- 回答: ReadIndex(查询最新commit index→等待状态机应用→返回数据)
- 参考答案: 具体步骤: 1) 记录当前commit index;2) 向所有节点广播心跳确认Leader身份;3) 等待状态机应用到该index;4) 读取本地数据。
- 其他一致性读方案
- 回答: 本地读(可能不一致)、线性写后读(效率低)
- 参考答案: 还有: 1) LeaseRead(租约期内允许本地读);2) FollowerRead(特定场景下允许从Follower读);3) StaleRead(允许读取过期数据)。
- Redis缓存不一致解决方案
- 回答: 延迟双删、布隆过滤器+熔断
- 参考答案: 其他方案: 1) 设置合理的过期时间;2) 使用canal监听binlog异步更新;3) 分布式锁保证串行化更新;4) 版本号/时间戳比对。
存储系统
- PebbleDB特点
- 回答: LSM Tree、增量式MemTable刷新、无CGO绑定
- 参考答案: 其他特点: 1) 支持并行压缩;2) 前缀Bloom filter;3) 更高效的内存管理;4) 与RocksDB API兼容但性能更优。
性能排查
- 分布式DB性能排查
- 回答: 网络、磁盘、CPU资源、连接池等
- 参考答案: 系统化方法: 1) 监控指标(QPS/延迟/错误率);2) 慢查询分析;3) 执行计划检查;4) 锁竞争分析;5) 资源饱和度(CPU/IO/网络);6) 垃圾回收情况。
系统设计
秒杀系统关键部分
- 回答: Redis+Lua解决超卖、etcd分布式锁(续租/状态恢复)、惊群问题解决
- 参考答案: 完整方案: 1) 前端限流(答题/验证码);2) 中间层缓存(Redis集群);3) 异步下单队列;4) 库存预热;5) 热点数据隔离;6) 降级策略。
用户层面考虑
- 回答: 限流熔断、令牌桶算法
- 参考答案: 还包括: 1) 排队机制;2) 购买结果异步通知;3) 防机器人验证;4) 分级用户(优先服务VIP);5) 页面静态化。
静态资源优化
- 回答: CDN/Nginx
- 参考答案: 其他优化: 1) HTTP/2 Server Push;2) 资源合并与压缩;3) 缓存策略优化;4) 域名分片;5) 预加载/prefetch。
自设计静态资源系统
- 回答: sendfile、mmap、IO多路复用(epoll/iouring)
- 参考答案: 设计要点: 1) 零拷贝技术;2) 异步IO;3) 智能缓存(LRU+热点检测);4) 压缩传输;5) 边缘计算节点部署。
sendfile作用
- 回答: 减少数据拷贝次数(传统2次→sendfile 1次DMA拷贝)
- 参考答案: 具体过程: 1) 传统read/write需要4次上下文切换+2次拷贝;2) sendfile只需2次上下文切换+1次DMA拷贝;3) Linux 2.4+支持"零拷贝"模式。
io_uring vs epoll
维度 epoll (2002) io_uring (2019) 系统调用次数 2次/操作(epoll_ctl+wait) 1次提交批量操作 内存拷贝 需要传递事件数组 共享环形缓冲区零拷贝 异步支持 仅通知就绪事件 完整异步IO生命周期管理 典型延迟 5-10μs 1-3μs - 参考答案: io_uring创新点: 1) 提交/完成环的双环形缓冲区;2) 支持缓冲IO;3) 可注册文件描述符集;4) 支持内核线程poll模式。
算法题
- 字符串表达式计算
- 题目: 计算不含括号的加减乘除表达式
- 回答: 使用vector先处理乘除再处理加减(也可用双栈)
- 做完之后表示没有加括号降低难度了
- 参考答案: (面试中写的时候考虑了空格还有除零错误之类的问题)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using namespace std;
int calculate(string s) {
stack<int> stk;
int num = 0;
char sign = '+';
for (int i = 0; i < s.size(); ++i) {
char c = s[i];
if (isdigit(c)) {
num = num * 10 + (c - '0');
}
if ((!isdigit(c) && c != ' ') || i == s.size() - 1) {
if (sign == '+') {
stk.push(num);
} else if (sign == '-') {
stk.push(-num);
} else if (sign == '*') {
int top = stk.top();
stk.pop();
stk.push(top * num);
} else if (sign == '/') {
int top = stk.top();
stk.pop();
stk.push(top / num);
}
sign = c;
num = 0;
}
}
int res = 0;
while (!stk.empty()) {
res += stk.top();
stk.pop();
}
return res;
} - 时间: 约20分钟
- 线段分割问题
- 题目: 一组一维线段,求坐标轴被分成几段
- 回答: 类似合并区间
- 优化: 对于[0,200]区间可用桶排序/bitset优化
- 参考答案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int countSegmentsBitset(vector<vector<int>>& intervals) {
bitset<201> covered;
for (const auto& interval : intervals) {
int start = interval[0];
int end = interval[1];
for (int i = start; i < end; ++i) {
covered.set(i);
}
}
int segments = 1;
for (int i = 0; i < 200; ++i) {
if (covered[i] != covered[i + 1]) {
segments++;
}
}
return segments;
} - 时间: 约10分钟
反问环节
- 主要业务方向: NoSQL相关开发
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 The Coding Odyssey | Chronicles of a Software Developer!