20260901练习
题目1 如何测试一个大模型推理服务
假设服务提供如下接口:
POST /v1/chat/completions请求示例:
{"model":"xxx","messages":[{"role":"user","content":"你好"}],"stream":true}请整理一份 3~5 分钟的面试回答,至少覆盖以下内容:
- 功能测试需要覆盖哪些场景?
- 如何测试流式输出?客户端中途断开连接时如何验证?
- 如何设计并发测试和压力测试?
- 什么是 TTFT、TPOT、E2E Latency、吞吐量和 P99 延迟?
- 输入长度、输出长度和并发数分别会怎样影响性能?
- 如果 P99 延迟突然升高,如何定位问题?
- 如何测试服务重启、GPU 不可用、请求超时和下游异常?
回答:
q1. 功能测试需要覆盖的基本场景如下,按照优先级排序:
p0 基本功能,如不同的模型id,构造不同的message类型,stream的true or false,是否返回预期内容,比如传入1+1 =? 模型是否正确的返回了答案等
p0 逻辑部分,是否依赖于其他前置接口,如auth等
p1 异常边界测试,如异常场景,参数异常等
p1 安全测试cookie token等认证
q2. 流式接口测试的核心理念是响应是否持续到大以及每个event是否都正确,因此要测试流式接口主要从以下步骤入手
s1. 确定流式接口的传输形式:
SSE: 标志为header中的Content-Type为text/event-stream
HTTP-Chunked:标志为 header中字段为transfer-encoding为chunked
WebScoket:连接建立后双向收发形式。
如果是前两种,则可以直接使用curl进行验证,如果是websocket,则需要编写代码或者专用客户端
s2. 应该验证断言内容如下:
- 状态码
- 首包及其响应时间
- 事件格式是否符合要求
- 事件顺序
- 事件是否正确结束
- 持续性与完整性
- 连接是否正常关闭
- 事件延迟,间隔,总耗时
- 事件结束后,服务端是否正确停止和生成资源
s3. 异常与压力测试
q3. 相较于传统的应答式接口,流式接口的测试应该从两个角度或者说测试模型入手。
a. 固定并发模型
能稳定维持多少个流式连接
b. 到达率模型
每秒能承受多少个新建请求,比如不去管先前的模型,而是固定的按时间启动固定数量的流
活跃并发数 = 每秒新请求 * 平均流请求时间
基于上述模型,需要进行以下阶段的测试
s1.基线测试,少量并发请求,确认接口和客户端逻辑基本正确
s2. 阶段性的加压测试:持续增加并发数量,每档保持一定量级的时间
s3. 临界点: 找到开始出现异常的时间点
s4. 突发异常流量测试: 短时间内从0至大并发,观察连接与服务器负载
s5. 稳定性测试:正常负载下持续并发请求与测试
s6. 取消与重试测试:随机中断连接,验证服务器资源是否正常
q4. TTFT: time to fisrt token,从请求发出到收到的第一个token,首token时间
TPOT:time per output token,首token后生成每个token所需要的平均时间,持续生成速度,Decode效率,流式输出是否顺畅的主要衡量项
E2E Latency: end-to-end latency 从请求开始到完整请求的总耗时
吞吐量:单位时间内处理的请求数,Token数和字节数
p99延迟:延迟分布的第99百分位,同样的p95和p50分别是95和50百分位,他们保证的分别是大多数用户,一般用户的体验
q5.
a. 输入长度对性能的影响主要体现在 TTFT 显存与最大并发等指标上,这是因为输入的内容在经过模型计算后,需要去建立KV Cache,都是些需要去消耗资源去做的事
输入长度越长<通常意义上来讲也就是说模型需要去理解的事情越复杂>,也就是模型需要去思考的事情变得更多了,那么思考阶段的成本也就变高了。
因此就会导致TTFT增大,E2E的延迟也会增大,模型思考阶段的KV Cache增加,显存需求量更大,输入阶段的注意力计算量也需要更大,此外,更长的输入也意味着,后续每个输入的token需要关注的上下文也越来越长,因此Decode阶段的计算和显存访问也会显存增加。
b. 输出长度主要会导致E2E的延迟变长,以及连接占用的时间变久,同时输出变长,每一个输出的token都需要调用一次decoder,所以decode阶段对显存的需求也会增长
c. 并发长度:并发数量则直观的影响系统资源,无需多言
三者情况组合的话,大概是如下组合:
长输入 + 高并发,容易显存不足;
长输出 + 高并发,容易连接堆积和排队;
长输入 + 长输出 + 高并发,P99超时 OOM等,最不健康的状态
q6. 定位途径 当前请求数量/负载 -> 系统资源<cpu/内存/显存> -> 服务状态 -> 网络状态
q7:
a. 预期流程重启,检查原请求是否正确路由或者说负载到了备用服务上,检查是否有新请求进来
b. 强制重启,预料外的断开,检查入口是否正确启动了负载,客户端是否正确发送了重试请求,是否正确路由到了新的服务器,原有长链接是否被正确关闭, 服务器重启后是否存在僵尸进程,残留进程,协议栈中连接是否正确释放
c. GPU异常应测试GPU调度异常,CUDA设备无法识别,GPU OOM,模型失败,GPU实例负载失败
具体方法有:
- 修改GPU配置以模拟调度异常
- 暂时屏蔽或者短线GPU模拟CUDA无法识别
- 减小GPU可用内存模拟显存OOM
- 模拟模型的返回给下游,返回异常
d. 请求超时,需要模拟的异常情况主要有首token超时,流中超时,E2E超时
f. 下游异常返回,mock异常返回即可
20260902练习
假设你需要用 Go 编写一个简单的大模型推理服务压测工具,要求如下:
- 总共发送 1000 个请求
- 最大并发数为 20
- 每个请求超时时间为 5 秒
- 支持通过
Ctrl+C取消测试 - 统计成功数、失败数、超时数、平均延迟和 P99 延迟
- 程序退出时不能遗留 goroutine 或 HTTP 连接
请整理一份 3~5 分钟的面试回答,至少覆盖以下内容:
- 如何使用 goroutine 和 channel 组织并发请求?
- 如何限制最大并发数?有哪些实现方式?
- 全局取消、单请求超时和程序退出之间是什么关系?
- 多个 goroutine 如何安全地统计测试结果?
- 如果收到
Ctrl+C,应该如何停止新请求并处理正在执行的请求? - 如何验证程序没有 goroutine 泄漏?
- “固定并发”与“固定每秒请求数”两种压测模型有什么区别?
- 如果 P99 延迟升高,你会优先查看哪些指标?
回答:
q1. 如何使用 goroutine 和 channel 组织并发请求?
任务队列 + 固定数量 worker模型:
主 goroutine 将请求任务写入jobschannel,启动不超过最大并发数的 worker;
每个 worker 循环读取任务、执行请求,并将结果写入resultschannel。
这样做的好处是可以复用固定数量的 goroutine,不需要为 1000 个请求一次性创建 1000 个 goroutine。jobschannel 负责分发任务,resultschannel 负责传递结果;
q2. 如何限制最大并发数?有哪些实现方式?
常见实现方式有三种:
- 固定数量 worker:直接启动
concurrency个 worker,每个 worker 同时只处理一个任务。 - 带缓冲 channel 作为信号:启动任务前向容量为
concurrency的 channel 写入一个令牌,任务结束后取出令牌。 - semaphore:使用
golang.org/x/sync/semaphore管理并发许可。
同时,并发实现时需要配合sync waitgroup,锁等机制来配合使用并保证并发安全,防止出现脏数据
q3. 全局取消、单请求超时和程序退出之间是什么关系?
- 全局取消:任务整体被停止,比如收到操作系统层面的
SIGINT信号后调用context 的cancel()。
实现全局取消的前提是所有任务、worker 和请求都应该监听这个 context。 - 单请求超时:在context 下为某个请求创建
context.WithTimeout子 context,只影响该请求;
超时后应记录为超时结果并释放资源。 - 程序退出:程序预期中的退出,任务完成,优雅的退出并清理释放资源
q4. 多个 goroutine 如何安全地统计测试结果?
有三种可行方案:
- worker 只负责发送
Result,由一个 collector goroutine 串行更新计数器和延迟数组; - 对共享计数器或切片使用
sync.Mutex保护;这也是最简单和常用的方法 - 对简单的成功数、失败数使用
sync/atomic,延迟明细仍由单独 collector 收集。
q5. 如果收到 Ctrl+C,应该如何停止新请求并处理正在执行的请求?
收到Ctrl+C后调用根 context 的cancel()。任务生产者在投递任务前监听ctx.Done(),取消后不再投递新任务;worker 在取任务和执行任务时也监听ctx.Done()。
q6. 如何验证程序没有 goroutine 泄漏?
可以采用以下方法:
- 在测试前后记录
runtime.NumGoroutine(),重复执行多轮取消测试,观察数量是否持续增长; - 使用
pprof.Lookup("goroutine")或runtime/trace查看 goroutine 的调用栈和生命周期; - 使用
go.uber.org/goleak等测试工具辅助检测; - 为 jobs、results 和 worker 的退出路径设置明确的关闭和等待逻辑;
- 使用
go test -race检查数据竞争,但要明确:-race不能替代 goroutine 泄漏检测。
q7. “固定并发”与“固定每秒请求数”两种压测模型有什么区别?
- 固定并发:始终维持固定数量的活跃请求。一个请求完成后,才补充下一个请求。它形式上比较类似于并发测试
- 固定请求:按照固定速率产生新请求,例如每秒启动 20 个请求,不等待之前的请求完成。形式上比较类似于新建测试
q8. 如果 P99 延迟升高,你会优先查看哪些指标?
“确认现象 → 拆分链路 → 关联资源 → 验证根因”的顺序定位:
- 确认时间窗口、样本量和统计口径,对比 P50、P95、P99、错误率和吞吐量,判断是整体变慢还是少量长尾变慢。
- 查看最近是否发生版本发布、模型切换、配置修改、扩缩容或流量结构变化,特别关注输入和输出 Token 长度分布。
- 将端到端延迟拆成网关、排队、Prefill、Decode、网络传输和下游依赖耗时。
- 查看推理服务的排队长度、batch 大小、TTFT、TPOT、GPU 利用率、显存和 KV Cache 占用,以及 OOM 和重试情况。
- 查看 CPU、内存、GC、连接数、负载均衡是否倾斜、网络丢包/重传和下游服务状态。
- 根据 request ID 抽取 P99 慢请求,关联日志和 Trace,最后通过回滚、降低流量、隔离实例或复现实验验证根因。
不能因为只有 P99 升高就直接排除网络问题;网络抖动、连接复用异常和负载均衡倾斜都可能主要表现为长尾延迟。
题目2 Go 小型工程练习:实现一个可取消、限并发的任务执行器
请实现下面的函数,暂时不要求真正调用 HTTP 接口:
typeResultstruct{SuccessboolLatencyMsint64Errerror}funcRunLoadTest(ctx context.Context,totalint,concurrencyint,)[]Result要求如下:
- 使用
context.Context支持任务取消。 - 最大并发数不能超过
concurrency。 - 每个任务随机模拟 10~100 毫秒的执行耗时。
- 收集每个任务的执行结果。
ctx取消后,不再启动新任务。- 已经开始执行的任务应能够感知取消并尽快退出。
- 结果收集不能产生数据竞争。
- 使用
go test -race检查并发安全性。 - 思考如何验证程序没有 goroutine 泄漏。