diff --git a/hzh/TEST/MicroService.md b/hzh/TEST/MicroService.md
new file mode 100644
index 0000000..5b1f156
--- /dev/null
+++ b/hzh/TEST/MicroService.md
@@ -0,0 +1,1171 @@
+---
+tags:
+ - 后端
+ - 微服务
+ - 测试
+ - 自我考察
+create time: 2026-05-06 12:00
+update time: 2026-05-06
+status: reviewed
+---
+
+# 微服务架构自测题
+
+## 概述
+
+本文档是一份微服务架构知识的自测题库,覆盖从基础概念到部署运维的全链路知识点。包含选择题、填空题、代码补全和主观题四种题型,附带详细参考答案与解析,适合用于自我评估或团队内部考核。
+
+## 正文
+
+### 使用说明
+
+本试卷覆盖 **基础概念 → 服务治理 → 数据一致性 → 可观测性 → 部署运维** 全链路知识点。共四部分:选择题、填空题、代码补全、主观题。
+
+**建议用时:** 60 分钟 | **及格线:** 70 / 100
+
+做完后对照下方「参考答案」自评,错的地方回到对应笔记重新理解。
+
+---
+
+## 一、选择题(每题 4.5 分,共 45 分)
+
+> 每题只有一个正确答案。
+
+### Q1.【微服务的拆分原则】
+
+关于 DDD(领域驱动设计)中"限界上下文"的微服务拆分原则,以下说法**正确**的是:
+
+A. 应该按技术层拆分——一个服务管数据库,一个服务管业务逻辑,一个服务管 Web 接口
+B. 每个微服务应围绕一个业务能力边界独立工作,各自拥有独立的数据库
+C. 为了减少跨服务调用,多个相关表可以放在同一个微服务共享数据库中
+D. 单体应用越大越好,因为可以避免分布式系统的复杂度
+
+> [!tip]- Q1 答案
+> **B — 每个微服务应围绕业务能力边界独立工作,且拥有独立数据库**
+>
+> **解析:**
+> DDD 的核心理念是**通过统一语言定义业务边界**,将系统拆分为相互协作的限界上下文(Bounded Context)。核心原则:
+>
+> - **独立数据库**:每个服务独占数据存储,禁止跨服务直接访问 DB
+> - **能力边界**:按业务域而非技术层拆分——"订单管理"是一个上下文,"用户认证"是另一个
+> - **松耦合**:服务间通过明确的 API 通信,内部实现互不透明
+>
+> **为什么其他选项错误:**
+> - ❌ A:按技术层拆分会导致"分布式单体"——所有功能紧密耦合,修改一处牵动全局
+> - ❌ C:共享数据库违反了微服务的自治原则,是典型的反模式
+> - ❌ D:微服务解决了单体的可扩展性和团队并行开发问题,但会引入分布式复杂度,需要权衡
+>
+> ```mermaid
+> graph LR
+> BAD["❌ 反模式:共享数据库"] --> S1["Order Service"]
+> BAD --> S2["Payment Service"]
+> S1 --> SHARED[(共享 DB)]
+> S2 --> SHARED
+>
+> GOOD["✅ 正模式:独立数据库"] --> O1[Order Service]
+> GOOD --> O2[Payment Service]
+> O1 --> DB1[(Order DB)]
+> O2 --> DB2[(Payment DB)]
+> ```
+>
+> 💡 **记忆要点:** "一个服务 = 一个业务域 + 一个数据库 + 一套团队",三者不可分割。
+
+---
+
+### Q2.【服务发现的注册方式】
+
+客户端发现模式 vs 服务端发现模式中,以下对比**最准确**的是:
+
+A. 客户端发现需要额外部署负载均衡器组件,服务端发现不需要
+B. 客户端发现将服务列表缓存到客户端,服务端发现由中间代理转发请求
+C. 两者没有本质区别,只是实现上的差异
+D. 服务端发现性能更高,因为减少了网络跳数
+
+> [!tip]- Q2 答案
+> **B — 客户端自行查注册中心 + 缓存;服务端由代理中转**
+>
+> **解析:**
+>
+> | 维度 | 客户端发现 | 服务端发现 |
+> |------|-----------|-----------|
+> | **工作方式** | 客户端查注册中心获取实例列表,自己选实例 | 客户端请求代理/网关,代理查注册中心并转发 |
+> | **代表工具** | Consul + Serf、Nacos 客户端、Eureka | Kubernetes Service、Envoy、Nginx |
+> | **部署成本** | 客户端库内置,无需额外组件 | 需部署负载均衡层(Sidecar 或独立 LB) |
+> | **灵活性** | 客户端自定义策略(加权、地域等) | 对应用透明,开发简单但控制力弱 |
+> | **性能** | 更直接,少一跳 | 多一层代理转发 |
+>
+> **逐项分析:**
+> - ❌ A:说反了——客户端发现不需要额外 LB 组件,服务端才需要
+> - ✅ B:完全正确描述了两种模式的核心区别
+> - ❌ C:有本质区别,涉及架构决策和部署拓扑变化
+> - ❌ D:相反,服务端发现多了一层代理跳数,理论上延迟更高
+>
+> ```mermaid
+> graph LR
+> Client["Client App"] -.查注册中心.-> RC[Registry]
+> Client -->|自选实例| Inst1[Instance A]
+> Client -->|自选实例| Inst2[Instance B]
+> style Client fill:#cfe2f3
+> ```
+>
+> vs
+>
+> ```mermaid
+> graph LR
+> Client["Client App"] --> Proxy["Load Balancer / Sidecar"]
+> Proxy -.查注册中心.-> RC[Registry]
+> Proxy --> Inst1[Instance A]
+> Proxy --> Inst2[Instance B]
+> ```
+>
+> 💡 **Kubernetes 场景:** K8s Service 就是典型的服务端发现模式,Service IP 作为虚拟入口,kube-proxy/IPVS 负责负载均衡。
+
+---
+
+### Q3.【熔断器状态转换】
+
+一个熔断器的配置如下:连续失败 5 次打开,恢复超时 30 秒。当熔断器从 Open 进入 Half-Open 后,放行了一个探测请求,该请求成功。熔断器接下来会怎样?
+
+A. 继续保持 Half-Open,再观察几个请求
+B. 回到 Open 状态,因为刚恢复不能轻信
+C. 回到 Closed 状态,恢复正常放行全部流量
+D. 关闭整个服务,防止不稳定
+
+> [!tip]- Q3 答案
+> **C — Half-Open 探测成功则回到 Closed**
+>
+> **解析:**
+> 熔断器的三态转换规则:
+>
+> ```mermaid
+> graph LR
+> C["Closed"] -->|连续失败超过阈值| O["Open"]
+> O -->|recoveryTimeout 到期| H["Half-Open"]
+> H -->|探测成功| C
+> H -->|探测失败| O
+> style C fill:#e8f5e9
+> style O fill:#ffebee
+> style H fill:#fff3e0
+> ```
+>
+> **半开状态的本质**是一次"试探":如果下游恢复了,就大胆地回到 Closed;如果还是不行,迅速再次打开,避免反复横跳消耗资源。
+>
+> 💡 **工程经验:** 很多团队把 recoveryTimeout 设得太短(如 5s),导致下游还没恢复好就已经回 Closed,形成"熔断振荡"。通常建议至少 30s~1m。
+
+---
+
+### Q4.【Outbox 模式的工作原理】
+
+关于 Outbox 模式保证消息投递的一致性,以下说法**正确**的是:
+
+A. 先写业务库再发消息,利用事务的回滚机制确保一致性
+B. 业务数据和消息写入同一个本地事务的 outbox 表,然后后台轮询发送
+C. 使用两阶段提交(2PC)协议同时提交业务数据和消息
+D. 在 MQ 层面设置事务消息即可,不需要额外的 outbox 表
+
+> [!tip]- Q4 答案
+> **B — 同事务写业务表和 outbox 表,后台定时任务投递**
+>
+> **解析:**
+> Outbox 模式的核心思想:**把"发MQ消息"这件事降级为"写DB一行"**,从而利用本地事务保证原子性。
+>
+> **关键步骤:**
+> 1. 业务操作与写入 outbox 表在同一个 `BEGIN...COMMIT` 中完成
+> 2. 出事务后,后台 worker 定时扫描 outbox 表中 `status='pending'` 的记录
+> 3. 投递成功后更新 status 为 `'sent'`;失败则标记 `'failed'` 等待重试
+>
+> ```go
+> // 事务内同时写业务数据和 outbox
+> tx.Exec("INSERT INTO orders (...) VALUES ...")
+> tx.Exec("INSERT INTO outbox (topic, payload, status) VALUES ('order.created', $1, 'pending')", payload)
+> tx.Commit() // 要么都成功,要么都回滚
+> ```
+>
+> **逐项分析:**
+> - ❌ A:先写业务再发消息时,如果发消息失败,业务已 commit,无法回滚——这就是"可靠事件发布"要解决的问题
+> - ❌ C:2PC 太重了,不是 Outbox 的设计目标
+> - ❌ D:RocketMQ 事务消息确实可以不用 Outbox 表,但这说的是 RocketMQ 原生方案,不是 Outbox 模式本身
+>
+> 💡 **优化技巧:** outbox 表的查询索引一定要带上 `(status, created_at)`,否则大规模下定时任务会成为性能瓶颈。
+
+---
+
+### Q5.【幂等性设计】
+
+在消息队列消费场景中,以下哪种方案**不能完全保障**消费幂等性?
+
+A. 用 msg_id 做数据库 UNIQUE 约束,INSERT ... ON CONFLICT DO NOTHING
+B. Redis SET key NX EX 去重键
+C. 乐观锁版本控制:`UPDATE table SET count = count + N WHERE version = V`
+D. 消费者收到消息后 sleep 100ms 再处理
+
+> [!tip]- Q5 答案
+> **D — sleep 不能替代真正的幂等机制**
+>
+> **解析:**
+> 幂等的本质是:**多次执行同一操作的结果相同**。sleep 只是一个延迟手段,无法防止重复消费。
+>
+> | 方案 | 可靠性 | 适用场景 |
+> |------|--------|---------|
+> | UNIQUE 约束 | ⭐⭐⭐⭐⭐ | 最可靠,强烈推荐 |
+> | Redis NX | ⭐⭐⭐⭐ | 高吞吐但极端情况下可能有窗口期 |
+> | 乐观锁版本 | ⭐⭐⭐⭐ | 适合金额、库存等计数类操作 |
+> | sleep | ❌ 无效 | 与幂等无关 |
+>
+> **为什么 B 也有风险?** Redis 存在 `SET NX` 检查和实际执行之间的极小时间窗,但如果用 Redis Lua 脚本可以做到原子性,所以实践中可用。
+>
+> ```go
+> // 正确的幂等检查顺序:
+> // 1. 先从 outbox 或结果表查是否已处理
+> // 2. 如果未处理,执行业务逻辑
+> // 3. 记录处理结果到 outbox/result 表
+> ```
+>
+> 💡 **核心认知:** 幂等性必须依靠**状态判断 + 条件写入**,而不是任何规避策略(sleep、随机等待等)。
+
+---
+
+### Q6.【分布式 ID — 雪花算法】
+
+标准 Snowflake 算法中 64 位 ID 的分布为:1 位符号位 + 41 位时间戳 + 10 位 WorkerId + 12 位序列号。以下说法**错误**的是:
+
+A. 41 位时间戳可以支撑约 69 年($2^{41} \text{ ms}$ ≈ 69.7 年)
+B. 12 位序列号意味着单台机器每毫秒最多生成 4096 个 ID
+C. 10 位 WorkerId 支持最大 1024 个节点
+D. 时钟回拨时应该立即抛出 panic 终止服务运行
+
+> [!tip]- Q6 答案
+> **D — 时钟回滑应有更优雅的处理策略,而不是一律 panic**
+>
+> **解析:**
+>
+> | 位段 | 位数 | 含义 |
+> |------|------|------|
+> | 符号位 | 1 | 固定为 0,保证 ID 为正数 |
+> | 时间戳 | 41 | 相对当前纪元的毫秒偏移,约 69 年 |
+> | WorkerId | 10 | 机器编号,支持 1024 台 |
+> | 序列号 | 12 | 同毫秒内的递增序号,每毫秒最多 4096 |
+>
+> **时钟回滑处理方案:**
+> - **等待策略**:阻塞直到时间追上来(可能影响可用性)
+> - **备用 WorkerId**:切换到预分配的备用 ID 继续生成
+> - **panic**:最简单但也最粗暴,适合容忍短时的不可用
+>
+> ```go
+> if now < sf.lastTime {
+> // 推荐:等待回拨期间再继续,而不是 panic
+> time.Sleep(time.Until(sf.lastTime + 1))
+> }
+> ```
+>
+> **为什么 D 错误:** panic 是一种极端手段。在生产环境中,短暂的时钟同步偏差(NTP 调整)不应导致整个服务崩溃。更好的做法是等待或者用日志记录告警。
+>
+> 💡 **演进:** UUID v7 就是为了解决 Snowflake 的 WorkerId 分配问题和时钟回滑问题而设计的标准方案。
+
+---
+
+### Q7.【可观测性三大支柱】
+
+关于 Metrics、Logging、Tracing 三种可观测性能力的区别,以下哪项描述**最准确**?
+
+A. Metrics 回答"发生了什么",Logging 回答"出事了没",Tracing 回答"在哪一步出的事"
+B. Metrics 回答"系统现在健康吗",Logging 回答"具体发生了什么",Tracing 回答"请求在哪步慢了/失败了"
+C. 三者都是记录历史数据的,用途基本一样
+D. Tracing 只能用于排查慢请求,不能用于监控系统健康度
+
+> [!tip]- Q7 答案
+> **B — 三个支柱各司其职,解决不同维度的问题**
+>
+> **解析:**
+>
+> | 支柱 | 核心问题 | 数据类型 | 典型工具 |
+> |------|---------|---------|---------|
+> | **Metrics** | "现在健康吗?" | 数值趋势(Counter/Gauge/Histogram) | Prometheus + Grafana |
+> | **Logging** | "具体发生了什么?" | 文本/结构化记录 | ELK / Loki |
+> | **Tracing** | "在哪一步出了问题?" | 时序链路图(span tree) | Jaeger / Tempo |
+>
+> **类比理解:**
+> - Metrics 像**仪表盘**📊:告诉你的速度、油耗、水温是否正常
+> - Logging 像**行车记录仪**📹:回放事故前后的详细情况
+> - Tracing 像**GPS 路线图**🗺️:显示经过哪些路口、在每个地方花了多久
+>
+> ```mermaid
+> graph TD
+> Problem["用户报告页面加载慢"]
+> Problem --> M{"看 Metrics"}
+> M -->|P99 延迟飙升| T["追踪 Tracing 定位到哪个 Span 慢"]
+> T --> L["查看该 Span 对应的 Logs 找根因"]
+> style M fill:#e8f5e9
+> style T fill:#fff3e0
+> style L fill:#fce4ec
+> ```
+>
+> 💡 **RED 方法:** 对服务级监控关注 **R**ate(请求率)、**E**rror(错误率)、**D**uration(持续时间);对基础设施关注 **U**tilization(利用率)、**S**aturation(饱和度)、**E**rrors(错误)。
+
+---
+
+### Q8.【K8s Probe 类型】
+
+在 Kubernetes 中,Liveness Probe 和 Readiness Probe 的行为差异是:
+
+A. Liveness 挂了重启 Pod,Readiness 挂了暂停接收流量但不重启
+B. Liveness 暂停接收流量,Readiness 挂了重启 Pod
+C. 两者作用完全相同,都是健康检查
+D. Liveness 只检查 HTTP 状态码,Readiness 只检查 TCP 连接
+
+> [!tip]- Q8 答案
+> **A — Liveness = 进程生死判断;Readiness = 流量开关**
+>
+> **解析:**
+>
+> | 探针类型 | 判断什么 | 失败行为 | 典型用途 |
+> |---------|---------|---------|---------|
+> | **Liveness** | "进程还活着吗?" | 重启容器(kubectl exec / Docker stop) | 死锁、goroutine leak 等不可恢复故障 |
+> | **Readiness** | "能接收流量了吗?" | 从 Endpoint 中移除,不再转发流量 | 启动中、依赖 DB 不可用、预热未完成 |
+> | **Startup** | "应用开始响应了吗?" | 根据配置可以选择不复活或直接杀掉 | 冷启动极慢的应用 |
+>
+> **关键点:** 很多时候**Readiness 比 Liveness 更重要**。比如数据库连接池耗尽时,进程是"活着"的,但不应该再接收流量——此时 Liveness probe 通过(不会重启),Readiness probe 失败(停止接流量)。
+>
+> ```yaml
+> # K8s manifest 示例
+> livenessProbe:
+> httpGet:
+> path: /healthz
+> port: 8080
+> initialDelaySeconds: 10
+> periodSeconds: 5
+>
+> readinessProbe:
+> httpGet:
+> path: /ready
+> port: 8080
+> initialDelaySeconds: 5
+> periodSeconds: 3
+> ```
+>
+> 💡 **常见陷阱:** Liveness probe 太激进(initialDelaySeconds 太小)会在启动过程中误杀正在初始化的容器。
+
+---
+
+### Q9.【Saga 补偿设计】
+
+在使用 Saga 模式处理"下单 → 扣库存 → 扣款"流程时,如果"扣款"步骤失败,正确的补偿操作链是:
+
+A. 退款 → 释放库存 → 取消订单
+B. 取消订单 → 释放库存 → 退款
+C. 只需退款,其他步骤自动回滚
+D. 重新尝试"扣款",无限重试直到成功
+
+> [!tip]- Q9 答案
+> **A — 按正向操作的逆序依次执行补偿**
+>
+> **解析:**
+> Saga 的补偿遵循**逆序原则**(Reverse Order Compensation):
+>
+> ```
+> 正向链路:CreateOrder → ReserveStock → ChargePayment
+> ↓ ↓ ↓
+> 逆向补偿:CancelOrder ← ReleaseStock ← RefundPayment
+> (扣款失败触发补偿链,从右往左走)
+> ```
+>
+> **为什么必须逆序?** 因为后续步骤的执行依赖前置步骤的状态。比如你不能先取消订单再释放库存——订单已经没了,库存管理器不知道要释放谁的库存。
+>
+> ```mermaid
+> flowchart LR
+> Fail["扣款失败"]
+> Fail --> Refund["① 退款"]
+> Refund --> Release["② 释放库存"]
+> Release --> Cancel["③ 取消订单"]
+> Cancel --> Done["✅ 全部补偿完成"]
+> style Fail fill:#ffebee
+> style Done fill:#e8f5e9
+> ```
+>
+> **补偿操作的幂等性**同样重要——CancelOrder 可能被触发多次,需要用订单状态机来保证(只有 PENDING 才能转 CANCELLED)。
+>
+> 💡 **TCC 对比:** TCC 的好处是在 Try 阶段就冻结了资源,Confirm 时才真正占用,避免了中间态的数据可见性问题。代价是每个业务方法都要实现三个接口。
+
+---
+
+### Q10.【CI/CD 中的 GitOps 流程】
+
+GitOps 与传统 CI/CD Pipeline 的核心区别在于:
+
+A. GitOps 不使用自动化流水线,全靠人工操作
+B. GitOps 以 Git 仓库为唯一事实源,控制器持续将集群状态同步到期望状态
+C. GitOps 只能用 GitHub Actions,不能用 Jenkins
+D. GitOps 不需要容器镜像,直接在 Git 里存储二进制文件
+
+> [!tip]- Q10 答案
+> **B — Git 仓库即唯一真相来源,控制器持续 reconcile**
+>
+> **解析:**
+>
+> ```mermaid
+> graph LR
+> Dev["开发者提交 Manifest 到 Git"] --> Controller["ArgoCD / Flux Controller"]
+> Controller -->|Pull 并 diff| Cluster["K8s Cluster"]
+> Controller -->|Apply 变更| Cluster
+> Cluster -->|实时 sync 状态| Controller
+> style Dev fill:#cfe2f3
+> style Controller fill:#fff3e0
+> style Cluster fill:#e8f5e9
+> ```
+>
+> **传统 CI/CD:** 推送构建产物 → 部署脚本报名 → 直接 push 到集群
+>
+> **GitOps:** 改 Manifest → PR/Merge → 控制器自动检测到变更 → 自动 apply
+>
+> | 维度 | 传统 CI/CD | GitOps |
+> |------|-----------|--------|
+> | 事实源 | 部署日志 + CI 产物 | Git 仓库中的 Manifest |
+> | 驱动方向 | Push(主动推送到集群) | Pull(控制器拉取并同步) |
+> | 漂移检测 | 无或手动比对 | 自动实时检测 |
+> | 回滚方式 | 重新部署旧版本 | Git revert + merge |
+>
+> 💡 **核心理念:** "声明式基础设施"——你只需要描述**想要的最终状态**(YAML),剩下的交给控制器去 reconcile。
+
+---
+
+## 二、填空题(每题 3 分,共 24 分)
+
+> 根据知识填写空缺的概念或代码。
+
+### Q11.【API Gateway 职责分类】
+
+以下操作中,哪些适合放在 API Gateway(**G**),哪些不应该(**O**,下沉到业务服务)?
+
+| 操作 | 位置 |
+|------|------|
+| HTTPS 终结 | _(空1)_ |
+| 创建订单(业务逻辑) | _(空2)_ |
+| JWT Token 校验 | _(空3)_ |
+| 邮件发送通知 | _(空4)_ |
+
+> [!tip]- Q11 答案
+> **G** ; **O** ; **G** ; **O**
+>
+> **解析:**
+>
+> | 操作 | 位置 | 理由 |
+> |------|------|------|
+> | HTTPS 终结 | G | 网关的统一入口职责,TLS 卸载 |
+> | 创建订单 | O | 业务逻辑应下沉,网关保持瘦 |
+> | JWT Token 校验 | G | 统一的鉴权前置条件 |
+> | 邮件发送通知 | O | 异步非关键路径,应在业务服务内处理 |
+>
+> ```mermaid
+> graph LR
+> Client --> GW["API Gateway
HTTPS/JWT/限流/路由"]
+> GW --> Svc["业务服务
订单/支付/邮件..."]
+> style GW fill:#cfe2f3
+> style Svc fill:#e8f5e9
+> ```
+>
+> 💡 **黄金法则:** 网关是 traffic cop,不是 warehouse manager。任何需要"知道业务状态"的操作都不该放在网关。
+
+---
+
+### Q12.【指数退避计算】
+
+某系统配置重试 3 次,基础等待时间为 1s,Jitter 范围为 ±100ms。第 2 次重试的理论等待时间范围是 _____ s 到 _____ s。
+
+> [!tip]- Q12 答案
+> **1.9** ; **2.1**(或 **2.0±0.1**)
+>
+> **解析:**
+>
+> 指数退避公式:`wait = base × 2^(attempt-1) + jitter`
+>
+> | 重试次数 | 基础等待 | Jitter | 总等待 |
+> |---------|---------|--------|--------|
+> | 第 1 次 | 1 × 2^0 = 1s | ±100ms | 0.9 ~ 1.1s |
+> | **第 2 次** | **1 × 2^1 = 2s** | **±100ms** | **1.9 ~ 2.1s** |
+> | 第 3 次 | 1 × 2^2 = 4s | ±100ms | 3.9 ~ 4.1s |
+>
+> 💡 **Jitter 的意义:** 如果没有 jitter,所有客户端同时重试会产生"retry storm"——像多米诺骨牌一样放大下游压力。
+
+---
+
+### Q13.【Prometheus 指标类型匹配】
+
+请将 Prometheus 的四种基础指标类型与其用途连线:
+
+> [!tip]- Q13 答案
+> **Counter = A(单调递增);Gauge = D(可上可下);Histogram = C(分布统计);Summary = B(客户端计算分位数)**
+>
+> **解析:**
+>
+> | 类型 | 特征 | 示例 |
+> |------|------|------|
+> | **Counter** | 只能增加(或重置为零) | 总请求数、错误计数 |
+> | **Gauge** | 可上可下,表示瞬时值 | 当前在线人数、内存使用量 |
+> | **Histogram** | 采样+分桶,服务端算百分位 | 请求耗时分布 |
+> | **Summary** | 采样+分桶,客户端算百分位 | SDK 内部聚合的分位数 |
+>
+> ```go
+> // Go 中 prometheus 库的使用
+> requestsTotal := promauto.NewCounterVec(
+> prom.CounterOpts{Name: "http_requests_total"},
+> []string{"method", "status"},
+> )
+> // CounterVec 带 label,可按 method/status 维度统计
+> ```
+>
+> 💡 **Histogram vs Summary:** 大多数场景用 Histogram 就够了,因为它可以在服务端通过 PromQL 灵活合并多个实例的数据。Summary 的 pre-computed quantiles 无法在服务端聚合。
+
+---
+
+### Q14.【gRPC 方法类型】
+
+gRPC 支持四种 RPC 方法类型,请写出另外三种的名称:
+
+> [!tip]- Q14 答案
+> **Unary(普通 RPC);Server Stream(服务端流式);Client Stream(客户端流式);Bidirectional Stream(双向流式)**
+>
+> **解析:**
+>
+> ```proto
+> // Proto3 定义的四种 RPC 模式
+> service WeatherService {
+> // 1. Unary —— 单次请求一次响应
+> rpc GetWeather(GetWeatherRequest) returns (WeatherResponse);
+>
+> // 2. Server Stream —— 一次请求,服务端返回数据流
+> rpc ListForecasts(ForecastRequest) returns (stream ForecastResponse);
+>
+> // 3. Client Stream —— 客户端发数据流,服务端最后返回一次响应
+> rpc AggregateTemperatures(stream TemperatureReading) returns (TemperatureReport);
+>
+> // 4. Bidirectional Stream —— 双方都在发数据流
+> rpc Chat(stream ChatMessage) returns (stream ChatMessage);
+> }
+> ```
+>
+> | 类型 | 请求 | 响应 | 应用场景 |
+> |------|------|------|---------|
+> | Unary | 1 → 1 | 1 → 1 | 最常见,CRUD |
+> | Server Stream | 1 → N | 1 → ∞ | 实时订阅、列表推送 |
+> | Client Stream | ∞ → 1 | N → 1 | 大数据上传、批量读取 |
+> | Bidirectional | ∞ → ∞ | ∞ → ∞ | WebSocket 替代、游戏服务器 |
+>
+> 💡 **gRPC Streaming 优势:** 基于 HTTP/2 多路复用,可以同时维持多个独立的流,不需要建立多个 TCP 连接。
+
+---
+
+### Q15.【容器化最佳实践】
+
+Dockerfile 最佳实践中,以下三个做法分别对应了什么目的?
+
+```dockerfile
+# (1) FROM golang:1.23-alpine AS builder
+# (2) COPY --from=builder /app/server .
+# (3) USER nobody
+```
+
+_(空1)_ → 减小镜像体积 _(空2)_ → 安全隔离 _(空3)_ → 多阶段构建
+
+> [!tip]- Q15 答案
+> **(3) / USER nobody** → 减小镜像体积;**(2) / COPY --from=builder** → 安全隔离;**(1) / FROM ... AS builder** → 多阶段构建
+>
+> **解析:**
+>
+> **多阶段构建的目的:** 编译环境(包含 go toolchain、依赖安装器等)和运行环境(仅运行时二进制)分离,最终镜像不包含编译工具。
+>
+> **USER 的目的:** 不以 root 身份运行容器进程。即使镜像有漏洞,攻击者也无法获得 root 权限。
+>
+> ```dockerfile
+> # 完整的多阶段构建示例
+> FROM golang:1.23-alpine AS builder
+> WORKDIR /app
+> COPY . .
+> RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server .
+>
+> FROM alpine:3.20
+> RUN apk --no-cache add ca-certificates tzdata
+> COPY --from=builder /app/server .
+> USER nobody
+> ENTRYPOINT ["./server"]
+> ```
+>
+> 💡 **distroless:** 进一步用 Google distroless 镜像替代 alpine,镜像更小且不包含 shell,安全性更高。
+
+---
+
+### Q16.【W3C Trace Context 传播】
+
+HTTP 请求头 `Traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01` 中,各字段的含义分别是:
+
+`_version_` - `_trace_id (16 bytes hex)_` - `_span_id (16 bytes hex)_` - `_flags_`
+
+其中 `01`(十六进制)对应的标志位含义是 _________。
+
+> [!tip]- Q16 答案
+> **`1`**(表示 Trace 被**采样/记录**,flag=00 表示不采样)
+>
+> **解析:**
+> W3C Trace Context 标准的请求头格式:
+>
+> ```
+> Traceparent: version-trace_id-span_id-trace_flags
+> │ │ │ └── 1字节标志位
+> │ │ └────────── 16字节 Span ID
+> │ └──────────────────── 32字节 Trace ID
+> └───────────────────────────── 版本号(目前固定为 00)
+> ```
+>
+> - **trace_id (32 hex chars = 16 bytes)**:标识一次完整的请求链路,同一链路的所有 span 共享同一个 trace_id
+> - **span_id (16 hex chars = 8 bytes)**:标识链路中的一个单独操作,每次 RPC 调用都会产生新的 span_id
+> - **trace_flags (1 byte)**:`01` = 已采样( sampled),`00` = 未采样
+>
+> ```go
+> // Go 中通过 Context 传播 trace context
+> ctx, _ := trace.StartSpan(ctx, "UserService.GetUserInfo",
+> trace.WithSpanKind(trace.SpanKindServer),
+> )
+> defer span.End()
+> ```
+>
+> 💡 **OpenTelemetry 的优势:** OTel 统一了 tracer provider、meter provider、logger provider 的 SDK 规范,支持 auto-instrumentation,一个 agent 搞定所有信号采集。
+
+---
+
+### Q17.【蓝绿 vs 金丝雀发布】
+
+蓝绿发布(Blue-Green)和金丝雀发布(Canary)的核心区别是:蓝绿发布切换流量时使用 ___________,金丝雀发布时使用 ___________。
+
+> [!tip]- Q17 答案
+> **完全切换(全部流量从蓝切到绿)** ; **按权重/比例逐步切换(如 5% → 20% → 50% → 100%)**
+>
+> **解析:**
+>
+> | 维度 | 蓝绿发布 | 金丝雀发布 |
+> |------|---------|-----------|
+> | 流量切换 | 一次性 100% 切换 | 渐进式(灰度放量) |
+> | 资源占用 | 需要两套完整环境 | 只需增量一份新实例 |
+> | 风险 | 切换后发现严重 bug,所有用户受影响 | 小流量先行验证,发现问题影响面小 |
+> | 回滚速度 | 瞬间(切回另一套环境即可) | 快速(降权新版本即可) |
+> | 典型工具 | K8s Deployment 切换 Service selector | Istio VirtualService weight、Nginx 权重 |
+>
+> ```yaml
+> # Istio VirtualService 金丝雀示例
+> route:
+> - destination:
+> host: app-v1
+> weight: 90 # 90% 流量走 v1
+> - destination:
+> host: app-v2
+> weight: 10 # 10% 流量走 v2(灰度)
+> ```
+>
+> 💡 **选择策略:** 小型改动用蓝绿,敏感业务上线用金丝雀。
+
+---
+
+### Q18.【分布式事务选择策略】
+
+根据业务需求选择合适的分布式事务方案:
+
+| 场景 | 推荐方案 |
+|------|---------|
+| 创建订单后发送通知,允许短暂不一致 | _(空1)_ |
+| 资金转账,需要强一致性保障 | _(空2)_ |
+| 不想改造现有业务代码,想自动管理分布式事务 | _(空3)_ |
+
+> [!tip]- Q18 答案
+> **本地事务 + MQ(Outbox 模式)** ; **TCC** ; **AT 模式(Seata)**
+>
+> **解析:**
+>
+> 三种方案的权衡矩阵:
+>
+> | 方案 | 一致性 | 性能 | 侵入性 | 一句话总结 |
+> |------|--------|------|--------|-----------|
+> | Outbox + MQ | 最终一致 | 高 | 低 | 默认首选,覆盖 80% 场景 |
+> | TCC | 准强一致 | 中 | 高 | 每个业务方法写 Three 个接口 |
+> | AT 模式 | 伪强一致 | 中 | 极低 | Seata 自动生成 undo_log |
+>
+> ```mermaid
+> flowchart TD
+> Req["你需要分布式事务吗?"]
+> Req -->|"否:最终一致即可"| Outbox["Outbox + MQ ✅ 默认选这个"]
+> Req -->|"是:强一致性要求"| Choice{"能否改造业务代码?"}
+> Choice -->|"能"| TCC["TCC ✅ 一致性最强"]
+> Choice -->|"不能 | 不能接受高侵入"| AT["AT 模式 ✅ 零代码改动"]
+> style Outbox fill:#e8f5e9
+> style TCC fill:#fff3e0
+> style AT fill:#fce4ec
+> ```
+>
+> 💡 **经验法则:** 能用异步事件解决的就不用 Saga/TCC/AT——它们都有各自的复杂度和 trade-off。
+
+---
+
+## 三、代码补全(每题 6 分,共 30 分)
+
+> 补充代码中空缺的部分。有些题目有多个空。
+
+### Q19.【熔断器封装】
+
+```go
+func callWithCircuitBreaker(cb *breaker.CircuitBreaker, timeout time.Duration, fn func() error) error {
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+
+ errChan := make(chan error, 1)
+ go func() {
+ errChan <- fn() // 在 goroutine 中执行远程调用
+ }()
+
+ select {
+ case err := <-errChan:
+ return err
+ case <-ctx.Done():
+ return _________ // 空1:返回超时报错
+ }
+}
+
+// 使用熔断器包裹调用
+result := cb.Execute(func() (any, error) {
+ return callWithCircuitBreaker(cb, 3*time.Second, func() error {
+ return client.CallService(context.Background())
+ })
+})
+if result.Err != nil {
+ log.Warn("服务调用失败,已短路", result.Err)
+ return fallbackResponse()
+}
+```
+
+> [!tip]- Q19 答案
+> **`fmt.Errorf("call timed out: %w", ctx.Err())`**
+>
+> **解析:**
+>
+> **超时处理的核心模式:**
+> ```
+> goroutine 执行调用
+> ├── 正常完成 → errChan 返回结果
+> └── 超时 → ctx.Done() 先触发 → 返回错误
+> ```
+>
+> | 空 | 填空 | 作用 |
+> |----|------|------|
+> | 空1 | `fmt.Errorf("call timed out: %w", ctx.Err())` | 保留原始 context 错误语义,支持 errors.Is 判断 |
+>
+> **关键点:**
+> - `context.DeadlineExceeded` 是最常见的超时报错,可以用 `errors.Is(err, context.DeadlineExceeded)` 精确判断
+> - 配合熔断器使用时,超时也应该计入熔断器统计——超时也是一种"失败"
+>
+> ```go
+> if errors.Is(result.Err, context.DeadlineExceeded) {
+> // 超时 → 可能是下游负载过高 → 熔断器会增加失败计数
+> }
+> ```
+>
+> 💡 **超时传递:** 子调用的超时时间必须是父调用剩余时间的子集。如果网关给订单服务分配了 200ms,订单服务调用库存服务就不能也给 200ms——至少要留出 50~100ms 余量给上游。
+
+---
+
+### Q20.【服务发现客户端实现】
+
+```go
+type DiscoveryClient struct {
+ registry map[string][]string // service -> instances
+ mu sync.RWMutex
+}
+
+func (dc *DiscoveryClient) Refresh() error {
+ // 从注册中心拉取最新的实例列表
+ instances, err := fetchFromRegistry()
+ if err != nil {
+ return err
+ }
+
+ dc.mu.Lock()
+ defer dc.mu.Unlock()
+
+ newMap := make(map[string][]string)
+ for _, inst := range instances {
+ newMap[inst.ServiceName] = append(newMap[inst.ServiceName], inst.Addr)
+ }
+ dc.registry = newMap // 替换为新列表
+ return nil
+}
+
+// GetInstances 获取指定服务的可用实例列表
+func (dc *DiscoveryClient) GetInstances(service string) ([]string, error) {
+ dc.mu._________ // 空1:使用读锁保护
+ instances, ok := dc.registry[service]
+ if !ok {
+ return nil, fmt.Errorf("service %s not found", _________) // 空2
+ }
+ return instances, nil
+}
+```
+
+> [!tip]- Q20 答案
+> **`RLock()`** ; **`service`**
+>
+> **解析:**
+>
+> **读写锁的正确使用场景:**
+> - `Lock()`(写锁):Refresh 时替换整个 registry map——并发频率低
+> - `RLock()`(读锁):GetInstances 时读取实例列表——并发频率高,多个请求同时查找不影响
+>
+> **为什么不用互斥锁?** 服务发现的读远多于写。如果用 `Mutex`,每次 GetInstances 都会串行化,在高并发下成为瓶颈。`RWMutex` 允许多个读者同时执行,大幅提升吞吐量。
+>
+> | 操作 | 频率 | 锁类型 |
+> |------|------|--------|
+> | Refresh(从注册中心拉取) | 低频(几十秒一次) | Write Lock |
+> | GetInstances(路由选择) | 高频(每次 RPC 调用) | Read Lock |
+>
+> ```go
+> // RWMutex 特性
+> reader1 ---||------- Reader A (并行)
+> reader2 ---||------- Reader B (并行)
+> writer --|X|------- 排他,读写互斥
+> ```
+>
+> 💡 **生产实践:** Nacos/Consul 客户端内部都会维护一个 instance cache,定时刷新(watch/polling),GetInstances 只读缓存——避免每次调用都直接查注册中心造成雪崩。
+
+---
+
+### Q21.【分布式追踪 Span 埋点】
+
+```go
+import "go.opentelemetry.io/otel/trace"
+
+func (s *OrderService) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.Order, error) {
+ // 空1:开启一个新的 Span
+ ctx, span := _________
+
+ defer span.End()
+ span.SetAttributes(attribute.String("user.id", req.UserId))
+ span.SetAttributes(attribute.Int64("order.total", req.TotalAmount))
+
+ // 调用库存服务
+ // 空2:注入 span context 到 gRPC metadata 以便下游继承
+ md := metadata.Pairs(trace.HeaderKey, _________)
+ ctx = metadata.NewOutgoingContext(ctx, md)
+
+ stockResp, err := s.stockClient.ReserveStock(ctx, &pb.ReserveRequest{...})
+ // ...
+ return order, nil
+}
+```
+
+> [!tip]- Q21 答案
+> **`tracer.Start(ctx, "OrderService.CreateOrder")`** ; **`span.TraceContext()`**
+>
+> **解析:**
+>
+> **OTel Span 的标准操作流程:**
+>
+> ```go
+> // 1. 初始化 tracer(通常在服务启动时做一次)
+> var tracer = otel.Tracer("my-service/order")
+>
+> // 2. 每个 handler/rpc 开始时创建 span
+> ctx, span := tracer.Start(ctx, "OpName")
+> defer span.End() // 确保 span 结束
+>
+> // 3. 记录属性和事件
+> span.SetAttributes(attribute.String("key", "value"))
+> span.AddEvent("step completed", trace.WithAttributes(...))
+>
+> // 4. 传播 context 到下游
+> // HTTP: otelhttp 会自动注入 Traceparent header
+> // gRPC: 需要手动注入 metadata
+> ```
+>
+> | 空 | 填空 | 说明 |
+> |----|------|------|
+> | 空1 | `tracer.Start(ctx, "OrderService.CreateOrder")` | 创建 Span 并返回增强后的 context |
+> | 空2 | `span.TraceContext()` | 提取 Trace Context 信息用于注入到下游请求 |
+>
+> ```go
+> // 更简洁的 gRPC 注入方式(推荐)
+> import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc"
+> // 使用 grpcbinjector 自动注入,无需手动处理 metadata
+> ```
+>
+> 💡 **采样策略:** 不是所有请求都需要记录完整的 trace。常用的采样策略有 AlwaysOn(开发)、AlwaysOff(生产全关)、ProbabilityBased(按比例)和 Adaptive(动态调整)。生产环境建议用概率采样,避免 trace 数据爆炸。
+
+---
+
+### Q22.【K8s Deployment 资源配置】
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: order-service
+spec:
+ replicas: 3
+ template:
+ spec:
+ containers:
+ - name: order-service
+ image: myrepo/order:v1.2.3
+ ports:
+ - containerPort: 8080
+ resources:
+ requests:
+ memory: "256Mi"
+ cpu: "250m"
+ limits:
+ memory: "512Mi"
+ cpu: "500m"
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 8080
+ initialDelaySeconds: ___ # 空1:给应用足够启动时间
+ periodSeconds: ___ # 空2:健康检查间隔不宜过短
+ readinessProbe:
+ httpGet:
+ path: /ready
+ port: 8080
+ initialDelaySeconds: 5
+ periodSeconds: 3
+```
+
+> [!tip]- Q22 答案
+> **10~30(或合理大整数)** ; **5~10**
+>
+> **解析:**
+>
+> **Resource Request vs Limit:**
+>
+> | 参数 | 含义 | K8s 调度影响 | OOM 影响 |
+> |------|------|------------|---------|
+> | **requests** | 保底资源,用于调度决策 | 调度器只在节点有 ≥ request 的资源时才安排 Pod | CPU 不会被限制(但可能竞争),内存超过不会 OOM Kill |
+> | **limits** | 硬上限 | 不参与调度计算 | CPU 超限被 throttling,内存超限被 OOM Kill |
+>
+> **Probes 参数建议:**
+>
+> | 参数 | 推荐值 | 原因 |
+> |------|--------|------|
+> | `initialDelaySeconds` (liveness) | 10~30 | 让应用在检测前有时间完成初始化 |
+> | `periodSeconds` (liveness) | 5~10 | 太频繁浪费资源,太长延迟发现故障 |
+> | `failureThreshold` | 3 | 连续失败 3 次才判定为死亡 |
+>
+> ```go
+> // Go 应用中实现 /ready 端点
+> var (
+> dbConnected bool
+> cacheConnected bool
+> )
+>
+> func readyHandler(w http.ResponseWriter, r *http.Request) {
+> if !dbConnected || !cacheConnected {
+> http.Error(w, "not ready", http.StatusServiceUnavailable)
+> return
+> }
+> w.WriteHeader(http.StatusOK)
+> }
+> ```
+>
+> 💡 **HPA 联动:** Resource requests/limits 是 HPA(Horizontal Pod Autoscaler)基于 CPU/Memory 自动扩缩容的基础——没有 resource metrics,基于资源的 HPA 无法工作。
+
+---
+
+### Q23.【ConfigMap 动态刷新】
+
+```go
+package config
+
+import (
+ "sync"
+)
+
+type Config struct {
+ mu sync.RWMutex
+ MaxRetries int
+ Timeout time.Duration
+ FeatureFlag bool
+}
+
+var globalCfg = &Config{}
+
+func Load(cfg *Config) {
+ globalCfg.mu.Lock()
+ defer globalCfg.mu.Unlock()
+ globalCfg.MaxRetries = cfg.MaxRetries
+ globalCfg.Timeout = cfg.Timeout
+ globalCfg.FeatureFlag = cfg.FeatureFlag
+}
+
+// GetMaxRetries 线程安全地读取配置
+func GetMaxRetries() int {
+ globalCfg.mu._______ // 空1:使用读锁
+ defer globalCfg.mu._______ // 空2
+ return globalCfg.MaxRetries
+}
+
+// 定期从配置中心(如 Nacos Config)拉取最新配置
+func WatchConfigChange(ctx context.Context, interval time.Duration) {
+ ticker := time.NewTicker(interval)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-ticker.C:
+ remoteCfg, _ := FetchFromConfigCenter()
+ Load(remoteCfg) // 热更新配置
+ }
+ }
+}
+```
+
+> [!tip]- Q23 答案
+> **`RLock()`** ; **`RUnlock()`**
+>
+> **解析:**
+>
+> **配置热更新的关键点:**
+> 1. **原子更新**:通过整块赋值(Load 函数)保证读到的是完整一致的配置快照
+> 2. **读写锁**:读取用 RLock(可并发),写入用 Lock(排他)
+> 3. **防惊群**:配置中心通常提供 watch/watch 长轮询机制,避免频繁 polling
+>
+> ```go
+> // 进阶:使用 RWMutex + 指针交换实现零锁读取
+> type Config struct {
+> data atomic.Value // 存储 *internalConfig
+> }
+>
+> func (c *Config) Reload(newCfg *internalConfig) {
+> c.data.Store(newCfg) // 原子替换指针,O(1) 更新
+> }
+>
+> func (c *Config) GetMaxRetries() int {
+> cfg := c.data.Load().(*internalConfig) // 无锁读取
+> return cfg.MaxRetries
+> }
+> ```
+>
+> | 空 | 填空 | 作用 |
+> |----|------|------|
+> | 空1 | `RLock()` | 多个 GetMaxRetries 调用可以并行执行 |
+> | 空2 | `RUnlock()` | 配对释放读锁 |
+>
+> 💡 **Apollo 实现原理:** Apollo 的客户端会在本地保存一份配置文件缓存,长轮询检测到变更后更新本地缓存,应用程序通过监听回调或主动读取来获取最新配置。核心也是"**缓存 + 异步更新**"。
+
+---
+
+## 四、主观题(每题 4 分,共 16 分)
+
+> 简要回答即可,不需要长篇大论。关键是说出你的理解。
+
+### Q24.【为什么微服务需要独立数据库?】
+
+如果两个微服务共享一个数据库,会带来哪些问题?请至少列举三点,并说明为什么这是"分布式单体"的反模式。
+
+> [!tip]- Q24 参考答案
+>
+> 1. **耦合度高**:两个服务的表结构互相依赖,修改一张表会影响两个服务。这违背了微服务"独立演化"的目标,变成用网络调用来通信的单体。
+> 2. **扩展性差**:如果只有订单模块增长快,却无法只扩展订单数据库——因为两个服务共用同一套 DB 实例。独立数据库可以实现按模块水平扩展。
+> 3. **技术锁定**:一个服务可能需要 MySQL,另一个可能需要 MongoDB。共享数据库迫使大家使用同一种数据库类型,牺牲了 polyglot persistence 的优势。
+> 4. **团队冲突**:两个团队的部署节奏不同,但数据库 schema 变更需要协调,容易出现"我改了字段你没改"的线上事故。
+>
+> **这就是"分布式单体":** 表面上有多组服务,但它们共享同一份数据层,任意修改都需要跨团队协作——本质上和单体的紧耦合一样,只不过加了网络和调用的复杂度。
+
+---
+
+### Q25.【重试风暴分析】
+
+一个电商系统中有三个服务链:API Gateway → Order Service → Payment Service。当 Payment Service 出现短暂不可用时,Order Service 启动了 3 次重试,Gateway 也对 Order Service 启动了 3 次重试。请问这可能引发什么问题?如何设计才能避免?
+
+> [!tip]- Q25 参考答案
+>
+> **问题:** 重试风暴(Retry Storm)。Gateway 的重试 × Order Service 的重试会产生组合放大效应。假设 1000 个并发请求,Gateway 重试 3 次,Order 又重试 3 次——相当于对 Payment Service 产生了 $1000 \times 3 \times 3 = 9000$ 次请求!这会把原本只是短暂抖动的服务彻底压垮,延长恢复时间。
+>
+> **避免策略:**
+> 1. **Jitter**:重试时加随机延迟,避免所有请求在同一时刻重试
+> 2. **只对瞬态错误重试**:4xx 和业务错误不应该重试,只重试 5xx、超时、连接重置
+> 3. **熔断器**:当 Payment 失败率过高时,熔断器直接短路,不发起重试
+> 4. **重试预算递减**:Gateway 不重试或只重试 1 次,由 Order Service 决定重试策略
+> 5. **指数退避**:重试间隔逐次翻倍,给下游更多恢复时间
+>
+> 💡 **核心认知:** 重试的目的是应对**偶发瞬态故障**,不是应对**系统性故障**。系统性故障需要熔断和降级,重试只会雪上加霜。
+
+---
+
+### Q26.【服务雪崩防控方案】
+
+一个秒杀系统中,用户请求到达 API Gateway 后,依次经过商品服务 → 库存服务 → 支付服务。如果库存服务的 DB 连接池被打满(所有连接被慢查询占满),请求会从库存服务蔓延到商品服务和 Gateway,最终导致整个系统不可用。请设计一套完整的雪崩防控方案。
+
+> [!tip]- Q26 参考答案
+>
+> **多层次防护:**
+>
+> 1. **超时控制**:每个 RPC 调用设置合理的超时时间,逐层递减。库存服务超时设为 500ms,商品服务调用库存的超时设为 300ms。
+> 2. **熔断器**:库存服务的调用方设置熔断器,失败率超过阈值(如 50%)时快速失败,不调用下游。
+> 3. **舱壁隔离**:商品服务对不同下游服务使用独立的线程池/连接池。库存服务的问题不会占满商品服务所有的连接。
+> 4. **限流**:网关层对秒杀接口限流,只放行合理数量的请求。例如每秒限 1000 个请求。
+> 5. **降级策略**:库存不可用时返回"暂时无法确认库存,请稍后再试",而不是返回 500 错误。对于非核心功能直接跳过。
+> 6. **快速失败**:在入口处就拒绝超额请求,不让无效请求进入下游链路。
+>
+> ```mermaid
+> graph TB
+> U["用户请求"] --> GW["L1: 网关限流 1000/s"]
+> GW --> CS["商品服务"]
+> CS --> IS["库存服务"]
+> IS --> DB[(DB)]
+> CS -.熔断.-> CB1["熔断器:失败率高 → 快速失败"]
+> IS -.熔断.-> CB2["熔断器:DB 打满 → 短路"]
+> GW -.降级.-> DG["降级:返回兜底页"]
+> style U fill:#cfe2f3
+> style GW fill:#fff3e0
+> style CS fill:#e8f5e9
+> style IS fill:#ffebee
+> ```
+>
+> 💡 **防御哲学:** 不要指望单个防护措施能挡住所有问题,要靠**纵深防御**——每一层都减轻上一层的压力,形成瀑布式的缓冲效果。
+
+---
+
+### Q27.【可观测性落地优先级】
+
+假设你刚接手一个全新的微服务项目(5 个服务,团队 3 人),需要在 3 个月内建立可观测性体系。你觉得应该按照什么优先级推进 Metrics → Logging → Tracing?为什么?
+
+> [!tip]- Q27 参考答案
+>
+> **推荐顺序:Metrics → Logging → Tracing**
+>
+> **理由:**
+>
+> 1. **先 Metrics(第 1 个月)**:最快见效,投入最小。只要接入 Prometheus + Grafana,配置 RED 方法的 exporter,就能立刻看到服务的 QPS、错误率和延迟趋势。有了 baseline 才知道什么是"正常",告警才有意义。
+> 2. **再 Logging(第 2 个月)**:Metrics 告诉你"出事了",Logging 告诉你"发生了什么"。集中收集 JSON 结构化日志(ELK 或 Loki),加上 trace_id 关联,就能在不装链路追踪的情况下定位大部分问题。
+> 3. **最后 Tracing(第 3 个月)**:链路追踪最有价值但在初期成本最高——需要 SDK 集成、上下文传播、采样策略配置、可视化平台搭建。当服务数量增多、问题开始在服务间流转难以定位时,Tracing 的价值才真正体现出来。
+>
+> ```mermaid
+> graph LR
+> Base["Metrics\n⏱ 最快上线 · 看趋势"] --> Struct["Logging\n📝 结构化 · 看细节"]
+> Struct --> Trace["Tracing\n🔗 看链路 · 最贵但最深"]
+> style Base fill:#e8f5e9
+> style Struct fill:#fff3e0
+> style Trace fill:#fce4ec
+> ```
+>
+> **底线:** 每个阶段都要保持一致性——日志里必须有 trace_id,Metrics 要有足够的维度(service/method/status),这样三个阶段的数据才能真正串联起来。
+>
+> 💡 **快速起步建议:** 先用 OpenTelemetry auto-instrumentation(如 Java Agent 或 Go OTel contrib)自动采集 Metrics 和 Spans,然后再补充自定义的业务埋点和日志结构化。
+
+---
+
+## 评分参考
+
+| 题目类型 | 满分 | 权重 |
+|---------|------|------|
+| 选择题(Q1-Q10) | 45 分 | 45% |
+| 填空题(Q11-Q18) | 24 分 | 24% |
+| 代码补全(Q19-Q23) | 30 分 | 30% |
+| 主观题(Q24-Q27) | 16 分 | 16% |
+| **总计** | **100 分** | **100%** |
+
+**及格线:** ≥70 分
+**优秀线:** ≥85 分