可观测性 — 小白讲解 + 面试题精解
第八阶段:可观测性(线上排障能力)— 讲解与面试题
本文档定位: 对应《简历技术栈学习指南》第八阶段。你简历中写的是“搭建SkyWalking全链路追踪 + Prometheus/Grafana监控告警体系,线上故障定位耗时从小时级降至分钟级,系统可用性提升至99.9%”,技能清单还有“Arthas”。这是高级工程师和新手的分水岭——面试官会重点考察“线上出问题你怎么查”的实战能力。
使用方法: 每个知识点按
小白讲解 → 名词详解(举例) → 结合简历案例 → 代码/配置示例 → 面试题组织。⭐ 越多越高频。
目录
- 第一章:可观测性三大支柱(总纲)
- 第二章:SkyWalking 全链路追踪
- 第三章:Prometheus + Grafana 监控告警
- 第四章:日志体系与 ELK
- 第五章:Arthas 线上诊断
- 第六章:故障排查实战剧本(面试杀手锏)
- 第七章:面试速查表
第一章:可观测性三大支柱(总纲)
1.1 什么是可观测性(Observability)?
小白讲解
一句话:系统出问题时,你能从外部“看见”它内部正在发生什么,并且能快速定位到根因。
传统监控(Monitoring)只能回答“你知道要问的问题”——比如“CPU 高不高、服务挂没挂”。可观测性更进一步,能回答你没预料到的问题——比如“为什么这个用户的请求失败了”。
名词详解:三大支柱
| 支柱 | 英文 | 解决什么问题 | 举例(以你的 RAG 系统为例) |
|---|---|---|---|
| 指标(Metrics) | 数值时间序列 | “有没有问题”——趋势、告警 | /chat 接口最近 5 分钟 P99 延迟涨到 8 秒了 |
| 日志(Logging) | 离散文本事件 | “具体发生什么”——细节还原 | 某条 ERROR 日志:向量化调用 Qwen API 超时 timeout after 3000ms |
| 追踪(Tracing) | 请求级调用链 | “问题在哪一环”——跨服务定位 | 这次请求:Gateway(2ms) → chat-service(5ms) → 向量检索(120ms) → LLM调用(7800ms) ← 瓶颈在这 |
三者配合的排查路径(必背):
告警触发(Metrics:P99 延迟超标)
↓ 拿到时间点 + TraceId(从日志/告警标签)
打开 Trace 链路(Tracing:发现慢在 LLM 调用环节)
↓ 看该 Span 关联日志
看日志细节(Logging:Qwen API 超时,重试 2 次都失败)
↓ 根因
第三方 API 限流 → 加熔断降级(Sentinel)+ 缓存高频答案
你简历中的案例话术
“我们能源平台有 12 个微服务,之前排查一个’数据大屏打开慢’的问题,要挨个登服务器 grep 日志,一个问题排查一两个小时。我搭了 SkyWalking + Prometheus/Grafana 之后:告警看大盘定位异常时间和接口 → TraceId 拉出全链路 → 直接看到慢在哪个服务的哪个方法。平均故障定位时间从小时级降到分钟级,年终复盘里系统可用性从 99.5% 提升到 99.9%。”
99.9% 是什么概念(面试官可能追问): 一年 8760 小时 × 0.1% ≈ 全年停机不能超过 8.76 小时。99.99%(四个九)≈ 52 分钟/年。数字越多 9,代价指数级上升。
第二章:SkyWalking 全链路追踪
2.1 全链路追踪的核心名词
小白讲解
一次前端请求经过 12 个微服务,就像快递经过 12 个中转站。全链路追踪就是给每个包裹(请求)发一个运单号(TraceId),每个中转站的分拣动作都记录时间——最后你能看到整条路线哪段耗时最长。
名词详解(APM 领域必懂词汇)
| 名词 | 解释 | 举例 |
|---|---|---|
| Trace(链路) | 一次完整请求的所有 Span 集合,用全局唯一 TraceId 标识 | 一次“上传文档并解析” = 1 个 Trace |
| Span(跨度) | 链路中的一个工作单元,含开始/结束时间、标签、日志 | DocumentService.parse() 是一个 Span,pgvector 相似度检索是子 Span |
| TraceContext(上下文) | 跨进程传递 TraceId 的载体 | HTTP Header sw8,跨服务自动透传 |
| OpenTracing / OpenTelemetry | 链路追踪的行业标准规范(API + 数据格式) | SkyWalking、Zipkin、Jaeger 都兼容,避免厂商锁定 |
| 采样率(Sampling) | 全量追踪开销大,按比例采集 | 生产设 agent.sample_n_per_3_secs=10,每 3 秒采 10 条;慢请求/错误请求 100% 采集 |
| 字节码增强(Bytecode Instrumentation) | 不改业务代码,JVM 类加载时动态注入探针逻辑 | java -javaagent:skywalking-agent.jar 启动即可埋点,业务零侵入 |
| OAP Server | SkyWalking 的后端分析集群(Observability Analysis Platform) | 接收 Agent 上报数据,聚合分析后存入 ES |
| 拓扑图(Topology) | 服务依赖关系的自动生成图 | 一眼看到 gateway → instruction-service → kafka → hbase |
Trace 数据结构举例
TraceId: a12bf3c8d9e0f1a2
├─ Span1: Gateway.filter() [0ms ────────── 4210ms] gateway服务
│ tags: http.url=/instruction/create, http.status=200
│
├─ Span2: InstructionController.create() [5ms ──── 4200ms] instruction服务
│ │
│ ├── Span3: InstructionService.save() [10ms ── 300ms]
│ │ tags: db.statement=INSERT INTO t_instruction...
│ │
│ ├── Span4: KafkaTemplate.send() [305ms ─ 315ms]
│ │ tags: mq.topic=instruction-status
│ │
│ └── Span5: HBaseClient.put() [320ms ─ 4190ms] ★ 最慢的 Span!
│ logs: exception=RpcTimeoutException
→ 一眼定位:HBase 写入超时是这次请求慢的根因。这就是“小时级→分钟级”的具体含义。
2.2 SkyWalking 架构与原理(面试必问原理)
架构图
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Java 服务 │ │ Java 服务 │ │ 前端/浏览器 │
│ ┌─────────┐ │ │ ┌─────────┐ │ └─────────────┘
│ │业务代码 │ │ │ │业务代码 │ │ ↑
│ └────┬────┘ │ │ └────┬────┘ │ │
│ ┌────▼────┐ │ │ ┌────▼────┐ │ ┌─────▼──────┐
│ │ SkyWalking│ │ │ │ SkyWalking│ │ UI 界面 │ 查询/拓扑/Trace
│ │ Agent │ │ │ │ Agent │ └─────▲──────┘
│ └────┬────┘ │ │ └────┬────┘ │ │
└──────┼──────┘ └──────┼──────┘ ┌─────┴──────┐
│ gRPC 上报 │ │ OAP Server │ 聚合分析
└────────┬─────────┘ └─────▲──────┘
└─────────────────────────┘
┌───────┴──────┐
│Elasticsearch │ 存储(也支持 H2/B MySQL)
└──────────────┘
字节码增强原理(背下这段话)
面试话术:
“SkyWalking 用的是 Java Agent + 字节码增强(基于 ByteBuddy 库)。JVM 类加载器加载类的时候,Agent 会拦截类加载事件,找到被插件声明的拦截点(比如 Spring 的
Controller方法、MyBatis 的Executor、Kafka 的Producer),在方法前后动态插入 Span 的创建和结束逻辑。所以业务代码零侵入,只需要启动命令加一个-javaagent参数。原理上和 Spring AOP 的动态代理不同——代理是运行时换对象,字节码增强是加载时改类。”
TraceId 跨服务怎么传?(高频追问)
服务A 调用 服务B:
服务A Agent 在发 HTTP 请求时,在 Header 塞入 sw8: 1-a12bf3c8...-spanId-parentService
↓ (HTTP Header 随请求传递)
服务B Agent 收到请求,解析 sw8 Header
↓ 继承同一个 TraceId,创建子 Span
↓ 跨线程:ThreadLocal 复制到子线程(SkyWalking 处理了线程池场景)
坑点(体现深度): 线程池/异步场景(比如你的 DAG 引擎用 CompletableFuture)ThreadLocal 会丢上下文,SkyWalking 插件对常见线程池做了增强;如果用了自定义线程工厂,需要 Supplier 包装 Runnable 来手工传递 ContextSnapshot。
2.3 落地配置(结合简历:12个微服务接入)
接入方式(只需改启动命令):
# K8s Deployment 里加环境变量 + 镜像内置 agent
env:
- name: JAVA_TOOL_OPTIONS
value: >-
-javaagent:/skywalking/agent/skywalking-agent.jar
-Dskywalking.agent.service_name=instruction-service
-Dskywalking.collector.backend_service=oap.observability:11800
-Dskywalking.agent.sample_n_per_3_secs=50
自定义业务埋点(你简历写了“自定义业务指标埋点”):
// 方式一:注解埋点(最简单)
@Trace // 该方法自动成为链路中的一个 Span
public MatchResult settleMatch(String instructionId) { ... }
// 方式二:主动打 Tag(在 Span 上记录业务信息,排查时直接看得到)
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;
ActiveSpan.tag("instruction.id", instructionId); // 链路里直接搜指令号定位请求
ActiveSpan.tag("settle.type", "SECURITY"); // 标记交收类型:证券交收
面试话术(自定义埋点): “除了框架级自动埋点,我们在关键业务方法上加了 @Trace 注解和业务 Tag——比如托管指令的指令号、交收类型。这样线上出问题时,用户报一个指令号,我在 SkyWalking 里直接按 Tag 搜出整条链路,不用再从网关日志开始逐层翻。”
2.4 SkyWalking 面试题
Q1:SkyWalking 是怎么做到无侵入的?(高频 ⭐⭐⭐⭐⭐)
答:Java Agent 机制 + 字节码增强。启动参数挂载 -javaagent 后,Agent 的 premain 在类加载前生效,通过 ByteBuddy 修改目标类的字节码(如 Controller、MyBatis Executor),在方法前后插入 Span 创建/结束与上下文传播逻辑。业务代码零改动,升级探针也不用改代码。
Q2:TraceId 是怎么跨服务传递的?(高频 ⭐⭐⭐⭐⭐)
答:进程内用 ThreadLocal 存 TraceContext;跨进程时 Agent 把 TraceId、SpanId 序列化进 HTTP Header(sw8)或 MQ 消息头;下游 Agent 解析后加入同一条链路。异步线程池场景需要包装 Runnable 传递 ContextSnapshot。
Q3:全量采集 Trace 有什么问题?采样策略怎么选?(中频 ⭐⭐⭐⭐)
答:Span 数据量大(一次请求几十个 Span),全量采集会占用网络、存储(ES)和应用 CPU。策略:① 概率采样(如 10%)+ ② 错误请求和慢请求(超过阈值的 Span)强制保留 ③ 核心链路 100% 采样、边缘服务低采样。原则是“宁可少而准,不能没有慢/错样本”。
Q4:SkyWalking 数据存哪里?为什么用 ES?(中频 ⭐⭐⭐)
答:默认 ES。Trace/Span 是典型写入密集 + 按时间范围查询的数据,ES 的倒排索引 + 按天滚动索引(index rollover)+ TTL 删除(保留 7-15 天)非常契合。也支持 H2(演示)、MySQL(小规模)、BanyanDB(官方新存储)。
Q5:你们线上 P99 突然飙升,用 SkyWalking 怎么排查?(场景题 ⭐⭐⭐⭐⭐)
答:① 拓扑图看是不是某一环整体变红变慢;② 打开该服务的 Trace 列表按耗时排序,取最慢的几条 Trace;③ 对比多个慢 Trace 的公共环节(比如都卡在同一个下游);④ 进入根因 Span 看异常日志和 Tag;⑤ 关联 Metrics(该环节所在实例 CPU/GC)判断是资源问题还是代码问题。
第三章:Prometheus + Grafana 监控告警
3.1 Prometheus 核心名词详解
小白讲解
Prometheus = 一个定时抽屉:每隔 15 秒去你每个服务的 /actuator/prometheus 抽屉里抄一遍所有数字(QPS、延迟、内存、GC……),存成时序数据库;Grafana 负责把这些数字画成图;Alertmanager 负责“数字越界就打电话”。
名词详解
| 名词 | 解释 | 举例 |
|---|---|---|
| Pull 模型 | Prometheus 主动拉取(HTTP GET)目标的数据,而非等目标推送 | 配置 metrics_path=/actuator/prometheus 每 15s 抓一次 |
| Exporter | 把第三方系统的指标暴露成 Prometheus 格式的“翻译官” | node_exporter 暴露服务器 CPU/内存/磁盘;kafka_exporter 暴露消费组积压 |
| PromQL | Prometheus 的查询语言 | rate(http_server_requests_seconds_count[5m]) = 5分钟内的 QPS |
| Counter(计数器) | 只增不减的累计值 | 请求总数、错误总数、GC 次数 |
| Gauge(仪表盘) | 可增可减的瞬时值 | 当前堆内存、活跃线程数、Kafka 积压量 |
| Histogram(直方图) | 按桶统计分布 → 算 P95/P99 | http_server_requests_seconds_bucket{le="1.0"} 1秒内完成的请求数 |
| Summary | 客户端直接算好分位数(对比 Histogram) | P99 由应用自己算好上报 |
| 服务发现(SD) | 自动发现抓取目标,不用手写 IP 列表 | kubernetes_sd_configs 自动发现带注解的 Pod |
| Alertmanager | 告警路由/静默/抑制/分组组件 | 告警发钉钉/企业微信;“节点down”时抑制其上所有服务告警 |
Counter vs Gauge vs Histogram(举例讲透)
Counter:http_requests_total = 102345(从启动开始累计,只增)
→ 求增速必须用 rate():
rate(http_requests_total[5m]) = 最近5分钟平均QPS
Gauge:jvm_memory_used_bytes = 536870912(当前值,可上可下)
→ 直接画图就是内存曲线
Histogram:http_server_requests_seconds_bucket{le="0.1"} = 950
http_server_requests_seconds_bucket{le="1.0"} = 990
http_server_requests_seconds_bucket{le="+Inf"} = 1000
→ P99 = 99% 的请求落在这个 le 桶以内:
histogram_quantile(0.99, rate(http_server_requests_seconds_bucket[5m]))
为什么 P99 要用 Histogram 而不是平均值: 平均值会骗人——100 个请求里 99 个 10ms、1 个 10s,平均才 109ms “看起来很好”,但那 1 个用户已经骂人了。长尾才是体验杀手,所以 SLO 都定 P99。
3.2 Spring Boot 接入(Micrometer 名词)
Micrometer = 监控界的 SLF4J(门面)。你的代码只写 Micrometer API,底层可以换 Prometheus / Datadog / Influxdb。Spring Boot Actuator 内置了它。
# application.yml — 暴露指标端点
management:
endpoints:
web:
exposure:
include: health,prometheus
metrics:
tags:
application: instruction-service # 所有指标自动带应用名标签
自定义业务指标(简历写了“自定义业务指标”):
@Component
@RequiredArgsConstructor
public class InstructionMetrics {
private final MeterRegistry registry;
// Gauge:当前待复核指令数(仪表盘实时值)
public InstructionMetrics(MeterRegistry registry) {
this.registry = registry;
registry.gauge("instruction_pending_review", new AtomicLong(0));
}
// Counter:指令状态流转计数(按状态打标签 → 每个状态一条曲线)
public void recordTransition(String from, String to) {
registry.counter("instruction_transition_total",
"from", from, "to", to).increment();
}
// Timer:交收匹配耗时分布(自动算 P95/P99)
public void recordMatchDuration() {
Timer.Sample sample = Timer.start(registry);
// ... 业务逻辑
sample.stop(registry.timer("instruction_match_duration"));
}
}
面试话术: “除了 JVM/HTTP 等技术指标,我给核心业务加了自定义指标——比如托管指令各状态的流转速率 Counter、待复核积压量 Gauge。上线后 Grafana 上能直接看到’复核环节积压了 3000 条指令’这种业务级异常,而不是等用户投诉才知道。”
3.3 常用 PromQL(必背 6 条)
# 1. QPS(按接口分组)
sum(rate(http_server_requests_seconds_count[5m])) by (uri)
# 2. P99 延迟
histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri))
# 3. 错误率(5xx 占比)
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count[5m]))
# 4. JVM 堆内存使用率
jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}
# 5. Full GC 频率(G1)
rate(jvm_gc_pause_seconds_count{action="end of major GC"}[10m])
# 6. Kafka 消费组积压(Lag)
kafka_consumergroup_lag{consumergroup="instruction-group"}
3.4 告警规则 + Alertmanager(结合简历“告警体系”)
# Prometheus 告警规则示例(贴合你的项目场景)
groups:
- name: instruction-service-alerts
rules:
# P99 延迟超 2 秒持续 5 分钟
- alert: HighLatencyP99
expr: histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le)) > 2
for: 5m # 持续5分钟才告警(过滤毛刺)
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} P99 超过 2s(当前 {{ $value }}s)"
# 错误率超 5%
- alert: HighErrorRate
expr: |
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count[5m])) > 0.05
for: 3m
labels:
severity: critical
# 服务实例挂了
- alert: ServiceDown
expr: up{job="instruction-service"} == 0
for: 1m
labels:
severity: critical
Alertmanager 三大降噪能力(防告警风暴,加分项):
| 能力 | 解释 | 举例 |
|---|---|---|
| 分组(Grouping) | 相似告警合并成一条通知 | 12 个服务同时抖动 → 合并成 1 条钉钉消息,而不是 12 条轰炸 |
| 抑制(Inhibition) | 高级告警触发时压制低级告警 | “机房网络断”触发时,自动抑制所有“服务不可达”告警 |
| 静默(Silence) | 维护窗口内不告警 | 发布窗口 22:00-23:00 设置静默 |
3.5 Grafana 名词(了解即可)
- DataSource:接 Prometheus(数据从哪来)
- Dashboard / Panel:面板;Panel 类型有 Time series(曲线)、Stat(大数字)、Table、Heatmap(热点图,看延迟分布)
- 变量(Variable):下拉框按 service/instance 切换,一套面板看所有服务
3.6 Prometheus 面试题
Q1:为什么用 Pull 不用 Push?(高频 ⭐⭐⭐⭐)
答:① Prometheus 主动拉,目标只暴露 /metrics 端口,实现极简、无状态;② 拉取失败即健康检查(up==0 直接告警);③ 易于水平扩容和调试(curl 就能看数据);④ 天然防止应用侧阻塞(推模式在监控端故障时会拖垮应用)。短生命周期任务例外,用 Pushgateway 中转。
Q2:Prometheus 数据存多久?怎么做长期存储?(中频 ⭐⭐⭐)
答:本地 TSDB 一般保留 15-30 天(时序数据自带时间戳+标签压缩,压缩率高)。长期存储用 Thanos / VictoriaMetrics 远端存储,或者按公司要求归档到对象存储。生产 Prometheus 建议双副本 + Alertmanager 集群防单点。
Q3:指标四类型区别?(高频 ⭐⭐⭐⭐)
答:Counter 只增(累计请求量,需 rate 求速率);Gauge 瞬时可增减(内存、积压);Histogram 按桶采样分布(可聚合算 P99,推荐);Summary 客户端预算分位数(无法跨实例聚合,一般不用)。
Q4:你们的告警怎么分级?怎么防止告警疲劳?(结合简历 ⭐⭐⭐⭐)
答:三级——P0 critical(服务不可用/错误率>5%)打电话+钉钉;P1 warning(P99 超阈值/积压超限)钉钉群;P2 info 只进日报。降噪靠:告警必须持续 for N 分钟、Alertmanager 分组、根因抑制(节点挂了不重复报上面所有服务)、发布窗口静默。
第四章:日志体系与 ELK
4.1 名词详解
| 名词 | 解释 | 举例 |
|---|---|---|
| 结构化日志 | JSON 格式而非纯文本,机器可解析 | {"ts":"...","level":"ERROR","traceId":"a12bf","msg":"匹配失败","instructionId":"I20260820001"} |
| MDC(Mapped Diagnostic Context) | Logback 的 ThreadLocal 上下文,自动把 TraceId 打进每行日志 | 每行日志自动带 traceId → 日志和链路联动 |
| ELK / EFK | Elasticsearch(存储检索)+ Logstash/Filebeat(采集)+ Kibana(查询界面) | 容器场景常用 Filebeat(轻量)或直接日志输出到 Kafka 再进 ES |
结合简历案例(日志规范)
面试话术:
“我定了团队日志规范:JSON 结构化输出,必须带 traceId(从 SkyWalking TraceContext 取)和业务主键(指令号)。Pod 日志打到 stdout 被统一采集进 ES。这样一次故障排查 = 告警定位时间 → SkyWalking 看链路 → 拿 TraceId 在 Kibana 一搜,三个系统一个 ID 串起来。”
// logback 里自动注入 traceId(每行日志都带)
<pattern>%d{HH:mm:ss} %-5level [%X{tid}] [%X{instructionId}] %logger{36} - %msg%n</pattern>
// 代码里放业务主键进 MDC
MDC.put("instructionId", instructionId);
try {
// 业务逻辑
} finally {
MDC.remove("instructionId"); // 线程复用,必须清理防串号
}
第五章:Arthas 线上诊断
5.1 Arthas 是什么?
小白讲解
线上 JVM 出问题(CPU 飙高、内存泄漏、方法莫名变慢),但没有接入监控、不能重启、不能加日志重新发版——这时候把 Arthas 挂到正在运行的 Java 进程上(attach 机制),像“听诊器”一样实时观察:哪个线程吃 CPU、某个方法参数返回值是什么、哪里对象暴涨。
核心机制: JVM 的 Attach API——Arthas 进程通过它把自己注入目标 JVM,加载自己的 Agent,从而能反编译类、插桩观察方法,全程不需要重启应用。
5.2 必会命令(结合场景记)
| 命令 | 作用 | 你项目里的使用场景 |
|---|---|---|
dashboard |
总览:线程/内存/GC | 第一眼全局判断:是不是 GC 频繁 |
thread -n 3 |
最忙的 3 个线程 + 栈 | CPU 飙高 → 看是不是正则回溯/死循环 |
thread --state BLOCKED |
找死锁/锁竞争 | 交收匹配批量处理卡住 → 定位锁竞争 |
jad 类名 |
反编译运行中的类 | 确认线上跑的代码版本对不对(发版发了没?热修生效没?) |
sc -d 类名 |
查类从哪个 jar 加载 | 排查 jar 包冲突/类加载了旧版本 |
watch 类 方法 '{params,returnObj}' -x 2 |
观察方法入参/返回值 | 某些指令匹配结果为空 → 看入参传的什么 |
watch ... '#cost>200' |
只看耗时超 200ms 的调用 | 抓慢调用 |
trace 类 方法 |
方法内部调用链各级耗时 | settleMatch 慢 → 看是 SQL 慢还是 HBase 慢 |
tt -t 类 方法 |
TimeTunnel,记录每次调用可回放 | 偶现 Bug,回放当时的调用现场 |
profiler start / profiler stop |
生成 CPU 火焰图 | 性能瓶颈直观定位 |
heapdump |
导出堆快照 | OOM 排查(配合 MAT 分析大对象) |
ognl |
直接执行表达式 | 动态改 logger 级别:logger --name com.xxx --level DEBUG |
场景剧本一:CPU 100% 排查(必背)
# 1. 看全局
arthas > dashboard
# 发现某线程 CPU 90%+
# 2. 找到最忙线程及其栈
arthas > thread -n 1
# 栈显示卡在 XxxRegexMatcher.match() —— 正则回溯灾难
# 3. trace 确认耗时分布
arthas > trace com.xxx.service.SettleService matchRule '#cost > 100'
# 100%+ 时间在正则匹配
# 4. 根因:用户输入触发了灾难性回溯的正则 → 改写正则/预编译+加超时
场景剧本二:接口偶现变慢(tt 回放)
# 记录该方法最近 100 次调用
arthas > tt -t com.xxx.SettleService settleMatch -n 100
# 发现某几次 cost 超过 3s,入参里 amountScale=8(小数位特别多)
# 回放那次慢调用
arthas > tt -i 1001
# 根因:BigDecimal 高精度 scale 计算慢 → 优化为统一 scale 预处理
5.3 Arthas 面试题
Q1:线上 CPU 飙高怎么排查?(超高频 ⭐⭐⭐⭐⭐)
答:两条路都要会——① 传统三板斧:top 找进程 → top -Hp <pid> 找线程 → printf '%x' 线程号转十六进制 → jstack <pid> | grep -A 20 <hex> 看栈;② Arthas 一条龙:dashboard 看全局 → thread -n 3 直接给最忙线程和栈 → trace 定位到具体方法。看到栈后再分情况:死循环/正则回溯(改代码)、频繁 GC(dashboard 看 GC 曲线,转内存排查)、锁竞争(thread --state BLOCKED)。
Q2:线上内存泄漏/OOM 怎么排查?(超高频 ⭐⭐⭐⭐⭐)
答:① jmap -histo <pid> 快速看对象排行(或 Arthas dashboard 看堆趋势);② 疑似泄漏 → heapdump(或 jmap -dump:format=b,file=heap.hprof)导出快照;③ 用 MAT(Memory Analyzer)看支配树/Dominator Tree,找 GC Root 引用链,定位是谁持有大对象;④ 常见根因:静态 Map 只进不出(本地缓存无淘汰)、ThreadLocal 未 remove、监听器/连接未关闭、大查询一次拉全表。
Q3:Arthas 是怎么做到不重启就能观察运行中方法的?(中频 ⭐⭐⭐⭐)
答:基于 JVM Attach API。Arthas 客户端通过 Attach 连接目标 JVM 加载 Arthas Agent,Agent 用 Instrumentation API + 字节码增强(ASM)在目标方法前后插入观察逻辑,watch/trace 的数据就是插桩后采集的。jad 反编译的是方法区/元空间里的运行时字节码,所以能确认“线上真实运行的代码”。
第六章:故障排查实战剧本(面试杀手锏)
面试官问“讲一个你排查过的线上故障”,用 STAR 结构讲下面的剧本(基于你简历项目改编,务必练熟一个完整故事)。
剧本:日终清算窗口拖长(项目一真实场景改编)
S(背景): 资产托管系统日终清算有时间窗口约束,某天清算任务耗时从 3 分钟涨到 40 分钟,影响次日开盘前的对账。
T(任务): 我负责定位并恢复性能。
A(排查动作,层层递进):
- 看大盘(Prometheus/Grafana):清算时段 instruction-service CPU 90%、老年代锯齿异常密集 → 不是网络/下游问题,是本服务自身
- 看链路(SkyWalking):对清算 Trace 按耗时排序,慢 Trace 全部卡在同一个 Span:
HBase 批量写入 - 看细节(Arthas):
trace到 HBase Put 构造环节,发现 RowKey 生成逻辑里String.format在千万级循环里反复调用;再看dashboard的 GC 压力来自临时对象暴涨 - 根因确认:当天的数据量翻倍(新接入一类产品),放大了两个问题——热点 RowKey 前缀冲突(Region 热点)+ 循环内对象创建过多导致 Young GC 风暴
R(结果): RowKey 加盐(业务日期+指令ID 前缀打散,正好呼应你简历写的“RowKey前缀策略”)、批量写入改为攒批 flush、String.format 换 StringBuilder。清算耗时回落到 3 分钟,并给清算任务耗时加了 Prometheus 告警(超 10 分钟预警),同类问题以后能在恶化前发现。
点题收尾(必说): “这件事也推动我们把’指标-链路-日志’三件套在故障复盘里串成了标准动作,是故障定位从小时级降到分钟级的典型例子。”
第七章:面试速查表
可观测性总纲
| 考点 | 一句话答案 |
|---|---|
| 三大支柱 | Metrics(有没有问题)+ Logging(具体发生什么)+ Tracing(问题在哪一环) |
| 排查路径 | 告警定位异常 → Trace 看链路瓶颈 → 日志看根因细节 |
| 99.9% 可用性 | 全年停机 ≤ 8.76 小时 |
SkyWalking
| 考点 | 一句话答案 |
|---|---|
| 无侵入原理 | -javaagent + ByteBuddy 字节码增强,类加载时在方法前后插桩 |
| TraceId 跨服务 | 进程内 ThreadLocal,跨进程走 HTTP Header(sw8)/ MQ 消息头 |
| Trace/Span | Trace=一次请求全链路;Span=链路中一个工作单元(含耗时/Tag/日志) |
| 采样率 | 概率采样+错误/慢请求强制保留;防开销过大 |
| 架构 | Agent 上报 → OAP 聚合分析 → ES 存储 → UI 展示 |
| 异步丢上下文 | 线程池 ThreadLocal 不传递,需包装 Runnable 传 ContextSnapshot |
Prometheus + Grafana
| 考点 | 一句话答案 |
|---|---|
| Pull 模型 | Prometheus 主动抓 /metrics;抓不到即 up=0 告警;应用无阻塞风险 |
| 四类指标 | Counter 只增需 rate / Gauge 瞬时 / Histogram 分桶算 P99 / Summary 预算分位不可聚合 |
| P99 为什么 | 平均值掩盖长尾,SLO 看分位数 |
| Micrometer | 监控门面(类 SLF4J),Actuator 内置,暴露 /actuator/prometheus |
| 自定义指标 | Counter 业务流转计数 / Gauge 积压量 / Timer 耗时分布 |
| 告警降噪 | for 持续时间过滤毛刺 + Alertmanager 分组/抑制/静默 |
| 长期存储 | 本地 TSDB 15-30 天,长期用 Thanos/VictoriaMetrics |
Arthas
| 考点 | 一句话答案 |
|---|---|
| 原理 | Attach API 注入 Agent + Instrumentation 字节码增强,无需重启 |
| CPU 飙高 | thread -n 3 看栈 → trace 定位方法(或 top+jstack 传统三板斧) |
| 偶现问题 | tt -t 录制调用现场,tt -i 回放 |
| 确认线上代码 | jad 反编译运行时字节码 / sc -d 看类从哪个 jar 加载 |
| OOM 排查 | dashboard 看趋势 → heapdump → MAT 支配树找 GC Root 引用链 |
| 常见内存泄漏 | 静态集合无淘汰 / ThreadLocal 未 remove / 连接未关闭 / 大查询 |
本文档对应《简历技术栈学习指南》第八阶段,最后更新:2026-08-20