This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hzh/TEST/MicroService.md
T
2026-05-06 15:03:31 +08:00

1179 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<br/>HTTPS/JWT/限流/路由"]
> GW --> Svc["业务服务<br/>订单/支付/邮件..."]
> 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 的四种基础指标类型与其用途连线:
| 选项 | 指标类型 | 用途描述 |
| ----- | --------- | -------------------------------- |
| **A** | Counter | 单调递增,只能增加或重置为零(如总请求数) |
| **B** | Summary | 采样+分桶,客户端计算百分位(SDK 内部聚合) |
| **C** | Histogram | 采样+分桶,服务端用 PromQL 计算百分位(如请求耗时分布) |
| **D** | Gauge | 可上可下,表示瞬时值(如内存使用量、在线人数) |
> [!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 分