可观测性 — 小白讲解 + 面试题精解

第八阶段:可观测性(线上排障能力)— 讲解与面试题

本文档定位: 对应《简历技术栈学习指南》第八阶段。你简历中写的是“搭建SkyWalking全链路追踪 + Prometheus/Grafana监控告警体系,线上故障定位耗时从小时级降至分钟级,系统可用性提升至99.9%”,技能清单还有“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(排查动作,层层递进):

  1. 看大盘(Prometheus/Grafana):清算时段 instruction-service CPU 90%、老年代锯齿异常密集 → 不是网络/下游问题,是本服务自身
  2. 看链路(SkyWalking):对清算 Trace 按耗时排序,慢 Trace 全部卡在同一个 Span:HBase 批量写入
  3. 看细节(Arthas)trace 到 HBase Put 构造环节,发现 RowKey 生成逻辑里 String.format 在千万级循环里反复调用;再看 dashboard 的 GC 压力来自临时对象暴涨
  4. 根因确认:当天的数据量翻倍(新接入一类产品),放大了两个问题——热点 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