米哈游 服务器开发方向 C++ 二面 面经

1. 自我介绍,介绍一下你做过的最有挑战性的项目

这道题考察的是表达能力和技术深度,回答框架建议用 STAR 法则:

答:我参与过一个游戏服务器的高并发架构重构项目。背景是原有服务器在日活 50 万时出现明显卡顿,延迟峰值达到 800ms。我负责的部分是网络层和消息队列的重新设计。

具体做了几件事:① 将原来的 select 模型换成 epoll ET 模式,配合非阻塞 IO,连接数从 1 万提升到 10 万;② 引入无锁 SPSC 队列替换原来加互斥锁的消息队列,热路径延迟降低了约 40%;③ 用内存池管理频繁分配的消息对象,减少了 GC 压力和内存碎片。最终上线后延迟峰值降到 120ms 以内,服务器 CPU 利用率下降了 25%。

挑战在于:无锁队列的内存序问题非常难调试,用 ThreadSanitizer 发现了两处 data race,最终通过仔细分析 acquire/release 语义解决。

2. 项目中遇到过内存泄漏吗?你是怎么定位和解决的?

答:遇到过。定位内存泄漏的常用手段:

① Valgrind Memcheck:运行程序后报告所有未释放的堆内存,精确到分配的调用栈。命令:valgrind --leak-check=full ./server,缺点是运行速度慢约 10-20 倍,适合测试环境。

② AddressSanitizer(ASan):编译时加 -fsanitize=address,运行时检测内存越界、use-after-free、内存泄漏,速度比 Valgrind 快很多,适合 CI 集成。

③ 自定义内存追踪:重载 operator new/delete,记录每次分配的地址、大小、调用栈,程序退出时打印未释放的记录。

我项目中的一次泄漏是:玩家断线时,持有该玩家 shared_ptr 的定时器没有被取消,导致玩家对象无法释放。用 Valgrind 定位到分配点后,发现是定时器回调里捕获了 shared_ptr 的 lambda,改成捕获 weak_ptr 后解决。

3. 如何设计一个支持百万并发连接的游戏服务器架构?

答:百万并发连接不能用单机单进程,需要分层架构:

网络接入层:多台 Gateway 服务器,每台用 epoll + 协程处理 IO,只负责连接维护和消息收发,不做业务逻辑。单机 epoll 支持 10 万级连接,10 台 Gateway 即可支撑百万连接。

逻辑层:按游戏功能拆分为多个服务(场景服、战斗服、社交服、背包服等),Gateway 根据消息类型路由到对应服务。服务间通过消息队列(如 Kafka)或 RPC(如 gRPC)通信。

数据层:Redis 做热数据缓存(玩家状态、排行榜),MySQL 做持久化存储,写操作异步落库,读操作优先走缓存。

关键设计点:① 无状态 Gateway,方便水平扩展;② 玩家 session 信息存 Redis,任意 Gateway 都能处理同一玩家的消息;③ 心跳检测 + 断线重连机制;④ 消息协议用 Protobuf,序列化效率高且跨语言。

4. 分布式系统中如何保证消息不丢失、不重复?

答:这是分布式系统的经典问题,核心是幂等性 + 持久化 + 确认机制。

不丢失:① 消息发送方持久化到本地 WAL(Write-Ahead Log)再发送;② 消息队列(Kafka)配置 acks=all,确保所有副本写入才返回成功;③ 消费方处理完业务后再提交 offset,而非收到就提交。

不重复:网络重试必然导致重复投递,解决方案是幂等处理。每条消息带唯一 message_id,消费方处理前先查 Redis 是否已处理过该 id,处理完后写入 Redis(设置过期时间)。这样即使消息重复投递,业务逻辑也只执行一次。

恰好一次(Exactly Once):Kafka 0.11+ 支持事务,配合幂等 Producer 可实现端到端 Exactly Once,但性能有损耗,游戏场景通常用 At Least Once + 幂等消费即可。

5. 数据库事务的隔离级别有哪些?可重复读是如何实现的?

答:四个隔离级别(从低到高):

  • 读未提交(Read Uncommitted):可读到其他事务未提交的数据,存在脏读。
  • 读已提交(Read Committed):只读已提交数据,解决脏读,但存在不可重复读(同一事务两次读结果不同)。
  • 可重复读(Repeatable Read):同一事务内多次读结果一致,解决不可重复读,MySQL InnoDB 默认级别。
  • 串行化(Serializable):完全串行执行,解决幻读,性能最差。

可重复读的实现(InnoDB MVCC):事务开始时创建一个 Read View,记录当前活跃事务列表。后续每次读操作都用这个 Read View 判断数据版本可见性,只读取在 Read View 创建时已提交的版本。因为 Read View 不变,所以同一事务内读到的数据始终一致。

幻读问题:InnoDB 在 RR 级别下通过 Gap Lock(间隙锁)+ Next-Key Lock 防止幻读,锁住查询范围内的间隙,阻止其他事务插入新行。

6. Redis 的持久化方式有哪些?AOF 重写的原理是什么?

答:两种持久化方式:

RDB(快照):定期将内存数据序列化为二进制文件。优点:文件紧凑,恢复速度快;缺点:两次快照之间的数据可能丢失,fork 子进程时有短暂阻塞。

AOF(追加日志):每条写命令追加到 AOF 文件。优点:数据更完整,最多丢失 1 秒数据(fsync every second);缺点:文件越来越大,恢复速度慢。

AOF 重写原理:AOF 文件会随时间膨胀(同一个 key 被修改多次,历史命令都保留)。重写时 fork 子进程,子进程遍历当前内存数据,将每个 key 的当前状态用最少的命令表示(如一个 list 用一条 RPUSH 替代多条操作),写入新 AOF 文件。重写期间父进程继续处理请求,新命令同时写入旧 AOF 和重写缓冲区,重写完成后将缓冲区追加到新文件,原子替换旧文件。

7. 什么是一致性哈希?它解决了什么问题?虚节点的作用是什么?

答:普通哈希取模(key % N)的问题:增减服务器节点时,N 变化导致几乎所有 key 重新映射,缓存大面积失效,引发缓存雪崩。

一致性哈希:将哈希空间组织成一个环(0 到 2^32-1),服务器节点映射到环上的某个位置,key 也映射到环上,顺时针找到的第一个节点就是负责该 key 的节点。增减节点时只影响相邻节点的数据,其他节点不受影响。

虚节点:真实节点少时,节点在环上分布不均匀,导致负载不均衡。虚节点是每个真实节点在环上的多个映射点(如每个节点对应 150 个虚节点),使数据分布更均匀。增减节点时,该节点的多个虚节点同时变化,数据迁移更均匀,不会集中压垮某个节点。

8. C++ 中的内存模型是什么?std::atomic 的 memory_order 有哪几种?

答:C++11 引入了多线程内存模型,定义了多线程环境下内存操作的可见性和顺序保证,解决了不同 CPU 架构(x86、ARM)的指令重排问题。

六种 memory_order:

  • memory_order_relaxed:只保证原子性,不保证顺序,性能最好,适合计数器累加。
  • memory_order_acquire:读操作,保证此操作之后的读写不会被重排到此操作之前,配合 release 使用。
  • memory_order_release:写操作,保证此操作之前的读写不会被重排到此操作之后。
  • memory_order_acq_rel:同时具有 acquire 和 release 语义,用于读-改-写操作(如 fetch_add)。
  • memory_order_seq_cst:顺序一致性,最强保证,所有线程看到的操作顺序一致,是默认值,性能最差。

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++八股文全集 文章被收录于专栏

本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。

全部评论
这一看就包装的
点赞 回复 分享
发布于 04-14 15:58 广东
感觉写的很好呢
点赞 回复 分享
发布于 04-03 23:29 北京

相关推荐

腾讯云平台架构后台开发日常实习一面凉经也没人告诉我腾子这么高强度啊,简直是本牛子经历过最难的一次面试面试官不开摄像头,不问项目,粗问实习,全程拷打底层和场景,越不会越问,面完十分钟秒挂。# 实习(答的比较流利且不对口,没有)## 1.说说你在实习中解决的最有价值的问题?## 2.你这个问题如何解决的?## 3.你是怎么排查到这个问题的?## 4.你如何量化解决这个问题后的效果的?有数据指标吗?# C++## 1.讲讲系统的内存分配机制?## 2.说说堆区和栈区的区别?怎么在堆区申请内存?new和malloc的区别是什么?## 3.如果malloc了一块堆内存,让`std::string`对象管理这片内存可以吗?为什么?## 4.如果只有2G物理内存,malloc申请3G内存会发生什么?如果报错是编译期报错还是运行时报错?如果是运行时报错报的是哪种错误?## 5.lamda的底层原理是什么?和匿名函数是一个东西吗?如果不是的话有什么区别?你知道C++11的std::function出现之前C++如何定义和使用函数对象吗?## 6.手写定义函数对象的代码## 6.说说三种智能指针的区别?如果你自己定义和使用指针时会用哪一种智能指针,为什么?## 7.手写循环引用产生的代码和如何解决循环引用的代码## 8.struct中定义int、double、char三种类型的成员变量,不同的定义顺序(比如类定义中以int、double、char的顺序定义和以char、double、int的顺序定义)你觉得会有什么不同吗?# 场景题场景:有一个服务器,负责跑用户发来的python脚本## 1.如果某用户脚本是恶意脚本,有无限循环持续占用服务器cpu,影响到其他用户使用,你如何排查和解决这个问题?## 2.你提到设计调度器、用户态线程、检测线程,由用户态线程来承载脚本任务,由调度器来给任务分配线程去执行,由检测线程观测用户态线程执行时长,那么你为什么要设计用户态线程去跑任务?## 3.怎么观测它的执行时长呢?检测线程是系统线程还是用户态线程?如果用户脚本并非恶意只是单纯执行比较久,你这样会不会误判?## 4.如果恶意脚本中出现数组越界等违规操作导致程序崩溃,即使它工作在用户态线程也会进而破坏系统线程和进程,你怎么解决?(意识到之前思路错误,改说用容器化技术)## 5.你提到改用容器化技术来跑用户脚本,说说容器的底层原理?在这个场景中具体怎么用容器化技术来解决问题?(之前了解过一点,但是完全讲不出)## 6.回到刚刚你提到的,手写实现用户态线程、调度器以及检测线程的代码## 7.你说你平常用的是框架提供的协程和调度机制,说说Boost中协程的机制和原理是什么样的?是哪些类提供了这些能力?## 8.假如用户发出http请求后迟迟没有收到服务器的响应,你应该如何排查和解决这个问题?## 9.你提到用心跳机制来保证用户和服务端连接未断开,但我说的未收到响应并不是传输层问题而是应用层问题,我想问的是在应用层如何排查和解决?(不清楚要从什么方向回答)# 算法力扣1262.可被3整除的最大和(暴力回溯写出来,不会优化)
查看23道真题和解析
点赞 评论 收藏
分享
面的挺好的 十天后告知横向挂 伤心透了兄弟们一、 实习项目与场景深挖1. 大模型接口限流与熔断接口限流具体是怎么做的?如何控制模型的并发量?超过限流阈值后怎么降级?兜底策略是什么?熔断机制的阈值是怎么设置的?触发熔断后,流量全部切到备用服务商吗?服务恢复后流量怎么切回?极端追问:切全量流量过去时,如果备用服务商瞬间被打挂,架构上有什么解法?2. Redis 大Key排查与存储优化怎么排查和解决项目中 Redis 大Key问题的?除了内存溢出(OOM),大Key还会给 Redis 带来哪些风险?为什么会拖垮性能?把 Hash 拆分到了不同节点的 String,为什么后续依然有压力?数据迁移到 MySQL 后,能省多少成本?细节辨析:内存溢出(OOM)和内存泄漏的区别是什么?3. MySQL 主从延迟与缓存引入怎么解决 Session 创建时下游业务读不到数据的主从延迟问题?极端追问:单机 MySQL 随便抗几万 QPS,你们业务 QPS 才 50-200,性能极度冗余的情况下,为什么不直接读主库?为什么要增加系统复杂性和成本去引入 Redis?4. Redis 集合应用(Set/ZSet)怎么用 Redis 实现点赞排行榜?怎么按时间排序?怎么用 Set 实现关注和共同关注功能?极端追问:如果关注量上限放宽到5万,用 Set 存会有什么问题?二、 Java 并发基础多线程场景下,怎么实现一个线程安全的计数器?为什么直接用 i++ 不行?底层会发生什么导致记错?Atomic 包下的原子类底层是怎么解决并发安全问题的?除了 CAS,原子类底层还有使用其他的操作或机制吗?三、 MySQL 底层原理与索引MySQL 有哪四种事务隔离级别?分别解决了哪三个问题?底层深挖:不同隔离级别在底层 MVCC 的执行逻辑有什么区别?当前读和快照读的区别是什么?为什么 MySQL 索引底层用 B+树不用 B树?常规情况下,B+树的树高一般是几层?什么场景下用联合索引?MySQL 索引失效的常见场景有哪些?四、 算法手撕螺旋矩阵:给定一个 N,顺时针打印从 1 到 N^2 的正方形矩阵。
查看26道真题和解析
点赞 评论 收藏
分享
评论
6
18
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务