Posted in

【陌陌Go故障响应SOP】:从告警触发到定位根因平均耗时<4分17秒的5级分级响应机制

第一章:【陌陌Go故障响应SOP】:从告警触发到定位根因平均耗时

陌陌Go服务采用“告警即工单、工单即SLA”的实时闭环机制,将故障响应压缩至亚分钟级。核心依托五级动态分级模型(P0–P4),每级严格绑定响应时效、升级路径、协同角色与自动化处置动作,确保P0级严重故障(如核心消息投递中断、用户登录成功率跌穿95%)从Prometheus告警触发到根因定位平均耗时稳定在4分17秒以内(2024 Q2 SLO达成率99.83%)。

告警自动分级与路由

告警经Alertmanager统一接入后,由go-sop-router服务基于预设规则引擎实时打标:

  • 匹配service="msg-gateway" && severity="critical" → 自动升为P0,同步触发企业微信P0群@全体+电话直呼oncall;
  • 匹配http_status_code{code=~"5xx"} > 1000且持续2分钟 → 升为P1,推送至值班工程师企微私聊+钉钉待办;
  • 其余告警默认归入P2–P4,按静默窗口与影响面二次校验。

P0级根因定位黄金四步法

  1. 秒级快照采集:告警触发瞬间,自动执行以下命令抓取运行时状态:
    # 并发采集goroutine堆栈、内存profile、HTTP连接数、GC统计(超时8s强制终止)
    curl -s "http://localhost:6060/debug/pprof/goroutine?debug=2" > /tmp/goroutine.log &
    curl -s "http://localhost:6060/debug/pprof/heap" | go tool pprof -top -lines -nodecount=20 - /tmp/heap.pprof &
    ss -tnp | grep ':8080' | wc -l > /tmp/conn_count.txt &
    go tool pprof -gc -lines http://localhost:6060/debug/pprof/gc -seconds=30 > /tmp/gc_stats.txt &
    wait
  2. 火焰图自动生成:通过pprof-to-flame工具将heap/goroutine数据转为可交互SVG,链接直接嵌入工单详情页;
  3. 依赖链路染色分析:调用Jaeger API查询该时段TraceID前缀匹配的慢请求,过滤出span.error=true且duration>2s的上游调用;
  4. 配置变更比对:自动拉取最近30分钟GitOps仓库中msg-gateway/deploy.yamlconfigmap变更记录,高亮diff行并关联告警时间戳。

五级响应能力对照表

级别 响应时限 升级阈值 自动化动作 协同要求
P0 ≤60秒 核心链路不可用 全量快照+熔断开关启用 SRE+后端+DBA三方强同步
P1 ≤5分钟 错误率>1%持续3min 日志关键词扫描+指标下钻 主责工程师主导
P2 ≤15分钟 单节点异常 重启Pod+日志归档 无需跨组协同
P3/P4 ≤30分钟 非核心模块降级 仅记录事件流 仅需确认接收

第二章:五级分级响应机制的设计原理与工程落地

2.1 基于SLI/SLO驱动的故障等级定义模型(含陌陌核心服务SLI指标矩阵实践)

故障等级不再依赖人工经验判断,而是由SLI劣化程度与SLO承诺窗口双重约束动态生成。陌陌将核心服务(如消息收发、关系链同步)抽象为4类SLI维度:可用性、延迟、正确性、一致性,并建立分层SLO目标:

SLI类型 指标示例 生产SLO P0触发阈值
可用性 http_status_5xx_rate 99.95% >0.1%
延迟 p99_request_latency_ms ≤400ms >800ms
正确性 msg_duplication_rate 0.0% >0.001%

数据同步机制

消息投递服务采用双写校验SLI采集:

# SLI实时计算逻辑(Flink SQL)
SELECT 
  window_start,
  COUNT(*) FILTER (WHERE status = 500) * 100.0 / COUNT(*) AS error_rate,
  PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY latency_ms) AS p99_lat
FROM kafka_events 
GROUP BY TUMBLING(window, INTERVAL '1' MINUTE);

该SQL每分钟滚动统计错误率与P99延迟;FILTER确保分母为全量请求,避免空值干扰;PERCENTILE_CONT提供高精度尾部延迟观测,支撑P0/P1故障自动分级。

故障等级映射规则

graph TD
A[SLI劣化] –> B{是否同时突破2个SLO?}
B –>|是| C[P0: 全链路阻断]
B –>|否| D{单指标超限持续≥3分钟?}
D –>|是| E[P1: 核心功能降级]
D –>|否| F[P2: 观察中]

2.2 响应时效性保障的三重约束:MTTD、MTTI、MTTK的Go语言级埋点验证

为精准捕获故障生命周期关键时点,需在Go服务核心路径植入轻量级、无侵入的埋点逻辑:

// MTTD(平均检测时间)埋点:从请求进入HTTP handler开始计时
func handleRequest(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    ctx := context.WithValue(r.Context(), "mttd_start", start) // 埋点起点

    // MTTI(平均确认时间):经规则引擎判定为真实异常后打点
    defer func() {
        if r := recover(); r != nil {
            tti := time.Since(start) // 实际确认耗时
            metrics.ObserveMTTI(tti.Seconds())
        }
    }()
}

该埋点将time.Time注入Context,避免全局变量污染;metrics.ObserveMTTI()调用底层Prometheus Histogram,支持P95/P99分位统计。

关键指标语义对齐

指标 触发条件 Go埋点位置
MTTD http.Request抵达Handler time.Now() at entry
MTTI 异常panic或显式errors.Is(err, ErrCritical) recover()/if err != nil分支
MTTK k8s.io/client-go成功提交修复Job clientset.BatchV1().Jobs().Create()返回后

验证闭环流程

graph TD
    A[HTTP Request] --> B[MTTD Start: Context注入]
    B --> C{Rule Engine Check}
    C -->|True| D[MTTI Mark: panic/recover]
    C -->|False| E[Normal Flow]
    D --> F[MTTK Trigger: Job Submit]
    F --> G[Prometheus Exporter]

2.3 分级自动路由引擎:基于OpenTelemetry TraceID与标签路由的Go中间件实现

该中间件在HTTP请求生命周期中动态提取OpenTelemetry标准traceparent头与自定义标签(如env=prod, region=shanghai),构建多级路由决策树。

核心路由策略

  • 优先匹配TraceID前缀(如01a2b3→灰度集群)
  • 次选业务标签组合(env=staging&feature=checkout-v2
  • 默认回退至全局负载均衡

路由决策流程

func RouteMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        // 从context提取OTel span,获取TraceID和属性
        span := trace.SpanFromContext(ctx)
        traceID := span.SpanContext().TraceID().String() // 如: "01a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6"

        // 解析X-Route-Tag头(支持逗号分隔多标签)
        tags := strings.Split(r.Header.Get("X-Route-Tag"), ",")

        cluster := routeByTracePrefix(traceID) // 前4位哈希映射
        if cluster == "" {
            cluster = routeByTags(tags) // 标签规则引擎匹配
        }

        r.Header.Set("X-Routed-To", cluster)
        next.ServeHTTP(w, r)
    })
}

逻辑分析traceID.String()返回32位十六进制字符串,routeByTracePrefix取前4字符转为uint32后模3,实现稳定哈希;X-Route-Tag头值经strings.Split解析为[]string,交由标签规则引擎(如AST匹配器)执行优先级判定。

支持的路由维度对比

维度 精度 可控性 典型场景
TraceID前缀 全链路灰度引流
自定义标签 多环境/功能开关
请求路径 接口级AB测试
graph TD
    A[HTTP Request] --> B{Has traceparent?}
    B -->|Yes| C[Extract TraceID]
    B -->|No| D[Use default cluster]
    C --> E[Hash first 4 chars → modulo N]
    E --> F[Select cluster]
    A --> G[Read X-Route-Tag]
    G --> H[Match tag rules]
    H -->|Matched| F
    H -->|None| D

2.4 值班链路熔断与动态升阶策略:Go协程安全的分级升降级状态机设计

在高可用值班系统中,链路异常需触发毫秒级熔断,并依据故障持续时长与影响面自动升阶至更高响应层级。

状态机核心结构

type EscalationState int

const (
    StateIdle EscalationState = iota // 初始空闲
    StateFused                       // 链路熔断(5s内3次失败)
    StateEscalated                   // 升阶至二级值班人
    StateCritical                    // 升阶至P0应急小组
)

// 线程安全状态迁移
func (s *StateMachine) Transition(err error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    // ……状态跃迁逻辑(见下文分析)
}

该实现通过 sync.RWMutex 保障多协程并发调用下的状态一致性;Transition 方法依据错误率滑动窗口(基于 time.Now().Sub(s.lastFail))判定是否满足熔断阈值,避免 Goroutine 泄漏。

升阶决策维度

维度 判定条件 动作
持续失败时长 ≥15s 触发 StateEscalated
并发失败数 同一周期内≥5次 直接跳转 StateCritical
SLA偏离度 P99延迟 > 2×基线且持续10s 启动人工确认流程

熔断-升阶协同流程

graph TD
    A[链路请求] --> B{失败?}
    B -->|是| C[记录失败时间戳]
    C --> D[计算最近60s失败率]
    D --> E{≥60%?}
    E -->|是| F[进入StateFused]
    E -->|否| G[保持StateIdle]
    F --> H{持续15s未恢复?}
    H -->|是| I[Transition→StateEscalated]

2.5 SOP执行沙箱机制:隔离式命令执行与变更审计日志的Go runtime集成方案

沙箱机制依托 syscall.Clone 创建 PID namespace 实现进程级隔离,所有 SOP 命令在独立命名空间中执行,避免污染宿主环境。

审计日志结构化捕获

type AuditEntry struct {
    ID        string    `json:"id"`
    Command   string    `json:"command"`
    StartTime time.Time `json:"start_time"`
    ExitCode  int       `json:"exit_code"`
    Stdout    string    `json:"stdout"`
    Stderr    string    `json:"stderr"`
}

该结构体作为审计事件载体,嵌入 context.WithValue 透传至子 goroutine,确保日志上下文不丢失;IDuuid.NewString() 生成,StartTime 精确到纳秒,支撑毫秒级变更追踪。

沙箱生命周期管理

  • 启动:调用 unshare(CLONE_NEWPID) + chroot 构建最小根文件系统
  • 执行:exec.LookPath 校验白名单命令路径
  • 收尾:自动注入 syncfs() 并写入审计日志至 WAL 文件
字段 类型 说明
Command string 原始 shell 命令(非解析后参数)
ExitCode int 真实 waitstatus 退出码,含信号位
Stdout/Stderr string Base64 编码,规避 JSON 控制字符
graph TD
A[Init Sandbox] --> B[Clone PID NS]
B --> C[Chroot + Drop Caps]
C --> D[Exec Command]
D --> E[Capture IO & Exit Code]
E --> F[Serialize AuditEntry]
F --> G[Append to Rotating WAL]

第三章:Go运行时根因定位的黄金信号体系

3.1 Goroutine泄漏与阻塞的实时检测:pprof+trace+自研gostat探针联动分析法

Goroutine泄漏常因未关闭的channel、死循环等待或忘记调用close()引发;阻塞则多见于同步原语(如sync.Mutextime.Sleep)长期持锁或无限等待。

数据同步机制

gostat探针以500ms周期采样runtime.NumGoroutine()/debug/pprof/goroutine?debug=2快照,并注入trace事件标记goroutine生命周期起点:

// gostat/probe.go:轻量级goroutine上下文追踪
func TrackGoroutine(id uint64, fnName string) {
    trace.Log(ctx, "gostat:start", fmt.Sprintf("gid=%d,fn=%s", id, fnName))
    // 注册退出钩子,避免defer丢失
    go func() { defer trace.Log(ctx, "gostat:end", fmt.Sprintf("gid=%d", id)) }()
}

该代码在goroutine启动时打点并异步记录结束事件,确保trace中可关联start→end跨度;idatomic.AddUint64(&gid, 1)生成,避免竞态。

联动分析流程

graph TD
    A[pprof/goroutine] -->|堆栈快照| B{gostat聚合}
    C[trace/event] -->|时间戳+gid| B
    B --> D[异常模式识别:>5min存活/无end事件]
    D --> E[告警+火焰图定位]

关键指标对比

指标 pprof单独使用 pprof+trace+gostat
定位泄漏goroutine ❌(仅快照) ✅(跨trace生命周期)
区分阻塞类型 ⚠️(需人工解析) ✅(结合Mutex/Chan事件)

3.2 GC压力与内存逃逸的现场快照:基于runtime.ReadMemStats与mmap元数据的秒级诊断

当GC频繁触发且PauseNs持续偏高时,需区分是真实堆增长,还是短期对象逃逸导致的假性压力。

关键诊断双轨法

  • 调用 runtime.ReadMemStats() 获取实时堆指标(如 HeapAlloc, NextGC, NumGC
  • 解析 /proc/self/mapsmmap 区域,识别未被GC管理的大块匿名映射(如 libgomheap 预留区或 unsafe 手动分配)
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("HeapAlloc: %v MiB, NextGC: %v MiB, NumGC: %d\n",
    m.HeapAlloc/1024/1024, m.NextGC/1024/1024, m.NumGC)

HeapAlloc 反映当前活跃堆字节数;NextGC 是下一次GC触发阈值;NumGC 突增配合 PauseTotalNs/NumGC 升高,表明GC效率劣化。该调用零分配、纳秒级完成,适合高频采样。

mmap元数据定位逃逸热点

地址范围 权限 偏移 设备 Inode 路径
7f8a2c000000-7f8a2c400000 rw-p 0 00:00 0 [anon](大页匿名映射)
graph TD
    A[触发诊断] --> B{ReadMemStats}
    A --> C{Parse /proc/self/maps}
    B --> D[计算GC频率与暂停占比]
    C --> E[过滤rw-p + [anon] + size > 2MB]
    D & E --> F[交叉比对:HeapAlloc稳定但mmap激增 → 内存逃逸]

3.3 网络连接池与context超时链路的端到端追踪:net/http transport层增强与Go 1.22 context.WithDeadline优化实践

连接复用与超时协同失效场景

默认 http.TransportIdleConnTimeoutcontext.WithDeadline 独立生效,导致“连接空闲未关闭但请求已超时”时仍占用池资源。

Go 1.22 中的 deadline 感知增强

// 新增 Transport.DialContext 隐式感知 context deadline
tr := &http.Transport{
    DialContext: (&net.Dialer{
        Timeout:   5 * time.Second,
        KeepAlive: 30 * time.Second,
    }).DialContext,
    // Go 1.22 自动将 ctx.Deadline() 注入底层 dialer 超时判定
}

逻辑分析:DialContext 在 Go 1.22 中被重写为优先使用 ctx.Err()ctx.Deadline() 中更早触发者终止连接建立;Timeout 退为兜底值。参数 KeepAlive 仅作用于已建立连接的保活探测。

端到端超时链路对齐策略

组件 推荐配置方式 说明
HTTP Client context.WithDeadline(ctx, t) 请求级截止时间
Transport IdleConnTimeout = 90% of deadline 避免空闲连接拖慢整体回收
TLSHandshakeTimeout 显式设为 3s(≤ client deadline) 防止 TLS 握手独占连接池
graph TD
    A[HTTP Request] --> B{WithContextDeadline}
    B --> C[Transport.DialContext]
    C --> D[Deadline-aware net.Conn]
    D --> E[IdleConnTimeout 触发回收]
    E --> F[连接池释放]

第四章:SOP自动化工具链与可观测性基建协同

4.1 Go故障快反CLI工具:mo-goreport——支持trace回溯、goroutine dump、heap profile一键聚合

mo-goreport 是面向生产环境的轻量级 Go 故障诊断 CLI 工具,专为 SRE 和后端工程师设计,可在单次调用中并发采集多维度运行时数据。

核心能力聚合

  • 一键触发 runtime/trace(30s 采样)、pprof/goroutine?debug=2 快照、pprof/heap(inuse_space)
  • 自动归档为时间戳命名的 .zip 包,含可视化 HTML 报告入口

使用示例

# 采集本机进程(PID=12345),超时60秒,输出至./report-202405/
mo-goreport -p 12345 -t 60 -o ./report-202405/

逻辑说明:-p 指定目标进程 PID;-t 控制 trace 最大持续时长(非阻塞);-o 指定输出根目录。工具内部通过 /proc/<pid>/cmdline 验证进程有效性,并自动注入 net/http/pprof handler(若未启用)。

输出结构概览

文件名 类型 用途
trace.html HTML 可交互式 trace 时间线
goroutines.txt Text goroutine stack dump
heap.svg SVG heap profile 可视化图谱
graph TD
    A[mo-goreport 启动] --> B{连接目标进程}
    B -->|成功| C[并发发起 trace/goroutine/heap 请求]
    B -->|失败| D[报错并退出]
    C --> E[压缩归档 + 生成 report.html]

4.2 Prometheus + Grafana + Loki三位一体告警归因看板:Go服务专属Dashboard模板与SOP动作绑定规则

数据同步机制

Prometheus 抓取 Go 服务 /metrics(含 go_goroutines, http_request_duration_seconds_bucket),Loki 通过 promtail 基于 job="go-app" 标签采集结构化日志(JSON 格式),两者共享 cluster, service, pod 等维度标签,实现指标-日志关联。

告警归因核心逻辑

# alert-rules.yml —— Go服务P99延迟突增触发归因链
- alert: GoHTTPDurationP99High
  expr: histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{job="go-app"}[5m])) by (le, job, service, pod)) > 0.5
  for: 2m
  labels:
    severity: critical
    category: latency
  annotations:
    summary: "P99 HTTP延迟超500ms ({{ $labels.service }}/{{ $labels.pod }})"

该规则基于直方图分位数计算真实P99延迟;for: 2m 避免毛刺误报;category: latency 用于Grafana看板动态过滤与SOP路由。

SOP动作自动绑定

告警类别 触发动作 执行方式
latency 检查goroutine泄漏 + 查看error日志上下文 Grafana点击跳转Loki查询 {job="go-app"} + | json | __error != ""
panic 拉取最近3条panic堆栈 + 通知oncall Loki查询 |~ "panic:" | limit 3 → Webhook调用内部运维平台

归因流程可视化

graph TD
  A[Prometheus告警触发] --> B[Grafana Dashboard高亮异常Pod]
  B --> C{点击“归因分析”按钮}
  C --> D[Loki执行预置日志查询]
  D --> E[自动注入$pod, $service变量]
  E --> F[展示错误日志+goroutine dump快照]

4.3 自动化根因建议引擎:基于历史故障知识图谱与当前指标异常模式的Go决策树推理模块

该模块以轻量级 Go 实现,将知识图谱嵌入向量与实时指标时序特征对齐,驱动多叉决策树动态剪枝。

核心推理流程

// 基于异常模式匹配的决策树前向推理
func (e *Engine) Infer(anomalies []MetricAnomaly, kgEmbed vector32) []RootCause {
    var causes []RootCause
    node := e.root
    for _, a := range anomalies {
        // 节点分裂依据:指标类型 + 变化斜率符号 + KG邻域相似度 > 0.78
        if score := cosineSim(a.Embedding, kgEmbed); score > 0.78 {
            node = node.children[a.MetricType]
        }
        if node.isLeaf {
            causes = append(causes, node.cause)
            break
        }
    }
    return causes
}

cosineSim 计算当前异常嵌入与知识图谱中故障实体向量的余弦相似度;阈值 0.78 来自历史验证集 ROC 曲线下最优截断点;a.MetricType 触发语义一致的子树跳转,避免全量遍历。

知识-指标对齐策略

对齐维度 历史知识图谱来源 实时指标输入
时间语义 故障发生窗口(±5min) 当前滑动窗口(3min)
实体粒度 Service → Pod → Container Prometheus label set
异常强度映射 归一化严重等级(1–5) Z-score > 3.0
graph TD
    A[实时指标流] --> B{异常检测器}
    B -->|MetricAnomaly[]| C[KG向量检索]
    C --> D[相似度过滤]
    D --> E[决策树路径选择]
    E --> F[根因建议列表]

4.4 故障复盘闭环系统:从SOP执行日志到Go test case自动生成的归档流水线

核心流程概览

graph TD
    A[SOP执行日志] --> B[结构化解析引擎]
    B --> C[故障模式标注]
    C --> D[测试用例模板填充]
    D --> E[生成go test file]
    E --> F[自动归档+CI触发]

日志解析与模式识别

解析器从 sop-run-20240521-1423.log 中提取关键字段:

  • error_code: "ERR_TIMEOUT_503"
  • recovery_steps: ["restart-proxy", "flush-cache"]
  • affected_service: "auth-service"

自动生成测试代码片段

// 生成路径: ./testcases/auth_service_timeout_test.go
func TestAuthService_TimeoutRecovery(t *testing.T) {
    t.Parallel()
    // 参数说明:
    // - timeoutSec=5:复现超时阈值(来自日志中 actual_duration=5200ms)
    // - recoverySteps:按SOP执行顺序注入模拟行为
    mock := newAuthMock().WithTimeout(5).WithRecoverySteps("restart-proxy", "flush-cache")
    assert.NoError(t, mock.Run())
}

该测试复现故障上下文并验证SOP有效性,参数直连日志元数据,确保可追溯性。

归档策略对照表

组件 存储位置 保留周期 关联CI触发条件
原始日志 /archive/logs/sop/ 90天
生成testcase /testcases/generated/ 永久 git push 到 main
模式标签库 /ml/labels/ 动态更新 每日增量训练模型

第五章:持续演进与跨团队协同治理

在某头部金融科技公司落地微服务治理平台的第三年,其核心支付网关集群已覆盖17个业务域、42个独立服务单元,由9个地理分散的开发团队共同维护。当单日交易峰值突破2300万笔时,传统“发布即交付”的协作模式彻底失效——一次跨团队依赖升级引发连锁故障:风控规则引擎v3.2要求认证中心强制启用OAuth2.1协议,但身份中台团队尚未完成兼容改造,导致37%的实名认证请求超时。

建立可验证的契约演进机制

团队采用Pact Broker构建双向契约流水线:消费者端(如营销系统)提前声明对用户服务GET /v2/profile的字段需求(含risk_score非空约束),提供者端(用户中心)每次CI构建自动执行契约测试。2023年Q4数据显示,因接口变更引发的集成缺陷下降82%,平均修复耗时从11.3小时压缩至27分钟。

构建跨职能治理看板

通过嵌入式Prometheus+Grafana实现多维治理仪表盘,关键指标包括: 指标维度 数据来源 预警阈值
跨团队SLA达标率 ServiceMesh遥测数据
契约变更响应延迟 GitLab MR合并时间戳 >72小时
故障根因归属准确率 AIOps平台聚类分析结果

实施渐进式权限沙盒

为解决“谁有权修改全局限流策略”争议,平台引入RBAC+ABAC混合模型:基础角色(如“流量治理员”)仅能调整本域服务配置;当需修改跨域熔断阈值时,系统自动触发三方会签流程——调用方、被调用方、平台SRE各需在15分钟内完成数字签名,否则策略回滚至上一稳定版本。

flowchart LR
    A[新策略提交] --> B{是否影响≥3个域?}
    B -->|是| C[启动跨域评审工作流]
    B -->|否| D[自动生效]
    C --> E[调用方确认]
    C --> F[被调用方确认]
    C --> G[SRE平台组确认]
    E & F & G --> H[三签通过→生效]
    H --> I[更新策略版本库]
    style I fill:#4CAF50,stroke:#388E3C

运行时策略热加载实践

2024年春节保障期间,为应对突发流量,支付网关团队通过Envoy xDS API动态推送新路由规则:将/pay/v2/submit路径的权重从主集群80%临时调整为灾备集群65%,全程耗时4.2秒且零连接中断。该能力依赖于自研的ConfigSyncer组件,它将GitOps仓库中的YAML策略实时转换为xDS格式,并通过gRPC流式推送至217个Envoy实例。

治理成效量化追踪

每季度生成《跨团队协同健康度报告》,包含12项核心指标:服务间平均MTTR(从故障发现到恢复)、跨域MR平均评审轮次、契约覆盖率(按服务维度)、策略变更回滚率等。2024年Q1报告显示,跨团队MR平均合并周期缩短至38小时,较2022年同期提升3.7倍;生产环境因治理策略冲突导致的故障数归零。

热爱算法,相信代码可以改变世界。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注