去哪儿一面:故障诊断项目深挖与 Java 后端基础
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 请做一下自我介绍。
- 云服务器故障诊断项目:项目背景是什么、要解决什么问题,采用了哪些技术方案?
- 采集的数据是日志还是其他数据,数据量大概多少?是实时采集,还是用户提问时获取已有数据?
- 一台机器有应用日志和资源使用信息等大量数据,系统如何确定应获取哪些数据来分析问题?例如是否能够分析 CPU 使用率高的原因?
- MCP 返回异常项时,“异常”如何定义?平台不知道用户问题属于哪一类时,怎样处理数据并作出判断?
- 系统上线后的使用情况怎样?用什么指标衡量?是否真的解决了实际用户问题?
- 云服务器可能同时涉及安全、负载均衡和网络等资源,现有分层框架未来怎样扩展到用户问题的完整维度?
- 如果故障分类扩展到几十或上百类,Skill 和 MCP 越来越多,应该怎样管理,避免冲突或让 Agent 误解?
- 线程池配置并上线后,应该关注哪些指标,判断参数是否合理、线程池是否健康?
- 通过 Spring IoC 的 getBean 获取对象时,希望对象交付前某个属性已被修改。结合 Bean 生命周期,有哪些实现方式?
- 使用 Redis 缓存并访问数据库时,如何保证二者的数据一致性?有哪些常用做法?
- 如果再增加一层本地缓存,访问顺序为本地缓存、Redis、MySQL,更新时怎样保证三者一致?请明确更新顺序。
- 一张表通常达到多少数据量会建议分表?应该基于什么逻辑判断?查询为什么会随数据量增大而显著变慢?底层原因是什么?
- binlog、undo log 和 redo log 分别有什么作用?
- 接到业务需求后,如果使用 Claude Code 等 AI Coding 工具实现,开发者和 AI 应分别做哪些动作,才能完成高质量开发?
《参考解析》
故障诊断 Agent:数据获取、异常定义与 Skill 扩展
项目类追问的答题主线是”数据从哪来 → 怎么判断 → 怎么给结论 → 怎么衡量效果”。数据获取上要讲清两条路:实时采集(agent 常驻、指标/日志流式上报,成本高但能看历史与趋势)和按需拉取(用户提问时再通过 API 查监控、拉日志,成本低但只能看现存数据),以及为什么这么选——多数诊断场景需要的是”问题发生前后一段时间”的数据,所以拉取式更常见,但要保证数据留存窗口足够覆盖。
“如何确定该获取哪些数据”是这类系统的核心设计问题,答法是分层收敛:先从用户问题做意图与实体识别(服务名、机器 IP、时间范围、症状关键词),再按症状选择一个诊断模板(CPU 高、内存泄漏、磁盘满、网络抖动、依赖超时各是一套),模板决定要拉哪些指标和哪些日志,最后按需下钻——比如怀疑 CPU 高,先看整体使用率,如果高再看是哪个进程、哪类线程(GC 线程占比是判断 Java 应用的关键信号),再看火焰图。没有模板的兜底路径是”先拉基础指标包(CPU/内存/磁盘/网络/进程 Top)+ 错误日志”,再让模型基于观测决定下一轮拉什么。
“异常怎么定义”这个问题最容易被问倒,答题要分两层:静态阈值(CPU > 90% 持续 5 分钟、磁盘 > 85%、OOM 出现)和动态基线(与同机型/同时段历史分位数比较,偏离 N 个标准差;或与同集群其他实例横向比较,找出”别人正常我异常”)。动态基线是解决”平台不知道用户问题属于哪一类”的关键——没有明确症状时,就做无监督的离群检测,把显著偏离的项按偏离度排序返回给模型,由模型结合用户描述判断相关性。这里要强调 Agent 的角色是”提出假设 + 主动验证”,而不是一次就答对:给观测、给出置信度、给出下一步建议的动作。
Skill 与 MCP 数量膨胀后的管理,答四个机制:命名与描述规范(每个 Skill 一句话说清”什么时候用、什么时候不用、输入输出是什么”,避免语义重叠);按域分组 + 两级路由(先选大类再选具体能力,减少一次性注入模型的工具数,工具超过 20~30 个选择准确率会明显下降);冲突检测(对相似描述做 embedding 聚类,人工定期合并去重);以及权限与版本管理(谁能发布、灰度、回滚,每个 Skill 带版本号和弃用标记)。指标衡量上,别只看调用量:要看诊断结论采纳率、平均定位耗时、MTTR 变化、误报率、以及”Agent 给出的结论与人工复盘结论一致”的比例——最后一类是真正说明有没有解决问题。
线程池该看哪些指标
光看”队列里堆了多少任务”不够,核心是四条曲线:活跃线程数(持续等于 maxPoolSize 说明池子不够,长期远低于 corePoolSize 说明资源浪费)、队列长度与排队时间(队列长时间大于 0 说明处理速度跟不上提交速度;有界队列打满并触发拒绝策略就是已经出事)、任务执行耗时分布(P50/P95/P99,而不是平均值——平均值会把长尾藏起来)、拒绝次数与异常数(拒绝策略如果配的是 CallerRunsPolicy,表现是上游线程被拖慢,很容易误判成上游问题)。
再补两条判优口径:线程数不是越大越好,要按任务类型定——CPU 密集型经验值 N_cpu + 1,IO 密集型按 N_cpu × (1 + 等待时间/计算时间) 估,然后上线后按上面的指标动态调;以及池子必须隔离——不同业务共用一个池会出现”一个慢接口拖垮所有接口”,正确做法是按业务分池 + 有界队列 + 明确的拒绝策略 + 给线程起有意义的名字(ThreadFactory 命名,否则出问题时线程栈根本认不出来)。Java 里还要看 ThreadPoolExecutor 的 getActiveCount/getQueue().size()/getCompletedTaskCount 并接到监控(Micrometer/Prometheus),配合告警阈值。
Spring Bean 生命周期里改属性
题目本质是问”Bean 的哪个扩展点能改属性”。按生命周期顺序,可用的方式有:① 实现 BeanPostProcessor#postProcessBeforeInitialization——在 @PostConstruct 之前拿到刚实例化的 Bean,用反射或 BeanWrapper 改属性(这是最标准的答案,Spring 自己的 @Autowired、@Value 就是靠 AutowiredAnnotationBeanPostProcessor 在这个阶段注入的);② 实现 postProcessAfterInitialization(初始化后、可能已被代理,改属性要注意代理对象的问题);③ 实现 InitializingBean#afterPropertiesSet 或标 @PostConstruct——此时属性已注入完,在方法里覆盖目标属性;④ BeanFactoryPostProcessor / BeanDefinitionRegistryPostProcessor——更早,改的是 BeanDefinition(比如把某个属性值改掉、或加一个 PropertySourcesPlaceholderConfigurer);⑤ 更细粒度还有 InstantiationAwareBeanPostProcessor#postProcessProperties(Spring 5.1 起 @Autowired 走这里)、以及 ApplicationListener<ContextRefreshedEvent>(容器刷新完成后统一改)。
答题时给出”在哪个阶段、用什么接口、影响哪些范围(单个 Bean 还是全局)“这三要素,比只报一个接口名有价值。还要提醒一句:如果是想替换某个属性值(配置中心动态刷新),正规做法是 @RefreshScope(Spring Cloud)或者自己实现 BeanPostProcessor + 事件通知,而不是在业务代码里到处反射。
三层缓存的一致性
Redis 与 MySQL 的常用方案在另一篇里已详述(Cache-Aside 先更新 DB 再删缓存、延迟双删、binlog 订阅),这里重点是加了本地缓存之后的顺序问题。三层是 本地缓存(进程内,如 Caffeine)→ Redis → MySQL,热点数据放本地最快,但更新时要考虑两个新问题:本地缓存是多实例的(每个 JVM 各存一份),以及本地缓存无法被其他实例直接删除。
推荐的更新顺序是:先更新 MySQL(拿到版本/时间戳)→ 再删 Redis → 再通过广播(Redis Pub/Sub、MQ、或 Spring Cloud Bus)通知所有实例失效各自的本地缓存。理由:DB 是唯一真相,一定要先落库;删 Redis 再广播,保证任何实例在收到广播后不会再从 Redis 读到旧值;本地缓存的失效必须是”广播删除”而不是”等 TTL 过期”,否则各实例会长期不一致。给本地缓存设一个很短的 TTL(秒级到分钟级)作为广播丢失时的兜底。读路径上是”本地命中直接回、未命中查 Redis 并回填本地、Redis 未命中查 DB 并回填 Redis 和本地”。
如果业务要求强一致,别用多层缓存——分布式环境下本地缓存 + 强一致几乎不可兼得,要么放弃本地缓存只用 Redis(配分布式锁或版本号校验),要么接受最终一致并明确可容忍的时间窗口(通常 1~2 秒内)。面试官追问”更新顺序”时,明确回答”DB → Redis → 广播失效本地”并说清每一步的理由,比含糊的”都删一遍”强得多。
MySQL 分表与日志
分表的判断不能只背一个数字(常见的说法是单表 500 万~2000 万行或超过 2GB 考虑),要讲清背后的逻辑:真正决定查询变慢的是索引树的高度和数据是否能在内存里放下。InnoDB 的 B+ 树三层大约能存两千万行左右(取决于行宽和主键大小),三层以内查任何一行最多 3 次磁盘 IO(根和中间层常驻内存,实际 1 次左右);超过三层树高加一,IO 次数上升,加上热点数据无法全部缓存进 buffer pool,命中率下降,查询就显著变慢。所以判断标准是”行数 + 行宽 + 查询模式 + 硬件内存”综合,而不是一个固定数字:如果表很窄、查询都能走覆盖索引,几千万行也不一定需要分表;如果行很大且都是随机点查,几百万行就可能需要。
分表要答出方案完整性:分片键的选择(要保证高频查询都能带上分片键,否则会变成全分片扫描,比不分还慢)、路由方式(客户端路由如 ShardingSphere,或中间件如 MyCat/Vitess,或按时间做冷热分离)、跨分片查询与聚合怎么处理(冗余表、异构索引表、或者把统计类需求走数仓/ES)、以及扩容方案(一致性哈希或成倍扩容 + 双写迁移)。先分区表、先读写分离、先归档冷数据,往往比分表容易得多,一般按这个性价比顺序来。
三种日志必须能一句话说清用途:redo log 是 InnoDB 的物理日志(记录”某页做了什么修改”),保证崩溃恢复的持久性(D,WAL:先写 redo 再刷脏页,所以事务提交只需保证 redo 落盘);undo log 是逻辑日志(记录”反向操作”),用于事务回滚和 MVCC 的一致性读(快照版本从 undo 里构造);binlog 是 Server 层的逻辑日志(记录语句或行变更),用于主从复制和数据恢复(PITR)。一次事务的写入顺序是:改内存页 + 写 undo → 写 redo(prepare)→ 写 binlog → redo commit,两阶段提交保证 binlog 与 redo 一致,这是主从数据不丢不重的关键。
AI 辅助开发流程
一段可复述的分工是:人负责需求对齐、方案设计、拆任务、定验收标准、review 与最终决策;AI 负责按任务生成代码、补测试、写注释与文档、做重复性改造(重命名、接口迁移、批量适配)。具体动作上,人要先给 AI 建立上下文(相关文件、既有约定、接口契约、不可破坏的约束),要求它先给方案和改动清单再写代码,把任务拆到一个可 review 的粒度(一次一个函数或一个模块),并在动手前准备好可自动验证的信号(单测命令、类型检查、lint、编译)。生成后人跑测试、逐块 review diff、检查错误处理/边界/并发/资源释放,把关键场景补上测试,必要时让另一个模型做对抗性 review。
要强调的是”不要跳过验证”这条纪律:AI 生成能跑但逻辑错的代码是常态(happy path 正确、异常路径缺失、边界条件写错、静默改变原有行为),所以每步都要小步提交、可回滚,绝不允许一次性大范围改动后才发现方向错。