mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3905 字
11 分钟
生产调试实战
2025-11-20

凌晨 3 点,P99 延迟从 200ms 飙升到 2s。你打开 Grafana,看到红色的曲线在飙升,但 CPU、内存、磁盘都正常。你不知道为什么。

这是可观测性工程的终极考验——不是搭建系统,而是用系统解决问题。本章将介绍四种最常见的生产调试场景,以及可观测性驱动的排障工作流。

一、排障工作流#

1.1 可观测性驱动的排障#

graph TB DETECT[" 检测<br/>指标发现异常"] --> HYPOTH[" 假设<br/>提出可能的原因"] HYPOTH --> CORR[" 关联<br/>追踪定位瓶颈"] CORR --> CONF["确认<br/>日志/性能分析验证"] CONF --> FIX[" 修复<br/>部署修复"] FIX --> VERIFY[" 验证<br/>指标确认改善"] style DETECT fill:#e3f2fd,stroke:#1565c0 style HYPOTH fill:#fff3e0,stroke:#e65100 style CORR fill:#e8f5e9,stroke:#2e7d32 style CONF fill:#f3e5f5,stroke:#6a1b9a style FIX fill:#c8e6c9,stroke:#2e7d32 style VERIFY fill:#e3f2fd,stroke:#1565c0

1.2 排障时间目标#

阶段目标时间工具
检测< 5 分钟SLO 告警
定位< 10 分钟追踪 + Exemplar
理解< 15 分钟日志 + 性能分析
修复视问题而定部署流水线
验证< 5 分钟SLO 面板

1.3 从信号到根因的方法论#

排障的本质是从信号到根因的推理过程。这个过程可以抽象为一个方法论:

graph TB SIGNAL["信号<br/>指标/日志/追踪"] --> PATTERN["模式识别<br/>延迟型/错误型/资源型"] PATTERN --> HYPOTH["假设生成<br/>列出可能的原因"] HYPOTH --> TEST["假设验证<br/>通过关联信号验证"] TEST --> ROOT["根因确认<br/>日志/性能分析证实"] ROOT --> FIX["修复验证<br/>指标确认改善"] style SIGNAL fill:#e3f2fd,stroke:#1565c0 style PATTERN fill:#e8f5e9,stroke:#2e7d32 style HYPOTH fill:#fff3e0,stroke:#e65100 style TEST fill:#f3e5f5,stroke:#6a1b9a style ROOT fill:#c8e6c9,stroke:#2e7d32 style FIX fill:#e3f2fd,stroke:#1565c0
信号模式特征常见根因验证方法
延迟型P99 飙升、P50 正常连接池耗尽、锁竞争、GC 停顿追踪 + 火焰图
错误型错误率上升下游故障、配置错误、资源不足日志 + 追踪
资源型CPU/内存/磁盘异常内存泄漏、无限循环、磁盘满性能分析 + 指标
间歇型偶发异常GC 停顿、网络抖动、竞态条件长时间追踪 + 日志关联

二、场景 1:P99 延迟飙升#

2.1 症状#

  • P99 延迟从 200ms 飙升到 2s
  • P50 延迟正常(~50ms)
  • CPU、内存、磁盘正常
  • 错误率略有上升

2.2 排障步骤#

步骤 1:确认延迟分布

# 查看延迟分布
histogram_quantile(0.50, sum(rate(http_request_duration_bucket[5m])) by (le, service))
histogram_quantile(0.90, sum(rate(http_request_duration_bucket[5m])) by (le, service))
histogram_quantile(0.99, sum(rate(http_request_duration_bucket[5m])) by (le, service))

如果 P50 正常但 P99 飙升,说明是尾部延迟问题——少数请求极慢。

步骤 2:定位慢服务

# 按服务分解 P99
histogram_quantile(0.99, sum(rate(http_request_duration_bucket[5m])) by (le, service))

步骤 3:点击 Exemplar 跳转到追踪

在 Grafana 面板中,点击 P99 曲线上的 Exemplar 小圆点,跳转到具体的慢请求追踪。

步骤 4:分析追踪链路

追踪显示请求在 db.query Span 卡了 1.8s。点击该 Span,查看关联日志。

步骤 5:查看关联日志

日志显示 connection pool exhausted, waiting for available connection

步骤 6:确认根因

数据库连接池配置过小(max=50),而并发请求数已增长到 80。

步骤 7:修复并验证

将连接池 max 从 50 增加到 100,P99 延迟恢复正常。

2.3 延迟分析方法论#

延迟分析不只是”看 P99”——它是一个系统性的方法论:

第一步:确认延迟类型

延迟类型P50P99特征常见原因
全局延迟所有请求都慢资源不足、下游超时
尾部延迟正常少数请求极慢连接池耗尽、GC 停顿、锁竞争
周期性延迟正常周期性高定期出现定时任务、GC、日志轮转
突发性延迟正常突发高随机出现网络抖动、DNS 解析、冷启动

第二步:分解延迟来源

# 按服务分解
histogram_quantile(0.99, sum(rate(http_request_duration_bucket[5m])) by (le, service))
# 按路由分解
histogram_quantile(0.99, sum(rate(http_request_duration_bucket[5m])) by (le, route))
# 按方法分解
histogram_quantile(0.99, sum(rate(http_request_duration_bucket[5m])) by (le, method))

第三步:关联分析

延迟飙升时间其他信号可能的关联
GC 停顿同时GC P99 也飙升GC 影响延迟
部署后新版本上线新版本引入回归
流量高峰QPS 增加容量不足
下游超时下游服务 P99 也高级联延迟

2.4 延迟分析检查清单#

检查项工具关注点
延迟分布HistogramP50/P90/P99 差异
慢服务PromQL 按服务分解哪个服务最慢
慢 Span追踪哪个操作最慢
关联日志Loki + TraceID错误上下文
性能热点火焰图代码级热点

三、场景 2:内存泄漏#

3.1 症状#

  • 内存使用量持续增长(每天 +10%)
  • GC 频率增加
  • 最终 OOM Kill

3.2 排障步骤#

步骤 1:确认内存增长趋势

# 查看内存使用趋势
process_runtime_go_memory_heap_inuse_bytes{service="order-service"}
process_runtime_go_memory_heap_alloc_bytes{service="order-service"}

步骤 2:查看 GC 指标

# GC 频率和耗时
rate(go_gc_duration_seconds_count[5m])
histogram_quantile(0.99, rate(go_gc_duration_seconds_bucket[5m]))

步骤 3:查看内存火焰图

在 Pyroscope 中查看内存分配火焰图,找到分配热点。

步骤 4:分析堆 profile

# Go: 生成堆 profile
curl http://localhost:6060/debug/pprof/heap > heap.prof
go tool pprof -http=:8080 heap.prof
# 查看分配最多的函数
(pprof) top20 -cum

步骤 5:确认根因

火焰图显示 cache.Set 函数分配了 60% 的堆内存——缓存没有设置过期时间,导致对象持续堆积。

步骤 6:修复并验证

为缓存添加 TTL,内存使用量稳定。

3.3 内存泄漏诊断方法论#

内存泄漏的诊断需要区分三种情况:

泄漏类型症状诊断方法常见原因
堆内存泄漏堆内存持续增长heap pprof缓存无 TTL、全局 Map 无限增长
Goroutine 泄漏Goroutine 数持续增长goroutine pprof未关闭 channel、WaitGroup 未 Done
系统内存泄漏RSS 持续增长但堆稳定cgo pprof / 系统工具cgo 内存泄漏、mmap 未释放

堆内存泄漏的诊断流程

graph TB MEM["内存持续增长"] --> HEAP{"堆内存增长?"} HEAP -->|"是"| HEAPPPROF["heap pprof<br/>找到分配热点"] HEAP -->|"否"| GOROUTINE{"Goroutine 数增长?"} GOROUTINE -->|"是"| GOROPROF["goroutine pprof<br/>找到泄漏 Goroutine"] GOROUTINE -->|"否"| CGO["cgo/系统级泄漏<br/>使用系统工具诊断"] HEAPPPROF --> FIX["修复泄漏"] GOROPROF --> FIX CGO --> FIX style MEM fill:#ffcdd2,stroke:#c62828 style HEAPPPROF fill:#e8f5e9,stroke:#2e7d32 style GOROPROF fill:#e8f5e9,stroke:#2e7d32 style CGO fill:#fff3e0,stroke:#e65100

Goroutine 泄漏的诊断

# 查看 Goroutine 数量
curl http://localhost:6060/debug/pprof/goroutine?debug=1
# 输出示例:
# goroutine 12345 [chan receive, 10 minutes]:
# main.processOrder(...)
# /app/order.go:42
#
# goroutine 12346 [chan receive, 10 minutes]:
# main.processOrder(...)
# /app/order.go:42
# 如果大量 Goroutine 卡在同一个位置,说明是泄漏
// 常见的 Goroutine 泄漏模式
// 错误:没有退出条件
func processOrders(ch <-chan Order) {
for order := range ch {
process(order) // 如果 ch 永远不关闭,Goroutine 永远不会退出
}
}
// 正确:使用 context 控制退出
func processOrders(ctx context.Context, ch <-chan Order) {
for {
select {
case order, ok := <-ch:
if !ok {
return // channel 关闭时退出
}
process(order)
case <-ctx.Done():
return // context 取消时退出
}
}
}

3.4 内存泄漏诊断清单#

检查项工具关注点
内存趋势指标是否持续增长
GC 频率指标是否越来越频繁
堆分配火焰图哪个函数分配最多
对象类型pprof哪种对象最多
GC 停顿指标GC 是否影响延迟
Goroutine 数pprof是否持续增长

四、场景 3:间歇性故障#

4.1 症状#

  • 偶发 500 错误(每小时 2-3 次)
  • 无法稳定复现
  • 错误率在 SLO 边界徘徊

4.2 排障步骤#

步骤 1:确认错误模式

# 查看错误率趋势
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service, status)
/
sum(rate(http_requests_total[5m])) by (service, status)

步骤 2:查看错误追踪

在 Tempo 中搜索错误追踪,查看 Span 的错误信息。

步骤 3:分析错误日志

{service="order-service"} | json | level="ERROR" | error_code="TIMEOUT"

步骤 4:关联分析

发现间歇性故障总是发生在下游服务 A 的延迟飙升时——服务 A 的 GC 停顿导致偶发超时。

步骤 5:修复

增加对服务 A 的超时时间和重试策略。

4.3 间歇性故障诊断方法论#

间歇性故障是最难诊断的问题类型,因为它无法稳定复现。以下是一个系统性的诊断方法:

第一步:确认故障模式

模式特征可能原因
随机型无规律出现竞态条件、网络抖动
周期型固定间隔出现定时任务、GC、日志轮转
关联型与特定事件关联部署、流量高峰、下游故障
渐进型频率逐渐增加资源耗尽、数据增长

第二步:收集所有故障实例

# 收集所有错误日志(带 TraceID)
{service="order-service"} | json | level="ERROR"
| line_format "{{.timestamp}} {{.trace_id}} {{.error_code}} {{.message}}"

第三步:关联分析

将所有故障实例的时间点与其他信号关联:

故障时间GC 停顿?部署?流量高峰?下游超时?
02:15
03:42
05:18
07:33

如果故障时间与 GC 停顿高度相关,则 GC 是根因。

第四步:验证假设

# 验证 GC 停顿与错误的关联
# 计算 GC P99 与错误率的相关性
(
histogram_quantile(0.99, rate(go_gc_duration_seconds_bucket[5m]))
> 0.01 # GC 停顿超过 10ms
)
and
(
sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))
> 0.001 # 错误率超过 0.1%
)

4.4 间歇性故障诊断清单#

检查项工具关注点
错误频率指标是否有规律
错误追踪Tempo错误的上下文
错误日志Loki错误的详细信息
时间关联指标是否与 GC/部署相关
依赖分析追踪是否与下游服务相关

五、场景 4:GC 停顿影响尾部延迟#

5.1 症状#

  • P99 延迟偶发飙升
  • GC 耗时增加
  • 堆对象数量大

5.2 排障步骤#

步骤 1:确认 GC 与延迟的关联

# GC 耗时
histogram_quantile(0.99, rate(go_gc_duration_seconds_bucket[5m]))
# P99 延迟
histogram_quantile(0.99, sum(rate(http_request_duration_bucket[5m])) by (le))

如果 GC P99 和请求 P99 同时飙升,说明 GC 影响了延迟。

步骤 2:分析 GC 原因

# 查看 GC trace
GODEBUG=gctrace=1 ./my-app 2>gc.log
# 输出示例:
# gc 1 @0.005s 2%: 0.018+0.52+0.023 ms clock, 0.14+0.26/1.0/0.37+0.18 ms cpu, 4->4->1 MB, 5 MB goal, 8 P

步骤 3:优化 GC

  • 减少堆对象数量(对象池、避免不必要的分配)
  • 调整 GOGC 参数
  • 使用 sync.Pool 复用对象
Warning

GC 问题是尾部延迟的头号杀手。Go 的 GC 停顿通常在 1-5ms,但如果堆很大(> 10GB),GC 停顿可能达到 50-100ms,直接影响 P99 延迟。

5.3 GC 优化实践#

// 优化 1:使用 sync.Pool 复用对象
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func processRequest() {
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
buf.Reset()
// 使用 buf...
}
// 优化 2:预分配切片
// 错误:动态扩容
items := []Item{}
for _, item := range data {
items = append(items, item) // 每次扩容都会分配新内存
}
// 正确:预分配
items := make([]Item, 0, len(data))
for _, item := range data {
items = append(items, item) // 无需扩容
}
// 优化 3:调整 GOGC
// 默认 GOGC=100,表示堆增长 100% 时触发 GC
// 对于延迟敏感的服务,可以降低 GOGC 以更频繁地 GC
// GOGC=50:堆增长 50% 时触发 GC,GC 更频繁但停顿更短
优化手段效果代价
sync.Pool减少堆分配需要手动管理
预分配避免扩容需要预估大小
降低 GOGCGC 停顿更短GC 更频繁,CPU 开销增加
减少全局变量降低 GC 扫描量需要重构代码
使用值类型减少堆分配可能增加栈使用

六、分布式问题定位#

6.1 分布式系统的排障挑战#

在微服务架构中,一个请求可能经过 5-10 个服务。当问题发生时,你需要快速定位是哪个服务出了问题:

graph LR GW["API Gateway<br/>200ms"] --> AUTH["Auth Service<br/>50ms"] AUTH --> ORDER["Order Service<br/>1800ms "] ORDER --> DB["Database<br/>1600ms "] ORDER --> CACHE["Cache<br/>5ms"] AUTH --> USER["User Service<br/>30ms"] style GW fill:#c8e6c9,stroke:#2e7d32 style AUTH fill:#c8e6c9,stroke:#2e7d32 style ORDER fill:#fff3e0,stroke:#e65100 style DB fill:#ffcdd2,stroke:#c62828 style CACHE fill:#c8e6c9,stroke:#2e7d32 style USER fill:#c8e6c9,stroke:#2e7d32

6.2 分布式排障方法论#

步骤工具操作目标
1指标确认哪个服务的 P99 最高缩小范围
2Exemplar跳转到慢请求的追踪获取完整链路
3追踪找到耗时最长的 Span定位瓶颈服务
4日志查看瓶颈 Span 的关联日志获取错误上下文
5性能分析查看瓶颈服务的火焰图找到代码级热点

6.3 级联故障的识别#

级联故障是分布式系统中最危险的问题——一个服务的故障会像多米诺骨牌一样传播:

sequenceDiagram participant C as Client participant A as Service A participant B as Service B participant C2 as Service C Note over C2: Service C 变慢 C->>A: 请求 A->>B: 调用 B B->>C2: 调用 C (超时 5s) Note over B: B 等待 C 响应<br/>线程池耗尽 Note over A: A 等待 B 响应<br/>线程池耗尽 Note over C: 所有请求超时<br/>级联故障
级联故障模式特征防御措施
超时级联下游超时导致上游线程池耗尽设置合理的超时时间
重试风暴重试放大流量导致下游崩溃限制重试次数 + 指数退避
资源耗尽下游变慢导致上游连接池耗尽熔断器 + 限流
死锁等待循环依赖导致互相等待避免循环依赖

七、实战案例#

7.1 案例一:电商大促期间的 P99 飙升#

背景:双十一大促期间,订单服务的 P99 延迟从 200ms 飙升到 5s,但 P50 延迟正常。

排障过程

  1. 指标确认:P99 飙升但 P50 正常,典型的尾部延迟问题
  2. Exemplar 跳转:点击 P99 曲线上的 Exemplar,跳转到慢请求追踪
  3. 追踪分析:追踪显示 inventory.check Span 耗时 4.5s
  4. 日志关联:日志显示 redis: connection pool exhausted, max=20, active=20, waiting=150
  5. 根因:库存检查服务使用 Redis 连接池 max=20,但大促期间并发请求达到 200
  6. 修复:将 Redis 连接池 max 从 20 增加到 100,P99 恢复到 300ms
// 修复前
rdb := redis.NewClient(&redis.Options{
Addr: "redis:6379",
PoolSize: 20, // 太小
MinIdleConns: 5,
})
// 修复后
rdb := redis.NewClient(&redis.Options{
Addr: "redis:6379",
PoolSize: 100, // 增加连接池
MinIdleConns: 25, // 增加空闲连接
ReadTimeout: 500 * time.Millisecond, // 设置读超时
WriteTimeout: 500 * time.Millisecond, // 设置写超时
})

教训:连接池配置应该基于压测结果,而不是凭经验。大促前必须进行全链路压测。

7.2 案例二:支付服务的内存泄漏#

背景:支付服务的内存使用量每天增长 10%,7 天后 OOM Kill。重启后恢复正常,但 7 天后再次 OOM。

排障过程

  1. 指标确认process_resident_memory_bytes 每天增长约 200MB
  2. GC 分析:GC 频率从每秒 2 次增长到每秒 20 次,但回收的内存很少
  3. 堆 pprofpayment_cache.Set 占了 70% 的堆分配
  4. 代码分析:支付缓存使用 map[string]*Payment,没有设置过期时间
  5. 根因:支付记录不断添加到缓存中,但从未删除。7 天后缓存占用了 1.4GB
  6. 修复:使用带 TTL 的缓存库替换原生 Map
// 修复前:原生 Map,无过期
var paymentCache = make(map[string]*Payment)
func cachePayment(p *Payment) {
paymentCache[p.ID] = p // 永远不会删除
}
// 修复后:带 TTL 的缓存
var paymentCache = ttlcache.NewCache[string, *Payment]()
paymentCache.SetTTL(24 * time.Hour) // 24 小时过期
func cachePayment(p *Payment) {
paymentCache.Set(p.ID, p)
}

教训:所有缓存都必须设置 TTL 或最大容量。使用内存监控告警,在内存使用超过 80% 时提前预警。

7.3 案例三:搜索服务的间歇性 500 错误#

背景:搜索服务每小时出现 2-3 次 500 错误,错误率在 SLO(99.9%)边界徘徊。无法在测试环境复现。

排障过程

  1. 错误模式分析:错误集中在 POST /api/v1/search 接口,时间间隔不规律
  2. 错误追踪:追踪显示错误发生在 es.query Span,错误信息为 circuit breaker open
  3. 关联分析:错误时间与 Elasticsearch 的 search_thread_pool_queue 指标高度相关
  4. 根因:Elasticsearch 的搜索线程池队列满时触发熔断器,导致搜索请求被拒绝
  5. 修复:增加 Elasticsearch 线程池队列大小 + 实现客户端重试
# Elasticsearch 线程池配置修复
# 修复前
thread_pool.search.size: 5
thread_pool.search.queue_size: 100
# 修复后
thread_pool.search.size: 10
thread_pool.search.queue_size: 500
// 客户端重试
func searchWithRetry(ctx context.Context, query string, maxRetries int) (*SearchResult, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
result, err := esClient.Search(ctx, query)
if err == nil {
return result, nil
}
lastErr = err
// 指数退避
time.Sleep(time.Duration(math.Pow(2, float64(i))) * 100 * time.Millisecond)
}
return nil, lastErr
}

教训:间歇性故障往往与下游服务的资源限制有关。实现重试 + 熔断是防御间歇性故障的标准模式。

八、排障工具箱#

8.1 工具速查表#

问题类型第一步第二步第三步
延迟飙升Histogram 指标Exemplar → 追踪日志 + 火焰图
内存泄漏内存指标堆 pprof火焰图
间歇性故障错误率指标错误追踪日志关联
GC 问题GC 指标GC trace堆优化

8.2 常用 PromQL 查询#

# RED 方法
# Rate
sum(rate(http_requests_total[5m])) by (service)
# Errors
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
# Duration
histogram_quantile(0.99, sum(rate(http_request_duration_bucket[5m])) by (le, service))
# USE 方法
# Utilization
1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]))
# Saturation
avg(node_load5)
# Errors
rate(node_disk_io_time_seconds_total[5m])

8.3 排障调试检查清单#

当排障陷入僵局时,按以下清单逐项检查:

序号检查项命令/操作预期结果
1服务是否存活curl /healthzHTTP 200
2最近是否有部署查看 CI/CD 记录确认变更
3错误率是否上升PromQL 查询定位错误服务
4P99 是否飙升Histogram 查询确认延迟类型
5是否有 OOMkubectl describe pod查看 OOM 事件
6是否有磁盘满df -h磁盘使用率 < 90%
7GC 是否正常GC 指标查询GC P99 < 10ms
8连接池是否耗尽追踪 + 日志无等待超时
9下游是否正常追踪链路分析下游延迟正常
10是否有重试风暴追踪 + 日志无异常重试
Note

排障最忌讳的是”盲目尝试”——没有根据地重启服务、调整参数、回滚版本。正确的方法是先收集信号,再形成假设,最后验证假设。每次只改变一个变量,观察效果后再决定下一步。

九、动手实践#

9.1 模拟延迟飙升#

# 注入延迟
curl -X POST http://localhost:8080/api/v1/orders -d '{"delay_ms": 2000}'

9.2 排障演练#

# 1. 在 Grafana 中查看 P99 延迟飙升
# 2. 点击 Exemplar 跳转到追踪
# 3. 在追踪中定位慢 Span
# 4. 查看关联日志
# 5. 确认根因

9.3 模拟内存泄漏#

# 注入内存泄漏
curl -X POST http://localhost:8080/api/v1/leak -d '{"size_mb": 100}'
# 观察内存增长
curl http://localhost:9090/api/v1/query?query=process_resident_memory_bytes
# 生成堆 profile
curl http://localhost:6060/debug/pprof/heap > heap.prof
go tool pprof -http=:8080 heap.prof

9.4 验证清单#

检查项验证方式预期结果
延迟检测P99 面板显示飙升
Exemplar 跳转点击 Exemplar跳转到追踪
追踪定位查看追踪找到慢 Span
日志关联查看 Span 日志看到错误信息
内存泄漏检测内存面板显示增长趋势
堆 pprof生成 heap profile找到分配热点

十、本章小结#

上一章理解了可观测性平台设计。 用了整章的篇幅拆解生产调试的实战方法论。

主题核心要点关键词
排障工作流检测 → 假设 → 关联 → 确认 → 修复 → 验证。从信号到根因的推理过程是排障的核心。排障工作流
延迟飙升Histogram → Exemplar → 追踪 → 日志 → 性能分析。区分延迟类型(全局/尾部/周期/突发)是关键。延迟飙升
内存泄漏内存指标 → GC 指标 → 堆 pprof → 火焰图。区分堆泄漏、Goroutine 泄漏和系统级泄漏。内存泄漏
间歇性故障错误率 → 错误追踪 → 日志关联 → 依赖分析。关联分析是诊断间歇性故障的关键。间歇性故障
GC 问题GC 指标 → GC trace → 堆优化。sync.Pool、预分配、调整 GOGC 是三种主要优化手段。GC 问题
分布式问题定位指标定位服务 → 追踪定位 Span → 日志确认根因。级联故障需要熔断器和重试策略。分布式问题定位
实战案例连接池耗尽、缓存无 TTL、下游熔断——三个最常见的生产问题模式。实战案例

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

生产调试实战
https://blog.souloss.com/posts/observability/production-debugging/
作者
Souloss
发布于
2025-11-20
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时