微服务面试题 - 2026-06-19 17:59
生成时间:2026-06-19 17:59 | 共 10 题 | 难度递进(基础→进阶→高阶)
【面试题】1. 可观测性三大支柱 Logging、Metrics、Tracing 分别管什么?三者在排查一个问题时各自的角色是什么?
【标准答案】三个支柱分工明确。Logging(日志)记录离散事件——"某用户在某时刻调了某接口,返回了某错误码",是一行一行的故事。好处是最细粒度,缺点是量大、关联难——一百台机器每台都在打日志,查一个问题要 grep 一百台机器的日志文件。Metrics(指标)记录聚合数值——"过去 1 分钟接口 P99 延迟 300ms、QPS 5000、错误率 3%",是时间序列的数字。好处是能设告警阈值,坏处是只知道"出问题了"不知道"谁的锅"。Tracing(链路追踪)记录一次请求在多个服务间的完整调用链——"gateway(2ms)→order-service(50ms)→inventory-service(120ms)→payment-service(500ms)",是跨服务的时间轴。排查一个问题时的标准路径:先看 Metrics 大盘有没有异常(P99 飙升→知道有事),再切 Tracing 看是哪个下游耗时最长(payment 花了 500ms),最后翻那个服务的 Logging 看具体异常堆栈。三维联动才是完整的可观测性——没有 Tracing 你不知道查哪台机器的日志,没有 Metrics 你不知道什么时候该紧张。核心认知:三者不是"选一个",是"三位一体"——Tracing 把 Logging 串起来,Metrics 给 Tracing 提供宏观视角。
【面试题】2. SkyWalking 的 Java Agent 是怎么做到不改一行代码就能拦截所有 HTTP/RPC 调用的?字节码增强的原理口述一下。
【标准答案】SkyWalking 的 Java Agent 用的是 Java 的 premain 机制——JVM 启动时加载 agent jar,在类加载到 JVM 内存之前拦截字节码、修改完再交给 JVM 执行。核心组件是 ByteBuddy(字节码操作库)。流程:Agent 启动时定义一套"拦截规则"——比如"所有实现了 org.springframework.web.servlet.HandlerInterceptor 的类"和"所有 feign.Client 的子类"。当这些类被类加载器加载时,ByteBuddy 在构造方法、execute() 等方法前后插入埋点代码(before:记录 startTime + traceId 注入;after:记录 endTime + span 上报)。整个过程对于应用代码来说零侵入——你写的 Controller 源码里没多一行,但编译后的 .class 字节码里已经被插入了几十行监控代码。Dubbo 同理——SkyWalking 拦截的是 com.alibaba.dubbo.rpc.Filter 的实现类。关键细节是 TraceId 的传递:SkyWalking 在跨进程调用时,把 TraceId + SpanId 塞进 HTTP Header(sw8 头),下游的 Agent 自动解析并接上调用链,实现全链路串联。面试一句:Java Agent 的本质是"在 JVM 里做编译后插桩"——你写代码时不管,编译后代理帮你管,对开发者完全透明。
【面试题】3. Nacos + Gateway 怎么实现灰度发布?同一个服务部署了 v1 和 v2 两个版本,怎么让特定用户路由到 v2?
【标准答案】灰度发布的核心是"流量染色 + 路由匹配"。三步落地。第一步,打标:v2 实例注册到 Nacos 时带 metadata——spring.cloud.nacos.discovery.metadata.version=v2。普通 v1 实例不带或者 version=v1。第二步,染色:网关拿请求时判断——Header 有 X-Gray: true 的、或者 userId 在白名单里的,就是灰度流量。把灰度标记透传给 Spring Cloud LoadBalancer——通常用一个 ReactorLoadBalancer 的自定义实现,从请求上下文中读标记。第三步,路由:LoadBalancer 拿到实例列表后按 metadata.version 过滤——灰度请求只看 version=v2 的实例,非灰度请求只看 version=v1(或没打 version 标签的)。完整的生产循环:先在 Nacos 把 v2 注册上去→把内部测试用户的 Header 设好→v2 先处理内部流量 2 天→没问题后把 5% 真实用户灰度→监控 v2 的错误率和 P99→没问题逐步扩大到 50%→100%→下线 v1。全链路灰度的坑:不能只灰网关——如果 A 服务灰度到 v2 但下游 B 服务还是 v1,数据格式不兼容直接炸。解法:全链路透传灰度标签,每个服务都按标签路由到对应版本,要么全链路 v1 要么全链路 v2。Nacos 的优势是 metadata 原生支持和实例级别动态变更,不需要额外中间件。面试金句:灰度本质上不是技术问题,是流程问题——最大的风险不是路由不准,是灰度时间太短,没充分暴露 bug 就全量了。
【面试题】4. 服务雪崩是怎么从"A 服务响应变慢"一步步蔓延到整个系统的?熔断、降级、限流分别在哪一层切断传播链?
【标准答案】雪崩传播就像多米诺骨牌。第一步:C 服务的 DB 慢查询导致 C 响应变慢(P99 从 50ms 飙升到 5s)。第二步:B 服务调 C,B 的 Tomcat 线程全被卡在等 C 返回(原来 1 秒内释放的线程现在要等 5 秒),B 的线程池满了→B 也开始不可用。第三步:A 服务调 B→A 也卡住→网关调 A 也卡住→整个链路全崩。更可怕的是"雪上加霜"——B 挂了后 A 调 B 失败→A 不停重试→给 B 发更多请求→B 更起不来。三层防线:第一层,限流(入口):网关或服务入口用 Sentinel/QPS 限制——"我这个接口最多抗 1000 QPS,多出来的直接拒绝",不让流量把服务冲垮。第二层,熔断(调用方):B 调 C 失败率达到阈值后直接熔断——短时间内不再调 C,快速失败返回 fallback,释放 B 的线程。这是防止垮的关键。第三层,降级(业务方):熔断触发后的兜底策略——C 挂了但 A 不能也跟着挂,调 C 时返回一个降级结果("推荐列表返回默认热门商品"),保证核心链路不断。三者的配合逻辑:限流守门→熔断断后→降级兜底。一个常见踩坑:熔断时间设太短(5 秒),下游刚缓过来又被流量冲死——建议初始熔断时间至少 30 秒,给下游足够恢复时间。面试一句:雪崩的本质是"等待下游的线程耗尽了上游的资源"——切断这个等待就是熔断的价值。
【面试题】5. 微服务里如何设计对账系统保证跨服务数据的最终一致性?"实时对账"和"T+1 对账"各解决什么问题?
【标准答案】对账系统是最终一致性的最后一道防线——实时链路告诉你"应该一致",对账系统告诉你"真的一致吗"。两种对账模式。实时对账(准实时,秒级):订单服务写完订单后发一条对账事件(Kafka),对账服务消费后查下游服务(积分服务、库存服务)当前状态,对比是否吻合。不吻合→补偿任务自动触发(调积分加积分、调库存扣库存)→告警通知。实时对账不是给每个请求建一个对账任务(那样对账比业务还重),而是抽样 + 异常聚合——每秒随机抽 1% 做校验,异常率超过阈值才全量铺开。T+1 对账(离线,凌晨跑):每天凌晨把所有订单跑批,和下游全量数据做数据校对——导出两份 CSV 用脚本 diff——"订单 123 支付了但积分表没记录"一条一条对。T+1 对账的核心价值是"兜住实时对账漏掉的"——实时对账可能漏了某条消息未消费,但 T+1 全量对比一定能发现。架构关键设计:对账的幂等补偿——补偿逻辑不能"发现积分没加就加一次",而是"发现积分少了就加上差额"(set 而非 incr),因为可能同一个订单已经被补过多次。对账元数据要存——哪一笔对账在什么时间发现什么差异、补了什么操作,出问题能回溯。面试总结:实时对账保体验(秒级发现秒级补),T+1 对账保完整(全量兜底不怕漏)。两者必须同时存在——实时证明快,离线证明全。
【面试题】6. 一个新项目从 0 搭微服务,你第一周会做哪五件事?技术选型的决策顺序是什么?
【标准答案】第一周不做业务代码,做基础骨架。一、团队技能摸底——团队里的人熟悉 Spring Cloud Alibaba 还是 Spring Cloud Netflix?有没有人搞过 K8s?技术选型不选"最好的",选"团队能接住的"。曾经一个团队 Java 主力但选了 Go-Micro 微服务框架,三个月都在学语言,业务零交付。二、注册 & 配置中心选型——小团队 Nacos 一把梭(注册+配置同进程不折腾),大公司已有 Apollo 就 Apollo + Nacos(仅注册)。三、网关选型——Spring Cloud Gateway 是第一选择(WebFlux 异步),别碰 Zuul 1.x。网关挂上去后第一个 Filter 是全局鉴权——不然后面每个服务都要写一遍 JWT 校验。四、脚手架搭建——写一个 archetype 模板项目,包含统一异常处理、统一返回体、logback 日志格式、健康检查端点、Prometheus metrics 暴露——新服务从模板 init 而不是从零写,保证十个服务长相一致。五、建立 CI/CD 和 dev 环境——Jenkins/GitLab CI + Docker Compose 一键拉起 dev 环境,PR 自动跑集成测试。这一步在没写业务代码之前就必须做,因为"能快速验证"比"写对代码"更重要——微服务最大的成本不是开发,是联调和环境管理。技术选型决策顺序:先定团队能力边界→再定基础设施(注册/配置/网关)→再定中间件(MQ/缓存/DB)→最后定业务框架。倒过来选(先选中间件再搭基础)的结果就是 MQ 选好了但服务之间还没法互相发现。面试一句:微服务项目失败的第一大原因不是技术选型烂,是基础骨架没搭好就开始写业务,三个月后十个服务十个样子谁也维护不了。
【面试题】7. CAP 理论告诉我们三者只能选二,为什么现实中很多系统声称"既要又要"?PACELC 是怎么补充解释的?
【标准答案】CAP 现在面试问得更深了——不是考你"CAP 是啥"而是"你怎么理解 CAP 的局限性"。CAP 的"一致性"是指强一致性(线性一致性)——任何时刻所有节点看到的数据完全一样。现实中 99% 的系统不需要这么强的一致性——订单状态从"待支付"到"已支付"之间的 30 秒窗口里,看微微不一致的"待支付"业务完全能接受。这时候你说它是 CP 还是 AP 已经说不清了——因为没有网络分区的时候它提供强一致,分区发生时它退化成最终一致。CAP 的致命缺陷:它只讨论网络分区时的取舍,但 99.9% 的时间网络是通的——没有分区的时候你在牺牲什么?PACELC 补了这个洞:P 发生(Partition)时选 A(可用性)还是 C(一致性);P 没发生时(Else)选 L(延迟 Latency)还是 C(一致性)。也就是说——网络正常时你要权衡一致性和延迟,网络分区时你要权衡可用性和一致性。Paxos/Raft 系统:网络正常时牺牲延迟换强一致(E 选 C),分区时牺牲可用性保一致(P 选 C)。最终一致性系统:网络正常时牺牲一致性换低延迟(E 选 L),分区时牺牲一致性保可用(P 选 A)。Cassandra 和 DynamoDB 就是 PA/EL——分区时保可用、正常时保低延迟。面试官问 CAP,你抛出 PACELC,他立刻觉得你不是背八股文而是真理解分布式取舍。面试一句:CAP 教你认识极端情况下的选择,PACELC 教你在日常场景下做工程权衡——分布式系统没有完美的,只有取舍。
【面试题】8. Sentinel 的集群限流是怎么实现的?Token Server 挂了会不会把整个集群拖垮?它的 fallback 机制是什么?
【标准答案】Sentinel 集群限流解决单机限流的痛点——"某接口全集群 QPS 应该 1000",但单机限流每台机器只能独立设——你设每台 100 结果 10 台总 QPS 1000,但如果流量不均匀(某台被打到 300 QPS 另几台 50 QPS),单机限流就失效了。集群限流流程:部署一个 Token Server(通常和 Sentinel Dashboard 同进程),所有客户端调用前问 Token Server 要令牌——Token Server 用全局计数器精确控制集群总 QPS=1000。Token Server 挂了怎么办?这就是重点——Sentinel 有一个"降级模式":客户端发现 Token Server 不可用时,自动切换回单机限流模式,每台按配置的 fallback 值独自限流。Token Server 恢复了再切回集群模式。这个降级是自动、瞬时的,不会有"Token Server 挂了全集群没人能限流"的问题。生产上 Token Server 要做高可用——部署两个 Token Server,用内嵌的 Raft 做选主(新版本),或者直接利用 K8s 部署多副本 + 健康检查自动切换。两个大坑:一,Token Server 本身成了瓶颈——每个请求都要先问 Token Server,多一跳 2ms 延迟。解法:客户端批量拉令牌(一次拉 100 个放在本地,用完再拉),99% 的请求都是本地拿令牌,延迟降到纳米级。二,令牌预分配——均匀预分导致流量不均的机器浪费令牌。Sentinel 的动态配额根据实际消费速率重新分配。面试定音:集群限流把"分布式计数"问题交给了 Token Server——这个 Server 的可靠性和性能决定了整个限流系统的天花板,所以必须做批量拉取 + 自动降级。
【面试题】9. 微服务 API 要升级但老客户端还在用旧接口——怎么设计 API 兼容性策略?版本号放 URL 里还是 Header 里?什么时候强制废弃旧版?
【标准答案】API 演进的黄金法则:新增不改旧、改旧不删旧。具体策略分三层。第一层,向前兼容(加字段):新增可选字段——response 加 newField,老客户端不认识就忽略,新客户端能用。这是最安全的兼容方式,不做版本号。第二层,破坏性变更(改字段名/改数据结构):必须发新版。版本策略的选择:URL 版本 /api/v1/orders、/api/v2/orders——优点是直观、抓包一眼看出版本,浏览器缓存不冲突;Header 版本 Accept: application/vnd.company.v2+json——优点是 URL 干净,但排查时得翻 Header 不直观。国内推荐 URL 版本——排查成本低、和网关路由天然契合(Gateway 按 /v1/** 和 /v2/** 路由到不同服务)。第三层,废弃旧版:监控旧版 v1 的调用量→降到接近零时发邮件通知所有调用方(给他们一个月迁移)→在 v1 接口返回 Deprecation: true Header + 文档标注→最后日期硬下线。废除的三个判断条件:调用量 < 日活的 1%、调用方就一家且已确认迁移、暴露安全漏洞。常见反模式:为了"代码复用",v2 Controller 继承 v1 Controller 改几个方法——结果 v1 改 bug v2 跟着出问题。正确做法:v2 独立写,只有真正稳定的公共逻辑才抽 Service。面试核心:API 版本管理是在"向前兼容成本"和"维护旧版成本"之间做平衡——加字段零成本就向前兼容,改数据结构必须发新版本。废弃时机不靠技术决定,靠旧版调用量的数据决定。
【面试题】10. 当注册中心从 100 个实例增长到 10000 个实例时,架构上会遇到什么瓶颈?怎么解决?注册数据的分发机制如何优化?
【标准答案】100→10000 实例,注册中心面临的不是量变而是质变。三个瓶颈:瓶颈一,心跳风暴——10000 个实例每 5 秒发一次心跳,Server 每秒要处理 2000 次心跳请求。Nacos 2.x 的解法:gRPC 长连接替代 HTTP 短连接——心跳走 gRPC Stream,连接复用不再每次建连拆连,Server 单机能扛 10 万实例。瓶颈二,注册表数据量——10000 实例的注册信息(IP+Port+Metadata+健康状态)序列化后几 MB。每次客户端拉全量注册表(Nacos Client 默认行为)——500 个消费者每台都拉几 MB,Server 出口带宽直接打满。解法:增量推送——客户端缓存版本号,只拉变化的部分(UDP 推送变更通知 + 客户端主动拉增量)。Nacos 的 notify 机制就是这么做的——Server 不推全量 push 注册表,而是推送一个"变更事件"("service A 的实例 192.168.1.10 下线了"),客户端收到后只拉变更的那条记录。瓶颈三,一致性协议开销——CP 模式下 Raft 写一个实例上下线需要半数以上节点确认,节点越多写延迟越高。解法:注册发现用 AP 模式(临时实例),配置用 CP 模式(持久实例)——把一致性成本只用在真正需要强一致的场景上。终极解法:注册中心分片——按业务域拆成多个注册中心集群,订单域一套、用户域一套,服务间不跨域调用就不需要全局注册表。K8s + Service Mesh 的解法是根本性的——注册发现下沉到基础设施层(Istio/Envoy),Sidecar 代理接管所有流量和健康检查,注册中心不再直接面对每个实例。面试核心:微服务规模化下的注册中心瓶颈不是 CPU 和内存,是"全量数据分发"的带宽成本和"一致性协议"的延迟成本——增量通知和业务域分片是两条腿。