适用场景:大厂后端/C++日常实习面试。
项目关键词:Epoll(ET)、ThreadPool、HeapTimer、AsyncLog、HTTP解析、ONNX Runtime、信号处理、wrk压测。
答:
这是一个基于 C++ 的高性能 WebServer。核心是 Epoll + 线程池 的并发模型,支持静态文件访问,同时新增了一个 AI 推理接口 POST /api/predict,通过 ONNX Runtime 做在线推理。
答: 我主要做了以下几件事:
- 打通 AI 链路:PyTorch 导出 ONNX,C++ 侧加载并推理;
- 改造 HTTP 处理:新增
/api/predict路由并返回推理结果; - 堆定时器:实现线程安全的 HeapTimer(mutex 保护),O(log n) 超时管理,自动踢掉空闲连接;
- 异步日志系统:有界阻塞队列 + 后台写盘线程,四级日志,按日期自动分割;
- 生产级加固:SIGPIPE/SIGTERM/SIGINT 信号处理、请求体大小限制、优雅关闭;
- 压力测试:wrk 多级并发压测(10~1000 连接),在测试中发现并修复了 6 个并发 Bug;
- 工程化:
.env环境变量配置、一键启动脚本run.sh、完善文档。
答:
- 页面:
GET /predict.html - 推理:
POST /api/predict,请求体是一个数字字符串,比如50,返回Result: 150.000000。
答: C++ 在系统编程和高性能网络场景有优势,尤其在连接管理、内存控制、零拷贝、低延迟方面更有可控性。
答:
主线程负责监听与事件分发:epoll_wait -> 区分 listen/read/write/error;读写处理通过线程池异步执行;HttpConn 负责请求解析和响应构造;HeapTimer 管理连接超时(O(log n) 堆+哈希表);AIEngine 负责模型加载与推理;Log 异步日志系统通过有界阻塞队列将日志写盘与业务线程解耦。所有模块通过 LOG_INFO/WARN/ERROR 宏输出日志,后台线程顺序写入 log/YYYY_MM_DD.log。
答:
ET 能减少重复事件通知,降低 epoll 唤醒次数,在高并发下吞吐更好。但代价是必须配合非阻塞 IO,并在读写时循环读/写到 EAGAIN。
答:
在 HttpConn::Read 里循环 ReadFd,直到 EAGAIN/EWOULDBLOCK 才退出;遇到 EINTR 继续重试;并累加真实读到的字节数,避免丢数据。
答:
防止同一个连接在同一时刻被多个线程并发处理,避免竞态。处理完成后再通过 ModFd 重新挂回读/写事件。
答:
静态文件用 mmap 映射到内存,响应时使用 writev 聚合发送(响应头 + 文件内容),减少用户态到内核态的数据拷贝。
答:
主线程只做事件检测和任务分发,真正的 OnRead_/OnWrite_ 放到线程池执行,避免主线程被耗时处理阻塞,从而提升并发能力。
答:
解析 Connection: keep-alive 决定是否保持长连接。写完响应后:
- 长连接:继续监听读事件;
- 短连接:关闭连接释放资源。
答:
解析 HTTP 请求体 -> stof 转 float -> 调用 AIEngine::Predict -> 构造纯文本 HTTP 响应。
答: 在 wrk 压力测试中一共发现了 6 个并发 Bug:
- 无限递归:
CloseConn_调用doWork()→ 执行回调 → 再次调用CloseConn_。修复:新增cancel()方法,只删除不执行回调; - 双重删除:
tick()先执行回调再pop(),回调中cancel()操作已被处理的节点。修复:先del_(0)删节点,再执行回调; - assert 崩溃:高并发下
assert(users_.count(fd)>0)和assert(!heap_.empty())触发。修复:替换为if检查 +continue; - 双重 close(fd):
HttpConn::Close()没有标记fd_ = -1,导致同一个 fd 被关闭两次。修复:close 后立即fd_ = -1; - 线程安全:HeapTimer 被主线程(tick/GetNextTick)和工作线程(cancel/adjust)并发访问。修复:所有公共方法加
std::mutex; - listen backlog 过小:
listen(fd, 6)导致高并发下连接被拒。修复:改为使用系统上限(当前环境为 4096),并启用SO_REUSEPORT。
这个排障过程是面试超级加分项,展示了压测 → 崩溃 → 分析核心转储 → 修复 → 回归验证的完整工程闭环。
答:
AIEngine::Predict 内部用 std::mutex 加锁,确保共享 session_ 并发访问时状态一致,避免多线程推理竞态问题。
答: ONNX 模型加载开销大,单例能避免重复加载,减少内存占用与初始化耗时;服务启动时加载一次即可。
答:
因为直接运行二进制通常不会自动 source .env。我在 main.cpp 增加了 .env 自动加载逻辑(支持当前目录和上一级目录),让 build/server 直接启动也能拿到配置。
答: 会导致服务直接崩溃,不够生产化。更稳妥是记录详细错误、降级重试或优雅退出并给出可观测日志。
答: 先分层定位:
- 网络层:epoll 事件积压、accept/read/write 热点;
- 线程池:队列长度、任务等待时间;
- 推理:模型推理耗时、锁竞争;
- 系统资源:CPU 抢占、上下文切换、内存缺页。
答: 已将推理路径改为 Session 池并发,当前主要瓶颈转为网络层与系统资源参数(连接队列、内核调度、压测机能力)。后续可继续做批处理、NUMA/绑核优化。
答:
最典型是“没有读到 EAGAIN 就返回”,会导致剩余数据不再触发读事件,表现为请求卡住或解析不完整。
答:
要区分 EAGAIN 与真实错误,解析层要支持半包;写路径要正确维护 iov 偏移,确保未发送完的数据继续监听 EPOLLOUT。
答:
- 先做可观测(请求耗时分位、线程池队列、推理耗时);
- 优化热点(锁、内存拷贝、系统调用次数);
- 再做架构优化(推理并行、IO/业务线程隔离)。
答: 已经实现的:
- ✅ 异步日志系统(四级日志,按日期分割);
- ✅ 优雅退出(SIGTERM/SIGINT 信号处理,设置
g_shutdown标志位); - ✅ 请求体大小限制(防 OOM,默认 1MB);
- ✅ 连接超时管理(HeapTimer 自动踢空闲连接);
- ✅ SIGPIPE 处理(
signal(SIGPIPE, SIG_IGN)); - ✅ wrk 多级压测验证。
还需补的:
- 完善的 QPS/P99/错误率指标监控;
- 统一错误码与接口协议;
- 接口自动化测试;
- 配置中心化管理(如用于多实例部署)。
答:
已完成:异步日志 ✅、堆定时器 ✅、信号处理 ✅、请求体限制 ✅、压测报告 ✅、assert(false) 替换 ✅。
下一步:
- 推理链路耗时分位指标(P50/P99);
- 接口自动化测试(回归验证);
- 推理批处理(micro-batch)与线程亲和优化,进一步提升吞吐与尾延迟稳定性;
- HTTP/1.1 chunked transfer 支持。
Q:项目里最有挑战的一次排障?
答: 我遇到过 ONNX 模型能导出但服务加载失败的问题。先从报错里定位到 IR version 不兼容,再验证运行时版本上限,最后把导出后的模型版本做兼容处理并固化到流程里。这个过程让我意识到“能跑”不等于“可复现”,所以后续我补了 .env、run.sh 和文档,保证别人也能稳定复现。
- 先讲结论,再讲证据(日志/代码路径/复现步骤)。
- 遇到缺陷不要回避,强调“怎么定位 + 如何修复 + 如何防再发”。
- 每道题尽量落到你项目中的具体实现点,避免空泛。
- 架构:
Epoll(ET+ONESHOT) + 线程池 + AI Session 池并发推理。 - 性能点:非阻塞循环读写、
mmap + writev。 - 稳定性:错误分支处理、连接生命周期管理。
- 工程化:
.env、run.sh、README、一键复现。 - 典型 bug:请求体未落盘导致预测恒为 0;ONNX IR 版本不兼容。
建议:面试前把本文件中的前 15 题至少口述一遍,做到“闭眼可讲”。
以下是面试官极有可能根据你的代码细节继续追问的方向。
答:
Reactor。主线程负责事件检测(epoll_wait),工作线程执行实际 I/O 与业务逻辑。
Proactor 是异步 I/O 模型,内核完成 I/O 后通知用户态,典型实现是 Windows IOCP 或 Linux io_uring。
我的项目属于 主从 Reactor 单 Reactor + 线程池 的变体:一个主线程做事件分发,多个工作线程做读写处理。
答:
backlog 控制全连接队列(已完成三次握手但尚未被 accept)的长度上限。设太小在高并发下会导致 SYN 被丢弃或连接被 RST。
当前设 6 是演示级别,生产环境通常设 SOMAXCONN(一般 128 或 4096,取决于内核参数 net.core.somaxconn)。
答:
SO_REUSEADDR 允许在 TIME_WAIT 状态下复用端口,防止服务重启时 bind 失败。
相关的还有 SO_REUSEPORT(允许多进程/线程绑定同一端口做负载均衡)和 TCP_NODELAY(关闭 Nagle 算法,降低短报文延迟)。
答:
当前代码没有处理 SIGPIPE。
如果对已关闭的连接执行 write/writev,内核会发送 SIGPIPE,默认行为是终止进程。生产环境必须忽略它:signal(SIGPIPE, SIG_IGN) 或在 send 时加 MSG_NOSIGNAL,然后通过返回值的 -1 / EPIPE 来判断。
答: 当前没有信号处理逻辑,进程会直接终止,可能导致:
- 正在处理的请求被中断;
mmap没有munmap、fd 没有close(系统会回收,但不够优雅)。
改进方案:注册信号处理函数,设isClose_ = true,让主循环退出后析构函数正常执行。
答:
会阻塞主线程,导致已连接客户端的读写事件长时间得不到处理(饥饿)。
改进:限制每轮 accept 次数(例如最多 64 个),剩下的靠 EPOLLET 在下一轮再处理;或者使用多 Reactor,把 listen 和 conn 拆到不同线程。
答:
从当前使用场景看,同一个 HttpConn 加了 EPOLLONESHOT 保护,同一时刻只有一个线程操作一个连接的 Buffer,理论上不需要 atomic。
但 atomic 在这里的开销极小(x86 上 relaxed 操作相当于普通读写),留着可以做防御性编程。如果将来 Buffer 需要跨线程使用(如 Producer-Consumer),则有实际价值。
答:
readv (分散读)可以一次系统调用同时往两个缓冲区写:iov[0] 是 Buffer 的剩余空间,iov[1] 是栈上 65535 字节的临时空间。
好处:大多数情况下数据直接进 Buffer 不需要扩容(一次系统调用完成);只有数据溢出时才 Append 到 Buffer 触发扩容。减少了系统调用次数和不必要的 resize。
答: 风险:
- 映射的文件在运行期被删除或截断会触发
SIGBUS; - 大文件会消耗大量虚拟地址空间,32 位系统几乎不可用;
- 映射过多导致页表膨胀。
改进:大文件改用sendfile零拷贝直接从 fd 到 socket,不经过用户态;或分块读写。
答:
在 HttpConn::Write 中,我循环调用 writev,每次根据实际写入字节数调整 iov_[0] 和 iov_[1] 的 iov_base 和 iov_len。如果 errno == EAGAIN(内核发送缓冲区满了),就退出循环,让 OnWrite_ 中重新挂 EPOLLOUT 等下一次可写事件继续发。
答:
detach 后线程生命周期不受控,主线程/析构结束后线程可能还在运行。在我的实现里,通过 shared_ptr<Pool> 保证 Pool 对象在最后一个线程退出前不被销毁,所以安全。
但如果线程池析构时工作线程正在执行一个耗时任务,这个任务会"野跑"直到完成。更严格的做法是析构时 join 等所有线程退出。
答:
因为 detach 出去的线程持有 pool 的拷贝(lambda 里 capture by value),如果是 unique_ptr 就无法共享所有权。shared_ptr 保证:即使 ThreadPool 对象被销毁,只要还有线程在运行,Pool 就不会被释放。
答:
这是完美转发。F&& 是万能引用,std::forward 保留实参的左/右值属性:
- 传入右值(如 lambda 临时对象)→ 移动;
- 传入左值 → 拷贝。
避免不必要的拷贝,提升性能。
答:
unordered_map 在 rehash 时会使所有迭代器失效,但不会使引用失效(C++17保证 node stability),不过指向元素的指针在 rehash 前后地址不变。
风险点:DealRead_ 把 &users_[fd] 指针传给线程池,如果此后主线程执行了 users_[newFd] 导致 rehash,指针可能失效(C++ 标准保证 unordered_map 的引用在 rehash 后仍有效,但取地址行为需要谨慎)。更安全的做法是改用 unordered_map<int, unique_ptr<HttpConn>>,这样指针不受 rehash 影响。
答:
accept→AddClient_: 创建HttpConn、设非阻塞、挂EPOLLIN;EPOLLIN→DealRead_→ 线程池执行OnRead_: 循环读到EAGAIN;OnProcess_: 解析 HTTP → 生成响应 → 挂EPOLLOUT;EPOLLOUT→DealWrite_→ 线程池执行OnWrite_: 循环写到完毕或EAGAIN;- Keep-Alive → 回到步骤 2;短连接 →
CloseConn_: 从 epoll 删除、close fd。
答:
当前实现用 \r\n 逐行解析,如果没找到完整请求行或 \r\n\r\n(头部结束标记),Process() 返回 false,OnProcess_ 会重新挂 EPOLLIN 等待更多数据。
但请求体长度目前没有对照 Content-Length 逐字节检查,如果 POST body 只到了一半就开始解析,可能会截断或解析错误。更完善的做法是引入状态机标记解析进度,分多次 read 累积。
答:
非常不安全。srcDir 参数是 const char*,const_cast 去掉 const 后如果通过指针修改了字符串(尤其是指向字符串字面量时),是未定义行为。
改进:把 srcDir_ 成员变量改为 std::string,从根本上消除裸指针和 const 转换问题。
答:
- 智能指针:
unique_ptr<Epoller>、unique_ptr<ThreadPool>、shared_ptr<Pool>; - Lambda 表达式:线程池任务、线程创建;
- 完美转发:
std::forward<F>inAddTask; - 移动语义:
std::move(task)取队列任务; - 原子变量:
atomic<int> userCount_、atomic<size_t> readPos_; - 条件变量:
condition_variable线程池等待/唤醒; - auto / 初始化列表:全项目大量使用;
thread+detach:线程池创建。
答:
头文件被其他文件 #include 后,using namespace std 会污染所有包含者的全局命名空间,可能导致命名冲突(如与第三方库的 string、vector 等同名符号冲突)。
正确做法:头文件里只用 std:: 前缀,using namespace std 只放在 .cpp 文件中。
答: 有几个潜在风险:
- 路径遍历:如果请求
GET /../../../etc/passwd,没有做路径规范化检查,可能泄漏系统文件; - 请求体大小无限制:没有检查
Content-Length,恶意客户端可以发超大 body 耗尽内存; - Header 注入:没有校验 Header 值中的
\r\n,可能被注入额外响应头; - 慢速攻击(Slowloris):没有超时机制,客户端可以建连后长时间不发数据,耗尽连接资源。
改进:加路径白名单、body 大小上限、连接超时定时器、请求速率限制。
- Reactor 模型:主线程检测事件,工作线程做 I/O
readv+ 栈 buffer:减少系统调用和不必要扩容detach+shared_ptr<Pool>:保证线程安全退出listen(fd, SOMAXCONN):生产环境不能用小 backlog- 必须处理
SIGPIPE:否则 write 对端关闭直接崩 - 缺定时器:无法剔除空闲连接(Slowloris 攻击)
const_cast/using namespace std头文件:典型 C++ 代码异味
建议:在已背熟前 28 题的基础上,再补 Q29–Q32(网络基础)、Q39–Q41(线程池实现)、Q45–Q48(C++ 语言 + 安全)这几题,覆盖面试官最爱追问的底层细节。
面试官最爱追问:"你为什么选 X 而不是 Y?" 以下按模块对比各方案的优缺点,以及本项目的选型理由。
| 方案 | 优点 | 缺点 |
|---|---|---|
| select | POSIX 标准,跨平台 | fd 数量限制(默认 1024);每次调用都要拷贝 fd_set 到内核;O(n) 遍历所有 fd |
| poll | 去掉 1024 限制,用 pollfd 数组 |
仍需每次拷贝整个数组到内核;O(n) 遍历 |
| epoll | O(1) 事件通知(红黑树 + 就绪链表);fd 数量仅受系统限制;支持 ET 模式 | 仅 Linux;学习成本稍高 |
| io_uring | 真异步 I/O,零系统调用开销(共享环形缓冲区);支持批量提交 | 仅 Linux 5.1+;API 复杂度高;生态不够成熟 |
| kqueue | macOS/BSD 上的高性能方案,类似 epoll | 不能在 Linux 用 |
选型理由: 项目部署在 Linux 上,epoll 是 Linux 高并发网络编程的事实标准,性能远优于 select/poll。io_uring 虽然更先进,但对内核版本有要求且 API 复杂度高,作为学习项目 epoll 已经足够展示核心原理。
| 模式 | 优点 | 缺点 |
|---|---|---|
| LT(水平触发) | 编程简单,不会漏读数据;缓冲区有数据就一直通知 | 事件重复通知,高并发下 epoll_wait 返回次数多,性能略差 |
| ET(边缘触发) | 事件只通知一次,减少唤醒次数,吞吐更高 | 必须配合非阻塞 I/O + 循环读写到 EAGAIN,否则会丢数据;编程复杂度高 |
选型理由: ET 在高并发场景下减少了不必要的事件通知,提升吞吐。虽然编程要求更高(循环读写 + 非阻塞),但也正好体现对底层的掌握——这也是面试加分项。
| 模型 | 优点 | 缺点 |
|---|---|---|
| 一请求一线程 | 编程最简单,逻辑清晰 | 线程创建销毁开销大;高并发下线程数爆炸,上下文切换严重 |
| 一请求一进程(如 Apache prefork) | 进程隔离,稳定性好 | 创建开销更大;进程间通信复杂;内存占用高 |
| 线程池 | 线程复用,避免反复创建销毁;控制并发度;配合任务队列解耦 | 需要处理线程安全(锁、原子变量) |
| 协程(如 libco / C++20 coroutine) | 用户态切换开销极低;可支撑百万级并发 | C++ 协程生态不成熟;调试困难;学习曲线陡峭 |
选型理由: 线程池是 Reactor 模型的经典搭配:主线程做事件检测 + 分发,工作线程做 I/O 和业务处理,兼顾性能和资源控制。相比一请求一线程避免了线程爆炸问题;相比协程方案在 C++14 下更成熟可控。
| 方案 | 优点 | 缺点 |
|---|---|---|
| read + write | 最简单直接 | 两次系统调用 + 两次用户态内核态拷贝(read: 内核→用户; write: 用户→内核) |
| mmap + writev | mmap 跳过用户态拷贝(直接映射内核页缓存);writev 聚合发送头 + 体,一次系统调用 |
需要 munmap 管理生命周期;大文件虚拟地址空间压力大;文件被修改可能 SIGBUS |
| sendfile | 全内核态完成文件到 socket 的传输,零用户态介入;最高效 | 无法在发送前修改文件内容(如加 HTTP 头);需要配合 send 单独发响应头 |
| splice / tee | 管道级零拷贝,更灵活 | API 复杂,适用场景有限 |
选型理由: HTTP 响应需要把 响应头(动态生成) 和 文件体(静态) 聚合发送。mmap 映射文件后与写缓冲区里的响应头通过 writev 一次性 scatter 发出,既减少拷贝又保持了头体合并的灵活性。sendfile 虽然更高效但不方便和响应头一起发。
| 方案 | 优点 | 缺点 |
|---|---|---|
| ONNX Runtime | 跨框架(PyTorch/TF 都能导出 ONNX);C++ API 简洁;CPU/GPU 通吃;部署包小 | 对超新模型算子支持可能滞后;版本兼容需注意 IR version |
| TensorRT | NVIDIA GPU 上推理性能最强;支持量化和层融合优化 | 仅支持 NVIDIA GPU;API 复杂;编译优化耗时长;不支持 CPU |
| LibTorch(PyTorch C++) | 与 PyTorch 完全兼容;动态图支持好 | 部署包巨大(>300MB);C++ API 文档不如 Python 完善;序列化格式绑定 PyTorch |
| TFLite | 移动端/嵌入式场景优秀;包体小 | 主要面向 TensorFlow 生态;算子覆盖有限 |
选型理由: 项目在 CPU 上运行,不依赖 NVIDIA GPU,排除了 TensorRT。LibTorch 部署包太大且与 PyTorch 强绑定。ONNX Runtime 轻量、跨框架、C++ API 友好,最适合"PyTorch 训练 → C++ 部署"的链路。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 单例(全局唯一 session) | 模型只加载一次,内存省;实现简单 | 推理必须加锁,成为并发瓶颈 |
| 每请求实例化 | 完全无锁 | 每次加载模型开销巨大(IO + 内存分配),不可接受 |
| 会话池(session pool) | 多个 session 并行推理,吞吐线性提升;无需全局锁 | 内存占用翻倍;管理复杂度增加 |
选型理由: 当前模型很小(SimpleAdd),单例 + 互斥锁即可满足需求。如果将来模型变大或 QPS 上去了,应改为会话池方案——预创建 N 个 OrtSession,通过队列分配和回收。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 手写解析 | 完全可控;面试能展示对 HTTP 协议的理解;无外部依赖 | 功能不完整(不支持 chunked、multipart 等);安全性不如成熟库 |
| http-parser(Node.js 同款) | 久经考验;零分配回调式 API | 已停止维护,被 llhttp 替代 |
| llhttp | Node.js 现用解析器;性能极强;主动维护 | 引入外部依赖;回调式 API 学习成本高 |
| 直接用 Nginx 模块 | 生产级性能和安全 | 强耦合 Nginx 架构;不适合学习项目 |
选型理由: 作为学习项目,手写解析器的目的是展示对 HTTP 协议(请求行、头部、body)的掌握,面试官能直接看到你理解状态机解析。如果生产使用则建议替换为 llhttp 等成熟库。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 硬编码 | 最简单 | 敏感信息泄漏;换环境得重新编译 |
| 命令行参数 | 灵活,--port 8080 |
参数多了很长;不好管理密码类敏感信息 |
| 配置文件(JSON/YAML/INI) | 结构化,可读性好 | 需要引入解析库;仍需防止提交到 Git |
| 环境变量 + .env | 12-Factor App 标准;Docker/K8s 原生支持;.gitignore 防泄漏 |
不能表达层级结构;类型全是字符串 |
| 配置中心(Apollo/etcd) | 集中管理、热更新、版本控制 | 架构过重,不适合单体项目 |
选型理由: .env + 环境变量 符合 12-Factor App 规范,是从开发到容器化部署的最佳实践。Docker 的 --env-file、K8s 的 ConfigMap/Secret 都原生兼容。对于本项目体量,这是最实用的方案。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 手写 Makefile | 完全控制编译过程 | 维护成本高;跨平台困难;依赖管理麻烦 |
| CMake | C++ 生态事实标准;跨平台生成构建文件;IDE 支持好(CLion、VS Code) | 语法晦涩(虽然 Modern CMake 改善了很多) |
| Bazel | 增量编译极快;大型项目(Google 级)表现优异 | 学习成本高;C++ 生态中小项目不常用 |
| xmake | 语法简洁(Lua-based);中文社区友好 | 生态和社区不如 CMake 大;企业采用率低 |
选型理由: CMake 是 C++ 开源项目的事实标准,大多数公司和开源库都在用。VS Code + CMake Tools 插件体验良好,compile_commands.json 还能提供代码补全和跳转支持。
| 方案 | 优点 | 缺点 |
|---|---|---|
固定数组 HttpConn conns[MAX_FD] |
O(1) 查找;无动态分配;缓存友好 | 浪费内存(需预分配 65535 个对象);fd 值必须在范围内 |
std::map<int, HttpConn> |
有序;红黑树实现,稳定 | O(log n) 查找;节点分配碎片化 |
std::unordered_map<int, HttpConn> |
O(1) 平均查找;按需分配 | 有 rehash 开销和迭代器失效风险;哈希冲突时退化 |
选型理由: 连接查找是最频繁的操作(每个 epoll 事件都要通过 fd 找到 HttpConn),需要 O(1) 复杂度。unordered_map 兼顾了查找效率和内存按需分配。如果追求极致性能,可以改用预分配数组方案(muduo 就是这么做的)。
面试官:你为什么选 X?
答: 我对比了 A/B/C 三种方案。A 的优点是……但缺点是……;B 的优点是……但缺点是……;我最终选了 X,因为在我的场景下(具体原因)。如果将来需求变化(如 QPS 上去 / 部署到 GPU / 团队扩大),我会切换到 Y 方案。
这个模板的核心:你不是随便选的,而是对比过的 —— 这就是面试官想听到的。