微服务面试题 - 2026-06-19 18:09
生成时间:2026-06-19 18:09 | 共 10 题 | 难度递进(基础→进阶→高阶)
【面试题】1. Nacos 配置中心怎么做灰度发布?让配置先在 2 台机器上验证无误再全量推送,具体怎么操作?
【标准答案】Nacos 配置灰度的核心是利用 namespace + group 做隔离。实操三步:一,建一个灰度 namespace(如 prod-gray),灰度机器连这个 namespace,其余机器连正式 namespace。二,改配置先改灰度 namespace,灰度机器吃新配置跑 1~2 天,观察错误率和业务指标。三,验证 OK 后把配置拷到正式 namespace 全量生效。轻量版:不用新 namespace,直接切灰度机器的 group——spring.cloud.nacos.config.group=GRAY,正式机器连 DEFAULT_GROUP。Nacos 的优势是配置历史版本一键回滚——灰度中出问题,点一下回退秒级生效,不用重新部署。核心认知:配置灰度和代码灰度是一样的哲学——"分批验证、可观测、可秒回滚"。坑:配置变更后某些 Bean 不会自动刷新,要加 @RefreshScope 注解(但 Scope 会创建代理对象有性能损耗),或者监听 RefreshEvent 手动重建组件。面试一句:Nacos 天生不是灰度发布平台,但 namespace + group 的组合拳足够中小团队零成本跑灰度配置。
【面试题】2. Cache-Aside、Read-Through、Write-Through、Write-Behind 四种缓存模式的区别是什么?微服务里哪个最常用、为什么?
【标准答案】Cache-Aside(旁路缓存,微服务首选):应用自己管缓存和 DB 的读写。读:先查缓存 → miss 查 DB → 把结果写回缓存;写:写 DB → 删缓存(或更新缓存)。应用掌控一切,灵活但代码要自己写。Read-Through:缓存层代理 DB 读,应用只调缓存,缓存没命中自动去 DB 加载返回——应用感知不到 DB。Write-Through:写缓存的同时同步写 DB,缓存和 DB 始终一致,但每次写都要等 DB 落地,写入延迟高。Write-Behind:写缓存立刻返回,异步批量刷 DB——写入性能最高,但缓存挂了几千条没落地全丢。微服务为什么选 Cache-Aside:一是简单——不用引入额外的缓存代理层,Redis + 应用代码就搞定;二是灵活——可以按业务做差异化(热点数据 TTL 短、冷数据 TTL 长),Read-Through 缓存层不好做这种区分;三是有成熟的一致性补丁——延迟双删(写 DB→删缓存→睡 N 毫秒→再删一次)应对并发读写带来的脏数据。面试金句:四种模式是按"缓存与 DB 的耦合程度"递进的——Cache-Aside 最松最灵活最常用,Write-Behind 最紧性能最高但丢数据风险最大。没有银弹,看业务对一致性和性能的取舍。
【面试题】3. Spring Cloud LoadBalancer 的加权轮询和平滑加权轮询有什么本质区别?平滑加权算法的核心数学逻辑口述一下。
【标准答案】普通加权轮询:权重 5,1,1 的三个实例,简单转——A,A,A,A,A,B,C,A,A,A,A,A,B,C……权重高的连续打好几次,请求分布不均匀。平滑加权轮询(Nginx 的 SWRR 算法):每个实例维护 currentWeight,初始 = 0。每轮:currentWeight += weight(原始权重),选出 currentWeight 最大的实例转发,然后把被选中的实例 currentWeight -= 总权重。举例:3 个实例权重 [5,1,1],总权重 7。第一轮 currentWeight [5,1,1]→选 A,A 减 7→[-2,1,1]。第二轮加上权重→[3,2,2]→选 A→[-4,2,2]。第三轮→[1,3,3]→选 B→[1,-4,3]。第四轮→[6,-3,4]→选 A→[-1,-3,4]。每 7 次一个周期,A 被选 5 次但分散均匀,不会连续 5 次打A。效果就是"权重高的被选得多,但分布是平滑的,不扎堆"。Dubbo 和 Spring Cloud LoadBalancer 默认都是平滑加权。为什么重要:连续打同一台实例,如果那台刚好 GC 了或负载高了,P99 瞬间飙升——平滑算法把风险分散了。面试说清楚:普通加权是"按权重抢",平滑加权是"按权重排队"——用 currentWeight 的加减法实现流量打散。
【面试题】4. Seata AT 模式的一阶段提交了什么、没提交什么?为什么要让一阶段就提交?写隔离和读隔离怎么保证?
【标准答案】Seata AT 的独特之处:一阶段就提交本地事务——不放数据库锁,这是和传统 XA 2PC 的根本差异。一阶段做了什么:执行真正的业务 SQL(UPDATE inventory SET count=count-1)→ 同时在 undo_log 表记录回滚日志(记录修改前的数据快照)→ 提交本地事务。一阶段就提交的原因是"不挂数据库锁"——传统 2PC 一阶段 prepare 后锁着资源等二阶段,一个全局事务可能锁几十秒。AT 一阶段秒级提交,其他全局事务可以同时操作同一行数据——这才是高性能的关键。读隔离:默认读未提交(READ_UNCOMMITTED)——一阶段提交后数据全局可见,但不保证全局事务隔离。如果业务要求读已提交,需 SELECT FOR UPDATE 等全局锁释放。写隔离:用全局锁 + 本地锁双重保护——全局事务一阶段提交前先拿行级全局锁(存在 Seata Server),两个全局事务不能同时写同一行。本地锁是数据库本身的行锁。一句话定音:Seata AT 用"一阶段提交 + undo_log"换取了短锁持有时间——代价是读隔离弱了。这就是"高性能的最终一致性"的工程本质。
【面试题】5. DDD 的限界上下文在微服务拆分中具体怎么落地?事件风暴工作坊的输入输出是什么?"同一个词在不同上下文含义不同"这个原则为什么这么重要?
【标准答案】DDD 拆微服务就是把限界上下文一对一映射为微服务——一个限界上下文 = 一个微服务。事件风暴是找限界上下文边界的工作坊方法。流程:拉上领域专家 + 开发 + PM 围一堵墙,用橙色便利贴贴出所有"领域事件"(订单已创建、库存已扣减等)→ 找出触发每个事件的"命令"(谁发起的)→ 识别"聚合"(一组生命周期一致的实体,以聚合根为入口)→ 当发现"同一个词在不同人嘴里含义不同"时,那里就是限界上下文的边界线。输出三件套:限界上下文映射图——每个上下文负责什么、上下文之间怎么通信(共享内核/防腐层/开放主机);聚合根清单——每个上下文里的核心实体;领域事件列表——跨上下文通信协议。关键原则:"同一个词含义不同就该拆"——比如"用户"在认证上下文里是 username+password,在营销上下文里是 age+tags+消费能力,在订单上下文里是收货地址+会员等级。你硬塞一个"统一用户微服务"反而制造耦合——认证改个字段、营销跟着改、订单也跟着改,成了分布式单体。面试一句:限界上下文划的不是"数据边界"而是"语言边界"——不同团队对同一个词的理解一旦分裂,就该用微服务把它隔离。
【面试题】6. 生产环境凌晨三点告警:订单服务大面积超时、上游网关 502 堆积——你被 oncall 叫起来,你的排查和止血 SOP 是什么?
【标准答案】第一分钟:止血,不是查根因。看 Metrics 大盘——订单服务 P99 飙到 10s、错误率 40%。先降级:如果是大促流量冲的,立刻在 Sentinel 控制台调低订单创建接口 QPS 阈值(业务降级返回"系统繁忙"比全部 502 好);如果是下游支付服务拖的,熔断支付调用走快速失败(返回"支付暂时不可用")。同时把故障实例从 Nacos 摘掉——nacos.instance.enabled=false 强制下线,让流量只走健康实例。止血后的定位:第二到第五分钟。顺着 Tracing 找——SkyWalking 拓扑图上哪一段最红?很可能是"订单服务调风控服务"这一段耗时从 50ms 飙到 8s。切风控服务的 Logging——看是风控服务自己的问题还是风控的 DB 出事了。常见的午夜杀手:慢 SQL(凌晨跑批锁表)、缓存过期(整点大批 key 同时到期→DB 穿透)、中间件故障(MQ 积压拖死消费线程、ES GC 停顿)。定位到根因后才做修复——扩容/重启/临时关闭非核心逻辑。第六步:复盘——为什么告警响了 5 分钟才发现?(告警阈值设太宽)为什么不能用降级开关一秒止血?(降级开关要重启,没做热更新)为什么风控挂了订单就全挂?(没做舱壁隔离)。面试总结:oncall 不是秀技术的时候是保业务的时候——先止血再定位,先恢复再复盘。90% 的故障扩大是因为第一反应是找原因而不是断链路。
【面试题】7. 微服务 CPU 突然飙到 80% 但 QPS 没涨,怎么排查?arthas 拿到后先看什么、后看什么?
【标准答案】QPS 没涨说明不是请求量问题,CPU 有异常消耗。排查三步:一,top -Hp <pid> 找到吃 CPU 最狠的线程 PID→ printf '%x\n' <tid> 转十六进制→ jstack <pid> | grep -A20 <hex_tid> 直接定位到代码行。二,常见三个凶手的排查口诀:死循环——看线程 dump 是不是大量线程卡在同一个方法里且没有 sleep/wait/lock;频繁 GC——jstat -gcutil <pid> 1000 看 FGC 次数/耗时,如果每秒都在 Full GC 那就是内存不够或泄漏,dump 堆 jmap -dump:live,format=b,file=heap.hprof <pid> 用 MAT 分析;正则回溯——灾难性回溯(Catastrophic Backtracking)特征是一个线程高 CPU 且 jstack 里卡在 java.util.regex.Pattern$...match,一般是不严谨的正则表达式((a+)+b 这种嵌套量词)在某些输入上指数级回溯。三,arthas 拿到后的标准起手:dashboard 看全局→ thread -n 5 看 CPU 最高 5 个线程→ watch 或 trace 跟踪具体方法耗时。小概率元凶:日志分级错误——log.debug("big json:{}", hugeObj) 最后生产关了 debug 但 hugeObj.toString() 还是执行了,CPU 全花在序列化上。面试核心:CPU 飙升不限 QPS 不可能是"正常业务处理",一定是代码在空转(死循环/GC/正则)——jstack 比 arthas 更快,因为不需要安装。
【面试题】8. Redis Redlock 多节点分布式锁为什么有争议?Martin Kleppmann 说它不安全的论据是什么?Redisson 做了什么来弥补?
【标准答案】Redlock 算法:在 N 个独立 Redis 节点上顺序加锁(SET NX PX 超时时间)→ 多数节点(N/2+1)加锁成功且在锁有效期前完成→ 拿到锁;否则全部释放。看似完美但 Kleppmann 捅了致命一刀——三个场景攻破 Redlock:场景一,客户端 A 拿到了锁但 GC 停顿了 60 秒→ 锁过期自动释放→ 客户端 B 拿到锁开始写数据→ A 从 GC 醒来以为自己还有锁,继续写→ A 和 B 同时写,数据冲突。因为没有 fencing token——A 不知道自己已经被"请出局"了。场景二,系统时钟跳跃——运维 NTP 校时,Redis 节点时钟突然跳了 5 分钟,锁提前过期,同样冲突。场景三,网络延迟不对称——A 到 Redis 节点的响应延迟比预期长,锁其实已经过期但 A 以为自己还持有。Redisson 的补救:看门狗机制(Watchdog)——加锁成功后启动后台线程每 10 秒续期,只要客户端活着锁就不会过期,从根源上减少了"锁过期但客户端还在用"的概率。但这不能解决 GC 停顿问题——GC 期间看门狗也停了。诚实结论:Redlock 在 99.9% 的场景下够用,但在金融转账、库存扣减这种"写错一行就是事故"的绝对安全场景,用 ZooKeeper 临时顺序节点 + fencing token 或 etcd 的 lease 机制——强一致存储才能在极端场景下保数据安全。面试一句话:Redlock 用"多数派投票"解决了 Redis 单点故障,但没解决"客户端不知道自己过期了"——这是分布式锁最难的问题。
【面试题】9. Istio + K8s 全链路灰度 vs Spring Cloud 网关灰度,核心架构差异在哪?什么团队该选哪个?
【标准答案】核心差异是"灰度控制面在哪一层"。Istio 灰度把流量治理下沉到基础设施(Sidecar Envoy):K8s 用 DestinationRule 给 Pod 打 subset 标签(v1/v2),VirtualService 按 Header/权重做 L7 路由拆分——90% 流量到 v1、10% 到 v2。关键是应用代码零改动——你的 Java/Go/Python 服务完全不知道自己在灰度中,Envoy 透明代理把所有流量染色和路由的活干了。还能做故障注入、超时重试、流量镜像等高级玩法。代价:每个 Pod 挂一个 Envoy Sidecar,至少多吃 200M 内存 + 10% CPU,K8s 集群以外用不了。Spring Cloud 灰度靠 Nacos metadata + Gateway/LoadBalancer 定制:v2 实例注册时带 version=v2,网关解析 Header 里的灰度标记,LoadBalancer 按 metadata 过滤实例路由。优势是轻量——不需要 K8s,Spring 生态内 Nacos 就够。代价:灰度逻辑写在应用框架层,非 Java 服务享受不到;没有 Istio 那种开箱即用的流量镜像、故障注入能力。选型分界线:团队有 3 种以上编程语言 + 深度使用 K8s → Istio 值得吃下运维成本;纯 Java 团队 + 不想折腾 K8s → Spring Cloud 灰度足够好。面试定音:Istio 灰度是基础设施层的"大一统",Spring Cloud 灰度是应用框架层的"轻量专精"——选择不取决于谁更强,取决于你的基础设施现状和团队能力边界。
【面试题】10. 用 Prometheus + Micrometer + Grafana 给整个微服务集群搭建统一监控,指标体系怎么分层设计?告警规则怎么避免"狼来了"?
【标准答案】指标体系四层金字塔,越往下越通用、越往上越业务定制。第一层基础设施层:node_exporter 采集 CPU/内存/磁盘/网络→ 所有机器统一纳管,PromQL: 100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)。第二层 JVM 层:Micrometer 自动暴露(management.endpoints.web.exposure.include=prometheus),堆内存使用率、GC 暂停时间、线程数、类加载数——零代码接入。第三层中间件层:各种 exporter 采集——Redis 连接池使用率、MySQL 慢查询数、Kafka Consumer Lag、HikariCP 活跃连接数。第四层业务层:Micrometer 手动埋点——Counter.builder("order.created").register(meterRegistry).increment(),记录订单量、支付成功率、各接口 QPS/P99/错误率。Grafana 按四层建 dashboard,从物理机到业务逐层下钻。告警设计核心:宁漏勿滥——一天 50 条告警 30 条是误报,第三天后团队就关通知了。规则分层:P0(电话+紧急通知)→ 服务不可用/核心接口 P99 > 5s 持续 3 分钟/支付成功率 < 90%;P1(企微群消息)→ 错误率 > 3%/磁盘 > 85%/单机内存 > 90%;P2(邮件日报)→ 慢调用趋势上升/Cache Miss Rate 升高/连接池接近上限。关键技巧:告警规则必须加 for: 3m——持续 3 分钟才触发,避免毛刺告警。告警信息要带 runbook 链接——不是只发"订单服务 CPU 高了",而是"订单服务 CPU > 80%,可能原因:①慢 SQL→看这个 Grafana 面板 ②正则回溯→jstack 命令见这个 wiki"。面试金句:一条告警如果没有附带行动指南,就是垃圾信息——监控的价值不在于"能发现多少问题",而在于"能跳过多少噪音直击根因"。