第一章:云计算Go真题故障场景全景概览
云计算环境中的Go应用故障具有典型的分布式、异步与资源敏感特征。常见故障并非孤立发生,而是由底层基础设施(如容器运行时、网络插件、Kubernetes调度器)、中间件服务(etcd、CoreDNS、Ingress Controller)以及Go应用自身设计(goroutine泄漏、context未取消、sync.Pool误用)共同交织引发。
典型故障维度分类
- 资源类故障:CPU节流导致HTTP超时、内存OOM触发Pod驱逐、磁盘IO饱和使etcd写入延迟激增
- 网络类故障:Service DNS解析失败、CNI插件异常导致跨节点Pod通信中断、TLS握手超时因证书过期或SNI配置错误
- 并发与状态类故障:未设置
context.WithTimeout的HTTP客户端请求永久挂起、sync.RWMutex误用引发读写死锁、time.Ticker未停止导致goroutine泄漏
关键诊断信号示例
当观察到以下现象时,应立即排查对应模块:
| 现象 | 可能根因 | 快速验证命令 |
|---|---|---|
kubectl get pods 显示 ContainerCreating 卡住 |
CNI插件未就绪或镜像拉取失败 | kubectl -n kube-system logs -l k8s-app=calico-node |
Go服务HTTP响应延迟突增且/debug/pprof/goroutine?debug=2显示数千阻塞goroutine |
context未传播或channel阻塞 | curl -s http://localhost:6060/debug/pprof/goroutine?debug=2 \| grep -c "select" |
实战定位片段:检测goroutine泄漏
# 在容器内执行,捕获当前活跃goroutine快照
curl -s http://localhost:6060/debug/pprof/goroutine?debug=2 | \
awk '/goroutine.*running/ {count++} END {print "Running goroutines:", count+0}'
# 输出示例:Running goroutines: 1247 → 显著高于正常基线(通常<50)
该命令通过pprof接口提取运行中goroutine数量,结合历史监控基线判断是否存在泄漏。若数值持续增长,需进一步分析堆栈:curl -s http://localhost:6060/debug/pprof/goroutine?debug=2 | head -n 100 定位高频阻塞模式。
第二章:表锁问题深度剖析与实战定位
2.1 Go语言中sync.Mutex与RWMutex的底层实现与竞态本质
数据同步机制
Go 的 sync.Mutex 基于原子操作(atomic.CompareAndSwapInt32)与操作系统信号量(futex)协同实现:当 Lock() 失败时,goroutine 进入休眠态;Unlock() 则唤醒等待队列首节点。
// runtime/sema.go 中简化逻辑示意
func semacquire1(addr *uint32, lifo bool) {
for {
if atomic.CompareAndSwapUint32(addr, 0, 1) {
return // 成功获取锁
}
// 否则 park 当前 goroutine
goparkunlock(...)
}
}
该循环尝试原子抢占锁位;addr 指向 mutex.state 字段,值为 0 表示空闲,1 表示已锁定。
读写锁的分层设计
RWMutex 将读/写操作解耦:
- 写锁独占,阻塞所有读写
- 读锁共享,但需规避“写饥饿”——通过
writerSem优先唤醒写者
| 特性 | Mutex | RWMutex |
|---|---|---|
| 并发读支持 | ❌ | ✅(多 reader) |
| 写优先保障 | — | ✅(writerSem 机制) |
graph TD
A[goroutine 调用 RLock] --> B{是否有活跃 writer?}
B -->|否| C[原子递增 readerCount]
B -->|是| D[阻塞于 readerSem]
C --> E[执行读操作]
2.2 数据库连接池与SQL执行链路中的隐式表锁触发点分析
在连接池复用场景下,事务未显式提交或回滚时,连接归还池中仍持有锁资源,极易引发隐式表级锁定。
常见触发路径
SELECT ... FOR UPDATE在自动提交关闭时未 commit- 连接泄漏导致事务长期挂起
- 连接池配置
removeAbandonedOnBorrow=true但未释放事务上下文
典型代码陷阱
// 错误示例:未捕获异常导致连接未提交
try (Connection conn = ds.getConnection()) {
conn.setAutoCommit(false);
PreparedStatement ps = conn.prepareStatement("UPDATE orders SET status=? WHERE id=?");
ps.setString(1, "shipped");
ps.setLong(2, 1001);
ps.executeUpdate();
// 忘记 conn.commit() → 归还连接后锁持续持有
} // conn.close() 仅归还至池,事务未结束!
该段逻辑使连接归还后事务仍活跃,InnoDB 持有行锁并可能升级为表锁(如扫描全表时);autoCommit=false 是关键开关,需严格配对 commit() 或 rollback()。
连接池行为对比
| 连接池实现 | 是否检测未完成事务 | 默认回收策略 | 锁释放保障 |
|---|---|---|---|
| HikariCP | 否(仅校验物理连接) | 不中断事务 | ❌ |
| Druid | 是(removeAbandonedTimeout) |
强制 rollback | ⚠️(依赖配置) |
graph TD
A[应用获取连接] --> B[执行DML/SELECT FOR UPDATE]
B --> C{autoCommit=false?}
C -->|是| D[事务上下文激活]
D --> E[连接归还连接池]
E --> F[锁资源未释放]
F --> G[后续SQL阻塞或锁升级]
2.3 pprof+trace联合诊断高并发下锁等待热图与goroutine阻塞栈
在高并发服务中,仅靠 go tool pprof -mutex 只能定位争用最激烈的互斥锁,却无法揭示谁在等、等多久、为何阻塞。此时需结合运行时 trace 数据,还原锁等待的时空上下文。
锁等待热图生成流程
# 启动带锁统计与trace采集的服务
GODEBUG=mutexprofile=10000000 go run -gcflags="-l" main.go &
# 采集30秒 trace + mutex profile
curl "http://localhost:6060/debug/pprof/trace?seconds=30" > trace.out
curl "http://localhost:6060/debug/pprof/mutex?debug=1" > mutex.prof
mutexprofile=10000000表示每千万次锁争用采样一次,值越小采样越密;trace?seconds=30捕获完整调度事件流,含 goroutine 阻塞/唤醒时间戳。
联合分析关键步骤
- 使用
go tool trace trace.out打开可视化界面,切换至 “Synchronization” → “Lock Profile” 查看热力分布 - 在
pprof -http=:8080 mutex.prof中点击热点锁,跳转至trace中对应 goroutine 的阻塞栈
| 工具 | 输出焦点 | 时效性 |
|---|---|---|
pprof -mutex |
锁争用频次与持有者 | 秒级聚合 |
go tool trace |
单次阻塞毫秒级时序+栈帧 | 精确到μs |
graph TD
A[HTTP请求触发高并发] --> B[goroutine尝试Acquire Mutex]
B --> C{锁已被持?}
C -->|是| D[进入waitq阻塞队列]
C -->|否| E[成功获取并执行]
D --> F[trace记录阻塞起止时间]
F --> G[pprof关联阻塞栈与锁地址]
2.4 基于go-sql-driver/mysql源码级调试:识别prepare语句导致的元数据锁(MDL)升级
调试切入点:Stmt.Exec 中的隐式 prepare 流程
当使用 db.Query("SELECT ?") 时,驱动会自动触发 prepare → execute → close 链路。关键路径位于 connection.go 的 writeCommandPacket 方法:
// mysql/connection.go 片段(v1.10.0)
func (mc *mysqlConn) writeCommandPacket(command byte) error {
if mc.stmtList != nil && command == ComStmtExecute {
// 此处未显式加锁,但 StmtExecute 协议包携带 statement ID
// 触发 MySQL Server 端 MDL 锁升级:从 MDL_SHARED_READ → MDL_SHARED_WRITE
}
return mc.writePacket(mc.packetBuf[:])
}
逻辑分析:
ComStmtExecute请求虽不修改表结构,但 MySQL 为保证 prepare/execute 语义一致性,在执行阶段对表施加MDL_SHARED_WRITE锁——阻塞 DDL 操作。
MDL 锁升级触发条件对比
| 场景 | SQL 类型 | 客户端驱动行为 | 服务端 MDL 锁类型 |
|---|---|---|---|
| 直接 Query | SELECT * FROM t |
不 prepare,单次发送 | MDL_SHARED_READ |
| 参数化 Query | SELECT ? FROM t |
自动 prepare + execute | MDL_SHARED_WRITE |
根因定位流程
graph TD
A[应用调用 db.Query] --> B[驱动检测参数化SQL]
B --> C[发送 ComPrepare]
C --> D[MySQL 返回 stmtID]
D --> E[后续 ComStmtExecute]
E --> F[Server 升级 MDL 锁]
- 复现方式:并发执行
PREPARE s1 FROM 'SELECT ?'+ALTER TABLE t ADD c INT - 关键观测点:
performance_schema.metadata_locks中LOCK_STATUS = 'PENDING'
2.5 生产环境表锁应急处置SOP:kill session、online DDL降级与读写分离兜底验证
当 information_schema.INNODB_TRX 检出长事务阻塞 DML 时,优先执行精准会话终止:
-- 查找阻塞源(运行超300秒且未提交的事务)
SELECT
CONCAT('KILL ', trx_mysql_thread_id, ';') AS kill_cmd,
trx_started, trx_state, trx_query
FROM information_schema.INNODB_TRX
WHERE trx_started < NOW() - INTERVAL 300 SECOND
AND trx_state = 'RUNNING';
逻辑分析:trx_mysql_thread_id 是唯一会话标识;INTERVAL 300 SECOND 避免误杀短时事务;trx_state = 'RUNNING' 排除已提交或回滚状态。
降级策略选择矩阵
| 场景 | Online DDL 选项 | 风险等级 | 锁粒度 |
|---|---|---|---|
| 添加非空列 | ALGORITHM=INPLACE, LOCK=NONE |
中 | 行锁 |
| 修改主键 | 强制 ALGORITHM=COPY |
高 | 表锁 |
读写分离兜底验证流程
graph TD
A[检测主库表锁] --> B{从库查询延迟 < 1s?}
B -->|是| C[切换流量至只读从库]
B -->|否| D[触发熔断告警]
C --> E[执行业务级一致性校验]
关键动作需在 90 秒内完成闭环,避免雪崩扩散。
第三章:内存泄漏的Go特有模式识别与根因追踪
3.1 Go runtime内存模型与GC标记-清除阶段的泄漏信号捕捉(heap_inuse vs heap_idle)
Go runtime 的 heap_inuse 与 heap_idle 是诊断内存泄漏的关键指标:前者表示已分配且正在使用的堆内存页,后者为已归还给OS但尚未被runtime复用的空闲页。
内存状态跃迁示意
// 获取当前堆统计(需在GC后调用以排除瞬时噪声)
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("HeapInuse: %v KB, HeapIdle: %v KB\n",
m.HeapInuse/1024, m.HeapIdle/1024)
该代码读取实时堆快照;HeapInuse 持续增长而 HeapIdle 不回升,常指向对象未被GC回收——如闭包持有长生命周期引用或 sync.Pool 误用。
关键指标对比
| 指标 | 含义 | 健康信号 |
|---|---|---|
HeapInuse |
已映射且被Go使用的内存 | GC后应显著回落 |
HeapIdle |
映射但未被使用的内存页 | 长期接近零提示释放受阻 |
GC标记-清除阶段泄漏特征
graph TD
A[GC Start] --> B[Mark Phase]
B --> C[Scan Roots & Traverse Pointers]
C --> D[Clear Phase]
D --> E[Free Unmarked Objects]
E --> F[heap_idle += freed pages]
F --> G{heap_idle stagnates?}
G -->|Yes| H[可疑:对象仍被隐式引用]
- 常见诱因:全局map未清理、goroutine泄漏导致栈帧驻留、
unsafe.Pointer阻断逃逸分析; runtime.ReadMemStats应配合debug.SetGCPercent(-1)控制GC时机以隔离观察。
3.2 goroutine泄露、time.Timer未Stop、sync.Pool误用三大高频泄漏模式实测复现
goroutine 泄露:select + channel 阻塞未退出
func leakyWorker(ch <-chan int) {
go func() {
for range ch { } // ch 永不关闭 → goroutine 永驻
}()
}
该协程在 channel 关闭前持续阻塞于 range,若 ch 无显式关闭或超时控制,将永久占用栈内存与调度器资源。
time.Timer 未 Stop 导致资源滞留
t := time.NewTimer(5 * time.Second)
<-t.C // 使用后未调用 t.Stop()
未 Stop() 的 Timer 会阻止其底层 timer heap 中的节点被回收,持续触发 GC 扫描开销,尤其在高频创建场景下显著抬升堆压力。
sync.Pool 误用:存储含 finalizer 或指针链的对象
| 误用模式 | 后果 | 推荐做法 |
|---|---|---|
存储含 runtime.SetFinalizer 的对象 |
Finalizer 延迟对象回收,Pool 持有引用导致内存滞留 | Pool 仅存 POD 类型(如 []byte、struct{}) |
| 存储含未清零指针字段的结构体 | 上次使用的指针残留引发意外引用 | Get 后强制清零关键字段 |
graph TD
A[创建 goroutine/Timer/Pool 对象] –> B{是否显式释放?}
B –>|否| C[资源持续累积]
B –>|是| D[GC 可安全回收]
3.3 使用pprof heap profile + go tool pprof -alloc_space对比分析内存增长拐点
内存采样与数据捕获
启动服务时启用持续堆采样:
GODEBUG="mmap=1" go run -gcflags="-m" main.go &
# 同时采集 alloc_space(累计分配量)和 inuse_space(当前驻留量)
curl "http://localhost:6060/debug/pprof/heap?alloc_space=1" > alloc.pprof
curl "http://localhost:6060/debug/pprof/heap?inuse_space=1" > inuse.pprof
-alloc_space=1 强制以累计分配字节数为指标,暴露高频小对象泄漏;?inuse_space=1 则反映真实内存驻留压力。
关键差异对比
| 指标 | 反映维度 | 拐点敏感性 | 典型场景 |
|---|---|---|---|
alloc_space |
分配总量趋势 | 极高 | 字符串拼接、临时切片 |
inuse_space |
实际堆占用峰值 | 中等 | 缓存未释放、goroutine 泄漏 |
分析拐点定位
go tool pprof -alloc_space -svg alloc.pprof > alloc.svg
该命令生成的 SVG 中,节点大小正比于累计分配字节数,可快速识别 bytes.Repeat 或 json.Marshal 等高频分配热点——其子树膨胀处即为内存增长拐点源头。
graph TD
A[HTTP 请求] –> B[JSON 解析]
B –> C[创建临时 []byte]
C –> D[未复用 buffer.Pool]
D –> E[alloc_space 指数上升]
第四章:云服务API超时故障的分布式链路归因与韧性加固
4.1 context.WithTimeout在HTTP/GRPC客户端中的传播失效场景与deadline穿透原理
deadline为何“消失”?
当 HTTP 客户端使用 http.Client 发起请求时,若仅对 context.WithTimeout 调用未显式传递至 http.Request,则底层 net/http 不会感知 deadline:
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
req, _ := http.NewRequest("GET", "https://api.example.com", nil)
// ❌ 缺失:req = req.WithContext(ctx) → deadline 不生效
resp, err := http.DefaultClient.Do(req) // 实际超时由 Client.Timeout 决定,非 ctx
此处
req.WithContext(ctx)被遗漏,导致context.Deadline()无法穿透至底层连接建立与读写阶段;http.Client.Timeout会覆盖 context 行为,形成“deadline 黑洞”。
GRPC 中的 deadline 穿透链路
GRPC 自动将 context.Deadline 注入 transport.Stream,但需满足:
- 上游调用必须传入带 deadline 的 context
- 所有中间 middleware(如拦截器)不得丢弃或重置 context
常见失效场景对比
| 场景 | 是否传播 deadline | 原因 |
|---|---|---|
http.Request 未调用 WithContext() |
❌ | context 未绑定到 request 实例 |
GRPC 拦截器中 ctx = context.Background() |
❌ | 显式切断 context 链 |
WithTimeout 创建后未用于后续调用 |
❌ | context 对象未被实际消费 |
graph TD
A[WithTimeout] --> B[绑定至 Request/UnaryInvoker]
B --> C[HTTP Transport / GRPC Stream]
C --> D[Deadline 穿透至 TCP 层]
D --> E[系统级 read/write timeout]
4.2 云厂商SDK内部重试机制与超时叠加效应(如AWS SDK v2 retryer timeout cascade)
超时叠加的典型场景
当客户端设置 socketTimeout=3s,而 SDK 默认 RetryableException 最多重试 3 次(含首次),每次请求携带 readTimeout=5s,实际最坏响应时间可达 3 × 5s = 15s —— 远超预期。
AWS SDK v2 RetryPolicy 示例
// 自定义重试配置:显式控制退避与总超时边界
RetryPolicy retryPolicy = RetryPolicy.builder()
.numRetries(2) // 首次 + 2次重试
.backoffStrategy(BackoffStrategy.defaultStrategy()) // 指数退避
.build();
逻辑分析:numRetries=2 表示最多发起 3 次请求;defaultStrategy 初始间隔 100ms,按 2^N 增长(100ms→200ms→400ms),但不约束单次请求超时,需配合 ClientOverrideConfiguration 单独设置 apiCallTimeout。
关键参数协同关系
| 参数名 | 作用域 | 是否参与叠加 | 示例值 |
|---|---|---|---|
apiCallTimeout |
单次请求总耗时上限 | ✅ 是 | 3s |
socketTimeout |
TCP 层读超时 | ✅ 是 | 2s |
retryTimeout |
全局重试窗口上限(v2.19+) | ✅ 是 | 10s |
超时级联路径
graph TD
A[发起请求] --> B{单次apiCallTimeout?}
B -- 超时 --> C[触发重试]
B -- 成功 --> D[返回]
C --> E{是否达最大重试次数?}
E -- 否 --> A
E -- 是 --> F[抛出SdkException]
4.3 分布式追踪中Span状态码(STATUS_CODE_UNAVAILABLE)与gRPC error code映射关系解析
STATUS_CODE_UNAVAILABLE 是 OpenTracing / OpenTelemetry 中表示“服务暂时不可用”的标准状态码,常对应 gRPC 的 UNAVAILABLE(code 14)错误。
映射原理
gRPC 客户端在连接断开、后端无健康实例或资源过载时返回 UNAVAILABLE;OpenTelemetry SDK 自动将其转换为 StatusCode.ERROR 并设置 status_code = STATUS_CODE_UNAVAILABLE。
典型场景示例
from opentelemetry.trace import Status, StatusCode
from grpc import StatusCode as GrpcStatusCode
# gRPC 拦截器中捕获错误并映射
if grpc_status == GrpcStatusCode.UNAVAILABLE:
span.set_status(Status(StatusCode.ERROR))
span.set_attribute("error.type", "UNAVAILABLE")
此代码将 gRPC 层的
UNAVAILABLE显式转为 OpenTelemetry 的STATUS_CODE_UNAVAILABLE,确保跨语言追踪链路状态语义一致。span.set_status()触发 Span 状态变更,error.type属性辅助可观测性平台分类告警。
映射对照表
| gRPC Error Code | Numeric Value | OpenTelemetry StatusCode |
|---|---|---|
| UNAVAILABLE | 14 | STATUS_CODE_UNAVAILABLE |
| DEADLINE_EXCEEDED | 4 | STATUS_CODE_DEADLINE_EXCEEDED |
状态传播逻辑
graph TD
A[gRPC Client] -->|UNAVAILABLE| B[Interceptor]
B -->|set_status ERROR + code| C[Span]
C --> D[Exporter]
D --> E[Jaeger/Zipkin UI]
4.4 基于OpenTelemetry Collector定制采样策略,精准捕获P99超时请求的完整调用链上下文
为什么默认采样不够?
默认的 probabilistic 采样(如 1%)会随机丢弃大量慢请求,导致 P99 超时链路被稀释甚至完全丢失。需转向语义化、条件驱动的采样。
自定义采样器配置示例
extensions:
memory_ballast:
size_mib: 512
receivers:
otlp:
protocols:
http:
processors:
tail_sampling:
decision_wait: 10s
num_traces: 10000
expected_new_traces_per_sec: 100
policies:
- name: p99-timeout-capture
type: latency
latency:
threshold_ms: 2000 # 捕获所有 >2s 的 span(覆盖P99典型阈值)
- name: error-fallback
type: status_code
status_code:
status_codes: ["ERROR"]
exporters:
logging:
loglevel: debug
逻辑分析:
tail_sampling在 trace 完整接收后(等待 10s)基于终端 span 的status.code和duration做最终决策;threshold_ms: 2000确保所有超时请求全量保留,避免采样偏差。
采样策略效果对比
| 策略类型 | P99 链路捕获率 | 存储开销增幅 | 实时性影响 |
|---|---|---|---|
| Probabilistic | ~32% | +5% | 无 |
| Latency-based | 100% | +28% | ≤10s 延迟 |
graph TD
A[Span 接入] --> B{Tail Sampling 缓存}
B --> C[等待 trace 完整]
C --> D[检查 duration > 2000ms?]
D -->|Yes| E[标记为采样]
D -->|No| F[检查 status == ERROR?]
F -->|Yes| E
F -->|No| G[丢弃]
第五章:调试速查表与高频真题应试锦囊
常见断点策略速查
- 条件断点:在 VS Code 中右键行号 →
Add Conditional Breakpoint,输入error?.code === 'ECONNREFUSED'快速捕获特定网络错误; - 日志断点(Logpoint):替代
console.log(),避免污染源码,例如在 Express 路由 handler 第一行设置log: 'REQ: ${req.method} ${req.url} | User: ${req.user?.id}'; - 异常断点:Chrome DevTools →
Sources→Breakpoints面板 → 勾选Caught Exceptions与Uncaught Exceptions,精准定位 Promise 拒绝未处理场景。
Node.js 进程崩溃现场还原
当生产环境出现 FATAL ERROR: Ineffective mark-compacts,立即执行以下三步:
- 启动时添加
--inspect-brk --trace-gc --trace-warnings参数; - 使用
node --diagnostic-report-unstable生成.json报告; - 分析堆快照:
chrome://inspect→Open dedicated DevTools for Node.js→Memory标签页 →Take heap snapshot→ 对比两次快照中Detached DOM tree或Closure占比突增对象。
高频真题:React 状态异步更新陷阱
某大厂笔试题要求修复如下代码导致的列表渲染错乱:
function CounterList() {
const [counters, setCounters] = useState([0, 0, 0]);
const increment = (index) => {
setCounters(counters.map((c, i) => i === index ? c + 1 : c)); // ✅ 正确
// ❌ 错误写法:setCounters(prev => prev.map(...)) 未保证引用稳定性
};
return counters.map((c, i) => <button key={i} onClick={() => increment(i)}>{c}</button>);
}
关键考点:map 返回新数组确保状态不可变性;key 必须为稳定标识(此处用索引仅因无唯一 ID,实际项目需 item.id)。
真题实战:TCP 连接超时诊断矩阵
| 现象 | 客户端 netstat 状态 |
可能根因 | 验证命令 |
|---|---|---|---|
connect() 阻塞 3s |
SYN_SENT |
防火墙拦截 SYN 包 | tcpdump -i eth0 port 8080 |
read() 返回 EOF |
ESTABLISHED → FIN_WAIT1 |
服务端主动关闭连接 | ss -tnp \| grep :8080 |
write() 返回 EPIPE |
CLOSE_WAIT |
服务端未读取即 close() | lsof -i :8080 \| grep CLOSE_WAIT |
浏览器兼容性调试清单
- Safari 15+ 不支持
Intl.DateTimeFormat的year: 'numeric'与month: 'short'组合 → 改用formatToParts()手动拼接; - iOS WebView 中
IntersectionObserver在position: fixed元素上失效 → 添加will-change: transform强制启用合成层; - Chrome 119+ 移除
document.execCommand()→ 替换为beforeinput事件 +getSelection().getRangeAt(0)手动操作 DOM。
Docker 容器内调试黄金组合
# 进入运行中容器并挂载调试工具
docker exec -it --privileged -v /proc:/hostproc myapp sh -c \
"apk add --no-cache strace lsof && strace -p \$(pidof node) -e trace=connect,sendto,recvfrom -s 256"
真题压轴:LeetCode 142 链表环检测的调试复盘
面试官要求现场调试 Floyd 判圈算法。关键观察点:
slow指针每步走 1,fast每步走 2,首次相遇点距环入口距离恒等于头节点到环入口距离;- 使用
console.table([{slow: slow.val, fast: fast.val}])实时打印双指针位置; - 当
fast === null || fast.next === null时提前退出,避免Cannot read property 'next' of null错误。
WebAssembly 调试避坑指南
在 Chrome 中启用 wasm 源码映射需满足三要素:
- 编译时添加
-g --debuginfo(Emscripten); - 生成
.dwarf文件并部署至同域; - DevTools →
Settings→Preferences→ 勾选Enable WebAssembly Debugging;
若仍显示??符号,检查Content-Security-Policy是否阻断.dwarf加载。
