微服务面试题 - 2026-06-19 17:14
生成时间:2026-06-19 17:14 | 共 10 题 | 难度递进(基础→进阶→高阶)
【面试题】1. 微服务拆分时,怎么判断两个功能应该放在同一个服务还是拆成两个?有什么具体的判断标准?
【标准答案】三个判断标准,依次递进。第一看数据主权——这两个功能是不是操作同一组核心数据?订单创建和订单查询操作的是同一张订单表,拆开反而要跨服务调用来读取数据,不如合在一起。第二看变更频率——如果 A 功能每周改三次,B 功能三年不变,强行放一起导致每次改 A 都要重新部署 B,B 的稳定性被连累,应该拆。第三看团队对齐——康威定律告诉你,系统架构会镜像组织结构。两个不同团队负责的功能,即使数据关系再密切,最终也会因为沟通成本而耦合失控,不如一开始就拆开用 API 约定隔离。实操层面有个"痛苦指数"判断法:先合在一起,等某一天你发现"改 A 要小心翼翼怕影响 B"或者"A 的慢查询把 B 拖死了",那时候再拆。过早拆分是过度设计,过晚拆分是技术债务,分裂点在"痛的时候"。
【面试题】2. SkyWalking 的 Java Agent 是怎么在不改一行代码的情况下实现链路追踪的?TraceId 跨线程、跨服务传递的原理?
【标准答案】SkyWalking Agent 核心靠 Java 字节码增强——启动时加 -javaagent:skywalking-agent.jar,利用 ByteBuddy 在类加载时拦截目标方法,在方法前后织入追踪代码。比如你调 restTemplate.getForObject(),Agent 在方法进入时创建 Span、生成 TraceId、注入 HTTP Header,方法退出时结束 Span 上报。不改一行代码的关键在于:Agent 针对的是框架和中间件(Spring MVC、Dubbo、HttpClient、RabbitMQ),不是你的业务代码。TraceId 跨服务传递:Agent 在发 HTTP 请求时自动把 TraceId 塞到 Header(如 sw8),下游 Agent 从 Header 里提取并设为当前上下文。跨线程传递更麻烦——线程池复用时 ThreadLocal 会丢数据,Agent 的做法是在 submit() 时把当前 TraceContext 快照下来,在 run() 里恢复。一句话:Agent 替你做了所有脏活——入口埋点、上下文传递、出口上报,你的代码不需要感知追踪的存在。
【面试题】3. Consul 的健康检查机制和 Nacos 有什么本质区别?Consul 的 Script、HTTP、TCP、gRPC 检查各自适用什么场景?
【标准答案】本质区别在检查发起方。Nacos 分两种:临时实例是客户端主动上报心跳,服务端被动等;持久化实例是服务端主动探测。Consul 全是服务端主动探测,客户端也可以上报但 Consul 自己有一层健康检查 Agent 跑在节点上做兜底。四种检查方式:Script 检查——在节点上执行自定义脚本,返回 0 算健康。适合检查 MySQL/Redis 这种没法直接暴露 HTTP 端口的中间件,写个脚本 mysql -e "select 1" 就行;HTTP 检查——定期 GET 一个 URL,2xx 算健康,适用所有 Web 服务;TCP 检查——只做 TCP 三次握手,端口可达即健康,最轻量,适合不想暴露 HTTP 的纯 TCP 服务;gRPC 检查——调 gRPC 的 Health/Check 标准接口,适合 gRPC 服务原生健康探活。Consul 一个优势是集成了 DNS 模式——非 Java 服务(Nginx、Python、Go)不需要 SDK,直接配置 Consul DNS 做服务发现,这是 Nacos 比较弱的地方。选 Consul 的场景:跨语言异构服务多、需要 DNS 发现、对健康检查类型有复杂需求。
【面试题】4. 线上报了一个"用户下单失败",你怎么利用 Logging、Metrics、Tracing 三个维度定位问题?排查路径是怎样的?
【标准答案】三支柱排查订单失败的黄金路径:第一步看 Metrics(指标)——打开 Grafana 看下单接口的 QPS 和 Error Rate 有没有突增。如果 Error Rate 从 0.1% 飙到 30%,确认是集中爆发还是零星失败;同时看 P99 延迟,延迟飙升通常指向下游变慢。第二步看 Tracing(链路)——从 SkyWalking 里搜一条失败订单的 TraceId,看调用链里哪个 Span 标红了。比如发现调用库存服务的 deductStock 耗时 5s 后超时报错,那就精准定位到了库存服务。第三步看 Logging(日志)——跳到库存服务,用 TraceId 在 ELK/CLS 里搜对应时间段的日志,看到 Redis connection timeout,根因就在 Redis 连接池耗尽。三者关系一句话:Metrics 告诉你"出事了没有"(宏观警报),Tracing 告诉你"出在谁身上"(定位节点),Logging 告诉你"为什么出"(看根因细节)。没有 Metrics 你不知道出问题,没有 Tracing 你要翻几十个服务的日志,没有 Logging 你只能看到现象看不到原因。三根支柱缺谁都是瘸子。
【面试题】5. 金丝雀发布在 Spring Cloud 微服务体系里怎么落地?从流量染色到金丝雀实例隔离的完整方案是怎样的?
【标准答案】金丝雀发布三步走:第一步,实例打标——注册到 Nacos 时给金丝雀实例打元数据标签,比如 canary=true, version=v2.0。第二步,流量染色——在网关层根据请求特征决定路由到哪。最简单的策略:Header 匹配——发版时测试人员在请求头带 x-canary: true,网关看到这个 Header 就把请求路由到 canary 实例。或者按 userId 取模——取 5% 的用户流量路由到金丝雀。第三步,路由隔离——Gateway 的自定义负载均衡在选取实例时过滤元数据:金丝雀流量只走 canary=true 的实例,普通流量只看 canary!=true 的实例,实现物理隔离。进阶玩法:用 Nacos 的元数据 + Spring Cloud LoadBalancer 的 ServiceInstanceListSupplier 过滤——继承后重写 get() 方法,根据请求 Header 筛出对应的实例列表。关键设计原则:金丝雀实例和正常实例注册到同一个服务名下(不同服务名会破坏调用链),靠元数据区分;金丝雀出问题一键下线——在 Nacos 控制台把这个实例下线或改权重为 0,流量立刻切走。灰度成功后把新版全量发布、摘掉 canary 标签即可。
【面试题】6. 2PC 的协调者单点故障问题怎么解决?3PC 的超时机制解决了哪些问题又引入了什么新问题?
【标准答案】2PC 最致命的不是性能,是协调者单点故障——协调者在"发完 commit 请求但还没收到所有参与者响应"时宕了。参与者全部锁着资源处于阻塞态,不知道是该提交还是回滚,一直等到超时...也没有用,因为2PC里参与者没有超时中断的权限——必须等协调者恢复后明确告知"commit"或"rollback"。这就是"阻塞"问题的根源。3PC 改进两点:第一,引入预提交阶段(CanCommit → PreCommit → DoCommit),在 PreCommit 后参与者就算联系不上协调者也可以自行决定;第二也是核心——给参与者加了超时机制。如果参与者在 PreCommit 后超时没收到协调者的 DoCommit/Abort,它默认提交(因为它知道其他参与者都已经通过了 PreCommit,一致性损害概率低)。3PC 解决了 2PC 的阻塞问题但也引入新问题:网络分区时——协调者发了 Abort 但网络丢包,参与者超时后默认提交了,数据不一致了。2PC 是"一致性优先于可用性",3PC 是"可用性优先但有极小概率不一致"。实际生产中 2PC 和 3PC 都用得少——前者因为阻塞性能差,后者因为实现复杂且仍有不一致窗口。真正的生产方案是 Seata AT、TCC、消息最终一致性这些工程落地更好的方案。
【面试题】7. 一致性哈希算法在负载均衡里解决的核心问题是什么?虚拟节点是什么、为什么能解决数据倾斜?
【标准答案】一致性哈希解决的核心问题:节点增减时只影响哈希环上相邻的一小段数据,而不是所有数据都要重新分配。传统取模哈希(hash(key) % N)扩容时 N 变了,几乎所有 key 的路由都变,在缓存场景里等于"缓存全量失效引发缓存雪崩"。一致性哈希的做法:把节点 hash 映射到一个 0~2^32-1 的环形空间,key 也 hash 到环上,顺时针找到第一个节点就是目标节点。当新增节点 X 落在环上 B 和 C 之间时,原来路由到 C 的 key 中只有 hash 落在 B→X 这一段的会改路由到 X,其他不动。虚拟节点的引入解决了"物理节点少导致环上分布不均"的问题——假如只有 3 个物理节点,环上 3 个点之间的弧长差异巨大,数据就严重倾斜到弧长大的那段。解法:每个物理节点映射出 N 个虚拟节点(比如 150 个),均匀散布在环上。节点多了弧长自然均匀,key 落到各物理节点的概率趋于平均。Nginx 的 upstream hash 和 Dubbo 的一致性哈希负载均衡都用了虚拟节点,通常设 160 个。面试关键一句:一致性哈希=最小化数据迁移成本,虚拟节点=把不均匀变成均匀。
【面试题】8. Redis Cluster 为什么选择 16384 个哈希槽?数据迁移和客户端重定向的完整流程是怎样的?
【标准答案】16384 这个数不是拍脑袋定的。核心原因一条:心跳包大小。Redis Cluster 的节点间通过 Gossip 协议持续交换彼此的状态信息,心跳包里夹带了本节点负责的槽位位图(16384 bits = 2KB)。如果槽数为 65536,位图变成 8KB,6000 个节点的集群光心跳每秒就要几十 MB,网络扛不住。反过来槽太少——比如 1024——大集群每个节点分到的槽太少,迁移粒度太粗(一个槽 = 总数据 / 16384 的数据段,已经够细了)。16384 是 2KB 位图 + 足够细粒度的最优平衡点。而且 CRC16 的哈希空间是 65535,槽数取 CRC16 结果的高 14 位(2^14 = 16384),正好。数据迁移流程:① 目标节点设 slot 为 importing,源节点设 migrating;② 用 MIGRATE 命令逐个迁移 slot 里的 key,迁移过程中客户端可能发请求→源节点返回 ASK 重定向,客户端临时跳转目标节点拿数据;③ 迁移完成后两个节点都更新槽位图,通过 Gossip 广播出去。客户端重定向:MOVED 是"槽永久换主人了,客户端更新本地槽位表",ASK 是"槽正在搬家,你这次问目标节点就行,别更新路由表"。迁移过程中 slot 的 key 跨两个节点,ASK 就是处理这个过渡态的机制。
【面试题】9. 你作为技术负责人,要把一个日活百万的 Spring Boot 单体拆成微服务,你怎么说服老板、怎么排期、怎么分阶段落地?
【标准答案】说服老板要用"痛苦翻译"而不是"技术优点"。不要说"微服务解耦好、扩展强",要说"现在每次改一个地方要全量回归 3 天,拆开后只测改的服务,发版从一周一次变成一天三次";不要说"独立部署",要说"上次购物车 Bug 导致整个站点挂了 2 小时,拆开后下单挂了但不影响浏览和支付,业务损失从 100 万降到 10 万"。老板要听的是人效提升、故障隔离半径缩短、业务响应速度加快。排期分三阶段六周:第一周——DDD 事件风暴,拉上产品、测试、各端开发,把现有单体按业务能力域画限界上下文,输出一份服务划分图。第二到四周——基础设施搭台,搭 Nacos(注册+配置)、Gateway、SkyWalking 全链路追踪、统一 CI/CD 模板,先让"地基"稳定。第五到六周——从边缘往核心拆,先拆风险最低、最独立的功能(比如短信服务、文件上传),验证全链路跑通,再动用户、订单等核心域。灰度切流:新服务和旧单体同时运行,Nacos 上先切 5% 真实流量验证,监控差异→50%→100%,每一步有回滚按钮。前三次拆分当"练手",每次复盘优化流程和自动化测试,第四刀开始效率会指数级提升。
【面试题】10. 熔断器的 CLOSED → OPEN → HALF_OPEN 三态转换的具体条件是什么?为什么半开状态不能用"只放一个请求探活"那么简单?
【标准答案】三态转换:CLOSED(正常)状态下,统计最近一段时间窗口内的调用——失败率超过阈值(比如 50%)并且请求量超过最小基数(比如 20 次,防止一两次失败就误熔断),切换到 OPEN。OPEN(熔断)状态下,所有请求直接返回降级响应不调下游,维持一段固定时间(比如 30s)后自动切换到 HALF_OPEN。HALF_OPEN(半开)状态下,允许有限请求通过去试探下游是否恢复——如果试探成功,切回 CLOSED;如果失败,重新切回 OPEN,计时器重新开始。半开不能"只放一个请求"的原因:单次成功不代表服务恢复——比如下游刚恢复但仍在预热,一个请求成功可能是偶然,立即全量放开可能再次把下游打挂。正确做法是"放一批"试探——比如放 10 个请求,这批的成功率达到阈值(如 80%)才认为恢复,否则继续 OPEN。Resilience4j 还引入了一个精妙设计:半开状态下不是等全部试探请求结束才决策,而是"一旦累计的失败数让你确定成功率不可能达标就立刻切回 OPEN",不用傻等。三个核心参数要配好:失败率阈值、OPEN 持续时间、半开试探请求数——这是熔断器的心脏。