谢飞机面试奇遇记:从JVM调优到Kafka延迟,一场关于音视频UGC与微服务的终极拷问

📅 2026/8/22 2:50:45
谢飞机面试奇遇记:从JVM调优到Kafka延迟,一场关于音视频UGC与微服务的终极拷问
谢飞机面试奇遇记从JVM调优到Kafka延迟一场关于音视频UGC与微服务的终极拷问“下一个谢飞机。”面试官老张推了推眼镜看着眼前这位简历上写着“精通Java全栈熟悉高并发秒杀曾参与过亿级流量项目”的年轻人。谢飞机穿着格子衬衫背着双肩包头发有些凌乱脸上挂着自信又心虚的笑容。“坐吧我们随便聊聊。先做个自我介绍”谢飞机清了清嗓子“面试官您好我叫谢飞机三年Java开发经验主要做后端熟悉Spring Boot、MyBatis、Redis这些之前在公司负责过用户模块和内容审核系统的开发。平时喜欢研究源码也写技术博客。”老张点点头翻开第一轮。第一轮JVM与核心语言从“内存溢出”开始面试官老张“你提到了内容审核系统那这个系统肯定要处理大量图片和文本对吧假设有一批用户上传的音视频元数据需要异步解析你负责的服务突然CPU飙升、频繁Full GC你一般怎么排查先说说JVM内存结构里哪些区域可能出问题”谢飞机“这个我熟JVM内存分堆、栈、方法区……堆里面又分新生代和老年代。如果Full GC频繁大概率是堆内存不够或者有对象没办法回收。我一般先看GC日志再用jstat看一下各个区的使用率再用jmap dump堆内存用MAT分析有没有大对象。”面试官老张“嗯基础不错。那如果dump出来发现是很多byte[]对象比如图片的Base64字符串或者音频二进制流占了很大空间你会怎么优化”谢飞机“啊……那可能就是代码里一次性把大文件读进内存了。比如用FileInputStream.readAllBytes()或者把图片Base64编码後直接存到Redis。可以改用流式读取或者用MappedByteBuffer再或者让前端直接传OSS的URL后端只存元数据。”面试官老张“很好。那再问一个稍微深入点的JVM的堆内存为什么要分代如果不分代用复制算法一直清理会有什么问题”谢飞机“这个……分代是为了效率吧。因为大部分对象朝生夕死新生代用复制算法快。如果不分代所有对象放在一起每次GC都要扫描全堆而且标记-清除容易产生碎片复制算法又浪费空间……嗯……反正就是性能不行。”面试官老张“回答得算可以至少方向没错。我们接着聊框架。”第二轮Spring生态与数据访问从“缓存穿透”到“分布式事务”面试官老张“你们内容社区有热门榜单比如用户刷信息流热点内容访问量极高。你用Redis做缓存但遇到了缓存穿透大量请求打到数据库。你是怎么解决的”谢飞机“缓存穿透嘛用布隆过滤器或者缓存空值。我当时是缓存空值设置一个很短的过期时间比如30秒。然后热点key用逻辑过期加上互斥锁重建缓存。还有Cache-Aside模式。”面试官老张“嗯那如果缓存和数据库双写怎么保证一致性比如你更新了用户的头像先改数据库再删缓存还是先删缓存再改数据库”谢飞机“应该先更新数据库然后删除缓存……不对先删缓存再更新数据库不对不对……有一种情况是缓存删除失败。呃其实我们当时直接用了Spring Cache注解上写CacheEvict但没考虑并发。好像要用延迟双删就是更新数据库后等几百毫秒再删一次缓存。”面试官老张“延迟双删能解决一部分问题但如果是有事务的消息队列比如用户发布一条动态需要同时更新帖子表和粉丝的Feed流你会怎么做这里涉及MySQL和Redis还有可能用消息队列解耦。你能说说最终一致性和分布式事务吗”谢飞机“啊……这个……我们当时用的本地消息表就是给消息加一个状态然后定时任务扫表把失败的重发。没有用Seata啊或者2PC那些。分布式事务我只听过好像挺复杂的有XA方案还有TCC但是具体让我写……可能会写不好。”面试官老张“行能说出本地消息表也算见识过。我们换个方向技术栈里你写了Spring WebFlux和R2DBC用它做过项目吗”谢飞机“呃……这个是最近看的自己写了Demo。WebFlux是响应式编程基于Netty可以高并发地处理IO密集型任务。R2DBC就是响应式JDBC。但是……我还没在线上用过因为公司用的MySQL司机比较多MyBatis也是同步的。”面试官老张“没关系知道概念就行。接下来我们进入第三轮。”第三轮微服务、消息队列与可观测性在“音视频场景”中考察架构设计面试官老张“假设我们要做一个UGC的音视频内容平台用户上传视频需要经过转码、审核、切片然后发布到CDN。这个流程涉及多个微服务比如上传服务、转码服务、审核服务、内容服务。你会怎么设计它们之间的通信需要保证不丢消息同时控制流量突发。用我们技术栈里的Spring Cloud、OpenFeign、Kafka、Resilience4j来聊一聊。”谢飞机“这个场景……我会分两个链路。上传的时候用户先把视频传到OSS然后上传服务发一个Kafka消息给转码服务。转码服务执行FFmpeg然后转完再发消息给审核服务。审核通过后内容服务把视频信息写入MySQL和Elasticsearch。服务之间同步调用用OpenFeign异步用Kafka。为了保证不丢消息Kafka要开acksall然后生产者重试消费者要手动提交offset并且幂等。流量突发的话……用Resilience4j做限流还有Kafka可以削峰填谷。”面试官老张“说得不错思路是清晰的。那我再问细节如果转码服务执行了一半宕机重启后怎么恢复Kafka消费者是至少一次语义你怎么保证幂等”谢飞机“嗯……转码状态可以存数据库比如有task表状态包括PENDING、RUNNING、SUCCESS、FAILED。重启后扫描RUNNING状态的重新执行或标记失败。幂等的话可以用业务ID去重比如视频ID加转码切片序号消费前查一下Redis或者数据库是否存在。”面试官老张“很好。那继续在微服务调用链中你怎么排查一个接口为什么耗时很长比如用户打开App发现首页推荐接口要3秒才返回你怎么定位瓶颈假设你用了Prometheus、Micrometer、Jaeger和ELK分别用来做什么”谢飞机“我会先用Micrometer把每个方法的执行时间以指标形式暴露给Prometheus然后在Grafana看哪个服务、哪个接口的P99高。再用Jaeger做分布式追踪看调用链路里哪一环慢比如是MySQL查询慢还是调用了别人的接口慢。最后把日志接入ELK搜索具体报错或者慢SQL。”面试官老张“嗯这个回答让我有点惊喜。最后一个问题你说了这么多但总感觉有些模糊。你能具体说一个你排查过得最复杂的线上问题吗从现象、定位到解决用一分钟讲清楚。”谢飞机“……呃……有一次我们服务器CPU飙高我用了top -Hp找到线程用jstack转dump发现是垃圾回收线程频繁运行。然后我调整了JVM参数用G1代替CMS设了MaxGCPauseMillis就把CPU降下来了。”面试官老张“这听起来不像是一个完整的案例。算了时间差不多了。小谢你基础还行但对一些核心原理的理解还不够深回去等通知吧。”谢飞机“好的好的谢谢面试官我回去继续学习”附录三轮面试完整答案详解小白学习版下面把每一轮的问题和答案进行详细的展开在真实的业务场景中带出技术点。请同学们结合上面的故事理解每个问题的意图和最佳回答。第一轮答案JVM与核心语言、音视频元数据解析场景问题1CPU飙升、频繁Full GC如何排差业务场景内容审核系统解析用户上传的音视频文件可能一次性读入大文件、或产生大量临时对象如Base64字符串、缩略图字节数组导致老年代被占满触发频繁Full GC。技术点JVM内存结构堆新生代、老年代、方法区元空间、虚拟机栈、本地方法栈、程序计数器。GC日志通过-Xloggc:gc.log -XX:PrintGCDetails查看Full GC频率和耗时。jstatjstat -gcutil pid 1000查看Eden、Survivor、Old、Metaspace容量及使用率。jmap加MATjmap -dump:formatb,fileheap.bin pid导出堆用MATMemory Analyzer查看Dominator Tree找出大对象和引用链。优化方案使用流式处理如InputStreamBufferedOutputStream不要调用FileUtils.readFileToByteArray()。上传文件时前端直接传到对象存储后端只保存URL如果需要图片处理使用ThumbnailUtil流式缩放并限制尺寸。对于字符串拼接避免在循环里使用改用StringBuilder减少创建StringBuilder和char[]。使用MappedByteBuffer映射大文件避免堆内复制。问题2JVM为什么分代不分代用复制算法会怎样技术点分代假设大部分对象生命周期极短朝生夕死少量对象长期存活。新生代使用复制算法Copying因为存活率低复制成本小只需要将存活对象复制到Survivor区效率高且无内存碎片。老年代使用标记-清除Mark-Sweep或标记-整理Mark-Compact因为存活率高复制算法效率很低而且需要额外空间。不分代的问题如果整个堆统一使用复制算法则需要预留一半内存作为空闲区浪费空间如果统一使用标记-清除则会产生大量内存碎片导致大对象无法分配触发Full GC每次GC都要扫描整个堆STW时间过长。面试官夸赞点能说出“分代是空间换时间、根据不同存活率选用不同算法”就是优秀的回答。第二轮答案Spring生态与数据访问问题1缓存穿透怎么解决业务场景热门榜单查询用户频繁访问一个不存在的ID如恶意刷接口缓存中没有每次都打穿到数据库。技术点缓存空值将key对应的null值也缓存设置较短过期时间如60秒防止恶意攻击。布隆过滤器Bloom Filter在缓存之前用布隆过滤器判定key是否存在不存在直接返回失败避免查询数据库。布隆过滤器会有小概率误判但不至于误杀它只影响“可能不存在”的判断。互斥锁当缓存过期时只允许一个线程去查询数据库并重建缓存其他线程等待防止缓存击穿热点key过期瞬间大量请求打到DB。逻辑过期缓存中不设置过期时间而是存储一个逻辑过期时间字段后台异步刷新注意要处理并发刷新时的锁。问题2缓存和数据库双写一致性怎么办业务场景更新头像、更新个人信息等。技术点Cache-Aside模式读操作先读缓存未命中则读DB然后写缓存写操作更新DB然后删除缓存或更新缓存。为什么更新DB后不能直接更新缓存因为并发下可能多个线程写DB和缓存最终缓存中的数据和DB不一致而且缓存更新失败会更容易导致数据脏读。推荐做法更新数据库后删除缓存。但存在一个经典并发问题线程A读缓存未命中线程B更新DB后删缓存线程A再把旧数据写回缓存。解决方案延迟双删更新DB后等500ms自己评估业务耗时再删一次缓存。消息队列异步删把删除缓存的操作放到MQ保证最终执行。分布式锁在更新DB和删除缓存期间加一个Redis锁禁止其他线程读DB并写缓存。监听binlogCanal监听MySQL binlog变化异步同步到Redis这是真正的最终一致性方案能够解决缓存与数据库不一致的极端问题。问题3用户发布动态涉及帖子表、粉丝Feed流、Redis和MQ如何保证最终一致性业务场景用户发布一条动态需要将动态写入MySQL的帖子表同时将动态的ID推送到关注该用户的粉丝的Feed流通常存Redis ZSetkey是粉丝IDmember是帖子IDscore是时间戳。如果一边成功一边失败就会造成数据不一致。技术点本地消息表经典方案在数据库创建一个消息表插入帖子记录时同时插入一条“消息状态为待发送”的记录。把帖子数据写入本地消息表和业务表放在同一个事务中。后台定时任务扫描状态为“待发送”的消息将消息发送到MQ如Kafka或RabbitMQ。MQ成功发出后更新本地消息状态为“已发送”。消费者Feed流服务消费消息写入粉丝的Redis ZSet。消费者需要开启手动ack并且保证幂等。事务消息RocketMQ支持事务消息先将半消息发送到MQ然后执行本地事务成功后提交半消息消费者才会看到。Kafka没有原生事务消息但可以通过本地消息表实现。分布式事务框架Seata的AT模式、TCC模式等。TCC要求每个参与者提供Try、Confirm、Cancel三个接口实现复杂。对于UGC这种场景本地消息表就够了。问题4WebFlux和R2DBC是什么技术点WebFlux是Spring 5引入的响应式Web框架基于Project Reactor使用Netty作为运行容器处理非阻塞IO适合高并发、IO密集型的场景。R2DBC是响应式关系型数据库驱动允许以非阻塞的方式操作数据库避免阻塞等待JDBC数据库连接。但现实是大多数团队使用MySQL MyBatis同步范式。在项目中使用WebFlux引入R2DBC需要整个链路包括中间件、ORM都是响应式的否则会阻塞。R2DBC和HikariCP传统的连接池是冲突的。面试回答要点能指出WebFlux适合网关、访问量极高的轻量服务不适合业务复杂的CRUD因为响应式编程调试难度高且不如Spring MVC生态成熟。第三轮答案微服务、消息队列、可观测性问题1音视频UGC平台如何处理上传、转码、审核、发布流程业务场景用户上传视频后端需要转码压缩成不同分辨率如720p/1080p、审核涉黄涉政、切片生成HLS的.m3u8和.ts文件然后更新内容服务、建立Elasticsearch索引使得用户可以搜索到。架构设计客户端 - OSS (对象存储) - 上传服务 - Kafka[转码消息] - 转码服务 - Kafka[审核消息] - 审核服务 - 内容服务 - MySQL ES Redis上传服务只负责生成临时凭证STS让客户端直传OSS避免走应用服务器带宽。上传完成后上传服务发送一个包含视频URL、视频ID的消息到Kafka的video-transcode主题。转码服务是Kafka消费者消费消息后开启线程池调用FFmpeg或阿里云MediaTrans进行转码将结果存到另一个OSS路径并把转码状态写入数据库任务表。转码完成后将消息发送到video-audit主题审核服务通过AI和人工结合审核。审核通过后内容服务把最终的视频信息JSON格式写入数据库删除“转码中”标签发布到CDN并异步更新Elasticsearch。同步通信 vs 异步通信同步OpenFeign适合实时性高、链路短的请求比如获取用户信息、点赞数等。异步Kafka适合任务型、耗时长、需要削峰的流程如转码、审核、邮件通知等。保证不丢消息Producer端acksallretries 0可能使用enable.idempotencetrue。Broker端min.insync.replicas2写入副本成功才算成功。Consumer端关闭自动提交手动提交offset但注意在业务处理成功后提交否则可能会重复消费。问题2转码服务宕机消费者幂等如何保证技术点转码状态机在任务表中存储状态PENDING、RUNNING、SUCCESS、FAILED。转码前将状态改为RUNNING转码成功后改为SUCCESS。消费者宕机重启后扫描所有状态为RUNNING但超时比如超过10分钟的任务重新加入队列。幂等消费Kafka至少一次语义At Least Once所以同一事件可能被消费多次。使用业务唯一键比如videoId transcodeType在消费前查询数据库或Redis是否存在成功标记如果存在则直接ack不重复执行。也可以使用去重表的唯一索引首次插入成功重复插入报异常捕获后视为已处理。消费者侧手动提交偏移量必须在处理成功之后。如果处理完但没提交重启后会重复消费需要配合幂等来解决。问题3如何排查接口耗时长的瓶颈业务场景首页推荐接口P99耗时3秒需要定位是哪个环节慢。技术点可观测性三大支柱日志Logging、指标Metrics、追踪Tracing。Micrometer在Spring Boot中注册MeterRegistry给核心方法添加Timed注解统计调用次数和耗时。它可以把数据写入Prometheus。Prometheus GrafanaPrometheus从/actuator/prometheus采集指标Grafana配置Dashboard展示QPS、P99、CPU、内存等。通过服务视角看是哪个服务慢。Jaeger / Zipkin在调用链上传播traceId与spanId显示从防火墙到网关到服务A到服务B到数据库的每一步耗时。链路视角能快速判断瓶颈是网关、服务A、服务B还是中间件。ELK Stack服务日志集中收集到Elasticsearch。搜索某个traceId可以找出详细的异常栈、慢查询日志、用户参数。常见瓶颈SQL没有走索引、全表扫描、大数据量磁盘排序。循环调用远程服务如for循环里逐条查Redis。线程池排队严重等待足够多的线程执行。大对象JSON序列化慢Jackson把大对象转字符串。频繁GC导致进入安全点暂停。问题4基于线程栈的CPU排查G1和CMS的区别技术点top -Hp pid找CPU最高的线程ID转十六进制后用jstack pid | grep -A 50 0xhex找到对应线程的栈。如果发现GCTaskThread、VMThread等执行频繁说明GC压力大。CMSConcurrent Mark Sweep是一款并发的垃圾收集器以获取最短回收停顿为目标但会产生内存碎片且与应用程序并发时占用CPU资源。G1Garbage First面向服务器端应用将堆划分为多个Region根据可预测的停顿时间模型如-XX:MaxGCPauseMillis50动态回收价值最高的Region同时避免Full GC。G1比CMS更适合大内存4GB和现代多核服务器。如果GC之后CPU仍然很高也可能有其他问题业务线程疯狂创建对象导致分配速率过高从而让GC线程忙于整理。此时要dump堆分析对象创建来源用-XX:PrintObjectPromotion查看晋升情况。总结这轮面试从音视频UGC场景展开覆盖了JVM性能排查、缓存一致性、Spring生态、消息队列、分布式事务和可观测性。谢飞机的表现在低水平里算可以但对于核心原理的回答仍然含糊比如对分代算法的理解停留在“为了效率”对缓存一致性只是停留在“延迟双删”。如果是真实面试需要深入实践、阅读源码并且用实验数据来支撑自己的观点。希望每一位看这篇文章的同学能从中梳理出自己知识图谱的盲区然后深入学习。祝你面试顺利不再像谢飞机一样回家等通知。