Posted in

Go后台接口上线前必须做的8项压测 checklist,漏1项可能导致大促宕机(附Prometheus+Locust脚本)

第一章:Go后台接口压测的底层逻辑与风险认知

压测不是简单地发起高并发请求,而是对 Go 运行时调度、内存管理、网络 I/O 模型及业务逻辑耦合度的一次系统性压力探针。理解其底层逻辑,是避免将“压出问题”误判为“发现瓶颈”的前提。

Go 并发模型与调度器影响

Go 的 GMP 调度模型决定了压测中 goroutine 数量激增时,并非线性提升吞吐,而可能触发频繁的 Goroutine 创建/销毁、P 抢占切换及 GC 压力上升。尤其当接口中存在阻塞式系统调用(如未设 timeout 的 http.DefaultClient.Do)或同步原语滥用(如全局 sync.Mutex),将导致 M 被挂起,P 闲置,整体并发效率断崖式下降。

内存与 GC 风险放大器

高频请求易引发对象高频分配,若响应体含大结构体或未复用 bytes.Buffer / sync.Pool,将加剧堆内存增长,触发更频繁的 STW(Stop-The-World)GC。可通过以下命令实时观测:

# 在压测期间持续监控 GC 暂停时间(单位:纳秒)
go tool trace -http=localhost:8080 ./your-app-binary &
# 然后访问 http://localhost:8080/debug/pprof/gc

重点关注 gctrace=1 输出中的 gc N @X.Xs X%: ... 行中 STW 时间是否超过 1ms。

网络层与连接复用陷阱

默认 http.Client 复用 TCP 连接,但若 Transport.MaxIdleConnsPerHost 设置过小(默认2),高并发下将大量创建新连接,耗尽本地端口或触发 TIME_WAIT 拥塞。建议在压测前显式配置:

client := &http.Client{
    Transport: &http.Transport{
        MaxIdleConns:        200,
        MaxIdleConnsPerHost: 200, // 关键:避免连接争抢
        IdleConnTimeout:     30 * time.Second,
    },
}

常见误判风险清单

  • 将 Prometheus 中 go_goroutines 持续攀升归因为代码泄漏,实则因 panic 后 defer 未执行导致资源未释放;
  • 认为 CPU 使用率低即无性能瓶颈,忽略 runtime.ReadMemStatsPauseTotalNs 占比过高;
  • 使用 abwrk 压测时未开启 HTTP/1.1 Keep-Alive,造成服务端连接风暴而非真实业务负载。

第二章:压测前环境与配置的8大校验点

2.1 核查Go运行时参数(GOMAXPROCS、GC策略)与容器资源限制对吞吐的影响

Go程序在容器中常因运行时参数与cgroup限制不匹配导致吞吐骤降。GOMAXPROCS默认等于Linux sysconf(_SC_NPROCESSORS_ONLN),但容器内/sys/fs/cgroup/cpu/cpu.cfs_quota_us可能限制实际可用CPU时间片。

GOMAXPROCS 与 CPU Quota 的错配

# 查看容器CPU限额(例如:quota=50000, period=100000 → 0.5核)
cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us /sys/fs/cgroup/cpu/cpu.cfs_period_us
# 输出:50000 100000

若宿主机有8核而容器仅配0.5核,GOMAXPROCS=8将引发大量P抢占与调度抖动。

GC策略调优对照表

环境类型 GOGC GOMEMLIMIT 适用场景
高吞吐API 50 80% container limit 减少GC频次
内存敏感批处理 20 显式设为512MiB 控制堆峰值

GC触发路径依赖图

graph TD
    A[Allocated Heap ≥ heapGoal] --> B{GOGC enabled?}
    B -->|Yes| C[触发Mark-Sweep]
    B -->|No| D[仅当达到GOMEMLIMIT才触发]
    C --> E[STW + 并发标记]
    D --> E

关键实践:启动时强制对齐——GOMAXPROCS=$(nproc --all) go run main.go需替换为GOMAXPROCS=$(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us | awk '{print int($1/100000)}')

2.2 验证HTTP Server配置(超时、连接池、Keep-Alive、TLS握手缓存)在高并发下的稳定性

关键参数压测对照表

参数 生产推荐值 高并发风险表现
read_timeout 5s 超时堆积导致连接耗尽
max_idle_conns 200 连接复用率下降30%+
tls_handshake_cache_size 10000 TLS握手延迟上升400ms

Keep-Alive与连接池联动验证

srv := &http.Server{
    Addr: ":8080",
    ReadTimeout:  5 * time.Second,
    IdleTimeout:  30 * time.Second, // 必须 ≥ Keep-Alive timeout
    MaxIdleConns: 200,
    TLSConfig: &tls.Config{
        ClientSessionCache: tls.NewLRUClientSessionCache(10000),
    },
}

逻辑分析:IdleTimeout需严格大于客户端Keep-Alive: timeout=30,否则服务端提前关闭空闲连接,触发客户端重连风暴;ClientSessionCache缓存TLS会话票证,避免每请求重复握手。

TLS握手缓存效果对比流程

graph TD
    A[新连接请求] --> B{是否存在有效session ticket?}
    B -->|是| C[复用密钥材料,跳过完整握手]
    B -->|否| D[执行完整1-RTT握手]
    C --> E[建立连接耗时≈0.8ms]
    D --> F[建立连接耗时≈4.2ms]

2.3 检查依赖服务熔断/降级开关状态及下游响应SLA是否匹配压测目标

熔断开关实时探活

通过 HTTP 健康端点批量拉取各依赖服务的熔断器状态:

curl -s "http://svc-order:8080/actuator/resilience4j" | jq '.circuitBreakers."order-service".state'
# 输出示例: "OPEN" / "CLOSED" / "HALF_OPEN"

该命令调用 Resilience4j Actuator 端点,解析 circuitBreakers 下指定实例的 state 字段,用于判断是否处于主动熔断态,避免压测流量误入已隔离链路。

SLA 匹配校验表

依赖服务 压测目标 P95 当前生产 P95 状态
user-svc 120 98 ✅ 合规
pay-svc 200 245 ❌ 超标

自动化校验流程

graph TD
    A[获取压测SLA阈值] --> B[查询下游服务Metrics API]
    B --> C{P95 ≤ 目标?}
    C -->|是| D[标记为就绪]
    C -->|否| E[触发告警并暂停压测]

2.4 审计日志级别与采样率——避免INFO级全量打点引发I/O雪崩与内存泄漏

INFO级审计日志若无节制输出,将迅速耗尽磁盘I/O带宽并堆积未GC的StringBuilder对象,触发Full GC频发。

常见误配模式

  • 全链路请求ID、参数、响应体统一打为INFO
  • 日志框架未配置异步Appender(如Logback的AsyncAppender
  • 缺失采样开关,高QPS场景下日志量呈线性爆炸

合理分级策略

级别 触发条件 示例
DEBUG 开发环境+TraceID白名单 if (isDebug() && inTraceWhitelist())
INFO 关键状态变更(登录/支付成功) log.info("order_paid, order_id={}", id);
WARN 业务异常但可降级 log.warn("cache_miss_fallback", e);
// 动态采样:按TraceID哈希取模,仅1%流量全量审计
if (Math.abs(Objects.hashCode(traceId) % 100) == 0) {
    auditLogger.info("full_audit: {}", sensitiveData); // 仅采样命中时序列化
}

该逻辑规避了toString()高频调用导致的临时字符串对象暴增;hashCode()确保同TraceID始终被一致采样,便于问题复现。

采样率决策流

graph TD
    A[QPS > 5000?] -->|是| B[启用动态采样]
    A -->|否| C[固定INFO级关键事件]
    B --> D[traceId % 100 == 0?]
    D -->|是| E[全量审计]
    D -->|否| F[仅记录traceId+status]

2.5 校准Prometheus指标采集端点(/metrics)、标签维度与直方图桶设置是否覆盖业务P99延迟区间

直方图桶需对齐真实延迟分布

默认 http_request_duration_seconds_bucket 的桶(如 0.005, 0.01, 0.025, ...)常无法覆盖业务P99(如 1.2s)。应基于历史Trace数据动态生成桶边界:

# prometheus.yml 中的直方图重写示例
- job_name: 'app'
  metrics_path: '/metrics'
  static_configs:
  - targets: ['app:8080']
  histogram_quantile:
    # 覆盖 P99=1.2s → 至少包含 1.5s 桶
    buckets: [0.01, 0.05, 0.1, 0.2, 0.5, 1.0, 1.5, 2.0, 5.0]

此配置确保 sum(rate(http_request_duration_seconds_bucket{le="1.5"}[5m])) / sum(rate(http_request_duration_seconds_count[5m])) 可准确逼近P99,避免因桶上限不足导致分位数高估。

标签维度精简策略

过度打标(如 user_id, request_id)引发高基数问题,应仅保留可聚合维度:

  • ✅ 必选:service, endpoint, status_code, method
  • ❌ 禁用:trace_id, user_ip, query_params

P99验证流程

graph TD
  A[采集1h延迟直方图] --> B[计算le=\"X\"占比]
  B --> C{≥99%?}
  C -->|否| D[扩大最大桶]
  C -->|是| E[锁定桶配置]
桶上限 P99实测值 是否达标
1.0s 98.3%
1.5s 99.6%

第三章:核心接口的压测场景建模方法论

3.1 基于真实流量镜像的请求分布建模(含QPS峰谷比、用户会话粘性、读写比例)

真实流量镜像不是简单复制请求包,而是保序、保时延、保上下文的轻量级复现。核心建模维度包括:

  • QPS峰谷比:反映系统负载波动强度,典型电商场景常达 8:1~12:1
  • 用户会话粘性:以 session_id 为键,统计跨请求的路径连续性(如 73% 请求在 5s 内续发)
  • 读写比例:生产环境常为 87:13(查多写少),但需按接口粒度校准

流量特征提取流程

# 从镜像日志提取会话级统计(单位:秒)
import pandas as pd
df = pd.read_json("mirror_trace.json")
df["ts"] = pd.to_datetime(df["timestamp"])
df = df.sort_values(["session_id", "ts"])
df["delta_t"] = df.groupby("session_id")["ts"].diff().dt.total_seconds()
# 粘性阈值设为 8s:同一 session 相邻请求 ≤8s 视为高粘性行为
df["is_sticky"] = df["delta_t"] <= 8

该代码计算会话内请求时间间隔,delta_t 是关键粘性指标;is_sticky 用于后续聚类分析。

关键指标对照表

指标 生产实测均值 压测常用假设 偏差风险
QPS峰谷比 9.4:1 5:1 容器扩缩容滞后
读写比例 86.7:13.3 90:10 写操作瓶颈被掩盖
会话粘性率 72.1% 无建模 连接池复用失效
graph TD
    A[原始镜像流量] --> B[按 session_id 聚合]
    B --> C[计算 delta_t & request_type]
    C --> D[拟合峰谷周期/粘性分布/读写熵]
    D --> E[生成参数化负载模型]

3.2 关键路径链路压测设计(从API网关→Auth中间件→业务Handler→DB/Redis调用)

为精准定位全链路瓶颈,压测需穿透各依赖层并采集细粒度时序指标。

压测流量注入策略

  • 使用 k6 模拟阶梯式并发(10 → 500 → 2000 VU)
  • 每请求携带唯一 trace_id,贯穿网关、Auth、Handler、存储层

全链路埋点示例(Go Handler片段)

func businessHandler(w http.ResponseWriter, r *http.Request) {
    span := tracer.StartSpan("business_handler", 
        ext.SpanKindRPCServer,
        ext.TraceID(traceIDFromHeader(r)), // 复用网关透传ID
        ext.SamplingPriority(1))           // 强制采样
    defer span.Finish()

    // 调用Redis(带超时与重试)
    ctx, cancel := context.WithTimeout(r.Context(), 200*time.Millisecond)
    defer cancel()
    val, err := redisClient.Get(ctx, "user:123").Result()
}

逻辑分析ext.TraceID() 确保链路ID继承;SamplingPriority(1) 避免高并发下采样丢失关键慢请求;200ms 超时值基于P99 DB响应时间设定,防止雪崩扩散。

各环节SLA目标与观测维度

组件 P95延迟 错误率阈值 关键指标
API网关 ≤80ms TLS握手耗时、WAF规则匹配耗时
Auth中间件 ≤30ms JWT解析耗时、RBAC鉴权耗时
Redis调用 ≤15ms 连接池等待时间、网络RTT
graph TD
    A[API网关] -->|trace_id+auth_header| B[Auth中间件]
    B -->|context.WithValue| C[业务Handler]
    C -->|ctx.WithTimeout| D[Redis/DB]
    C -->|fallback| E[本地缓存]

3.3 异常注入测试:模拟下游超时、5xx错误、网络分区对Go HTTP Client重试与context取消传播的影响

模拟下游超时场景

使用 httptest.Server 配合 time.Sleep 注入延迟,触发 context.DeadlineExceeded

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    time.Sleep(3 * time.Second) // 超出 client timeout(2s)
    w.WriteHeader(http.StatusOK)
}))

逻辑分析:客户端设置 context.WithTimeout(ctx, 2*time.Second),服务端故意延迟3秒,强制触发 net/http 底层的 context.Canceled 错误,验证 RoundTrip 是否提前中止并透传取消信号。

重试策略与错误分类响应

异常类型 是否重试 原因
500, 503 可恢复服务端错误
502, 504 网关/超时类,适合重试
context.Canceled 客户端主动取消,不可重试

取消传播链路

graph TD
    A[User calls Do with ctx] --> B[http.Transport.RoundTrip]
    B --> C{ctx.Done() fired?}
    C -->|Yes| D[Return error: context.Canceled]
    C -->|No| E[Proceed to dial]

第四章:Locust+Go生态协同压测实战

4.1 编写Go原生HTTP Client性能基准脚本(对比net/http vs fasthttp,验证协程调度瓶颈)

为精准定位高并发场景下的调度瓶颈,我们构建轻量级基准测试框架,聚焦 net/httpfasthttp 的协程资源消耗差异。

测试设计要点

  • 使用 gomaxprocs=1 固定调度器线程数,排除多P干扰
  • 并发梯度:100 → 500 → 1000 goroutines
  • 每轮预热 2 秒,采样 10 秒有效吞吐量

核心基准代码(简化版)

func benchmarkNetHTTP(url string, conc int) float64 {
    req, _ := http.NewRequest("GET", url, nil)
    var wg sync.WaitGroup
    start := time.Now()
    for i := 0; i < conc; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            http.DefaultClient.Do(req)
        }()
    }
    wg.Wait()
    return float64(conc) / time.Since(start).Seconds()
}

逻辑分析:http.DefaultClient.Do() 内部隐式分配 *http.Responsebufio.Reader,每请求独占 goroutine + 堆内存;conc=1000 时触发 runtime.scheduler 频繁抢占,可观测 G-P-M 协程切换延迟上升。

性能对比(QPS,单核,localhost:8080)

100并发 500并发 1000并发
net/http 12.4k 9.1k 5.3k
fasthttp 28.7k 27.2k 26.9k

调度瓶颈可视化

graph TD
    A[goroutine 创建] --> B{net/http:阻塞IO+堆分配}
    B --> C[GC压力↑ → STW频次↑]
    C --> D[Scheduler M阻塞等待网络]
    A --> E{fasthttp:复用bytebuf+无GC路径}
    E --> F[goroutine轻量化 → P高效复用]

4.2 Locust TaskSet中集成Go编译的C-ABI共享库实现高保真加密/签名逻辑压测

为保障压测场景与生产环境加密行为严格一致,需在Locust TaskSet中调用原生级密码学逻辑。Go 1.21+ 支持 //export 语法生成符合 C ABI 的共享库:

// signlib.go
package main

import "C"
import (
    "crypto/ecdsa"
    "crypto/rand"
    "golang.org/x/crypto/sha3"
)

//export SignECDSAKeccak256
func SignECDSAKeccak256(data *C.uchar, dataLen C.size_t, privKeyBytes *C.uchar) *C.uchar {
    // ... 实际签名逻辑(省略密钥解析与签名步骤)
    return C.CBytes(signatureBytes) // 返回堆分配内存,需由Python侧free
}

逻辑分析:该函数接收原始字节流与私钥字节数组,使用 keccak256 + secp256k1 执行签名;C.CBytes 分配 C 堆内存,避免 Go GC 干预,但要求 Python 显式调用 libc.free()

集成关键点

  • Locust Worker 进程需通过 ctypes.CDLL 加载 .so 文件
  • 使用 ctypes.create_string_buffer 构造输入缓冲区
  • 签名结果指针须配合 ctypes.cast(..., ctypes.POINTER(ctypes.c_ubyte)) 解析

性能对比(单次签名耗时,单位:μs)

实现方式 平均延迟 标准差
Python cryptography 82.4 ±9.1
Go C-ABI shared lib 23.7 ±1.3
graph TD
    A[TaskSet.run] --> B[ctypes.CDLL.Load]
    B --> C[prepare input buffers]
    C --> D[call SignECDSAKeccak256]
    D --> E[cast & copy result]
    E --> F[free C-allocated memory]

4.3 Prometheus自定义Exporter开发:暴露Go runtime指标(goroutines、heap_alloc、gc_pause_ns)供Locust实时关联分析

核心指标选择依据

Go runtime 包提供低开销、高时效的运行时观测数据,其中:

  • runtime.NumGoroutine() → 反映并发负载压力
  • runtime.ReadMemStats()HeapAlloc → 内存分配速率瓶颈信号
  • debug.ReadGCStats().PauseNs → GC停顿时间分布(需取末尾最新值)

自定义Exporter实现(关键片段)

func init() {
    prometheus.MustRegister(
        goroutinesGauge,
        heapAllocGauge,
        gcPauseHist,
    )
}

var (
    goroutinesGauge = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "go_goroutines",
        Help: "Number of goroutines currently running",
    })
    heapAllocGauge = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "go_heap_alloc_bytes",
        Help: "Bytes of allocated heap objects",
    })
    gcPauseHist = prometheus.NewHistogram(prometheus.HistogramOpts{
        Name:    "go_gc_pause_ns",
        Help:    "Distribution of GC pause durations (nanoseconds)",
        Buckets: prometheus.ExponentialBuckets(1e4, 2, 12), // 10μs ~ 20ms
    })
}

func collectRuntimeMetrics() {
    goroutinesGauge.Set(float64(runtime.NumGoroutine()))

    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    heapAllocGauge.Set(float64(m.HeapAlloc))

    var stats debug.GCStats
    stats.PauseQuantiles = make([]time.Duration, 1) // only need latest
    debug.ReadGCStats(&stats)
    if len(stats.PauseNs) > 0 {
        gcPauseHist.Observe(float64(stats.PauseNs[len(stats.PauseNs)-1]))
    }
}

逻辑说明collectRuntimeMetrics 每次调用均采集瞬时快照;gcPauseHist 使用 Histogram 而非 Gauge,因停顿具有分布特性,支持 Locust 压测中按 P95/P99 关联请求延迟分析;ExponentialBuckets 覆盖典型 GC 停顿量级,避免直方图桶稀疏。

Locust 关联分析示意

Locust 指标 关联的 Go Runtime 指标 分析目标
response_time_p95 go_gc_pause_ns{quantile="0.95"} 判断 GC 是否拖慢关键请求
users go_goroutines 验证并发用户数与协程增长线性度
fail_ratio go_heap_alloc_bytes 检测内存泄漏引发 OOM 前兆
graph TD
    A[Locust压测启动] --> B[每秒上报 metrics]
    B --> C[Prometheus scrape /metrics]
    C --> D[PromQL join on timestamp]
    D --> E[Dashboard联动:goroutines vs rps]
    D --> F[Alert:gc_pause_ns > 5ms for 30s]

4.4 基于Grafana+Prometheus构建压测看板:动态追踪pprof火焰图触发阈值与goroutine泄漏拐点

核心监控指标设计

需采集三类关键指标:

  • go_goroutines(实时协程数)
  • process_cpu_seconds_total(CPU累积耗时)
  • 自定义 http_request_duration_seconds_bucket(P95延迟)

动态阈值告警规则(Prometheus Rule)

- alert: GoroutineLeakDetected
  expr: >
    (go_goroutines{job="api-server"}[10m]) 
    - (go_goroutines{job="api-server"}[10m] offset 5m) > 500
  for: 2m
  labels:
    severity: warning
  annotations:
    summary: "Goroutine count increased by >500 in 5min — possible leak"

逻辑说明:使用offset实现滑动差分,捕获5分钟内协程净增长;>500为压测场景下经验拐点阈值,避免毛刺误报。

Grafana 火焰图联动机制

面板元素 数据源 触发条件
Goroutine趋势图 Prometheus P95延迟 > 800ms + goroutines > 3k
pprof火焰图iframe nginx反向代理到/pprof 自动拼接 /debug/pprof/goroutine?debug=2

自动化采集流程

graph TD
    A[Prometheus scrape] --> B{go_goroutines > 2500?}
    B -->|Yes| C[调用curl -s http://svc:6060/debug/pprof/goroutine?debug=2]
    C --> D[保存为timestamp.svg]
    D --> E[Grafana iframe动态加载]

第五章:压测报告解读与上线决策红线

关键指标阈值对照表

压测报告不是数据堆砌,而是决策依据。以下是某电商大促前压测中必须校验的硬性阈值(单位:毫秒/次/百分比):

指标类别 服务类型 P95响应时间 错误率上限 平均TPS CPU使用率(单节点)
订单创建接口 核心链路 ≤ 320ms ≤ 0.1% ≥ 1800 ≤ 75%
商品详情页 高频读取 ≤ 180ms ≤ 0.05% ≥ 4200 ≤ 65%
库存扣减服务 强一致性 ≤ 260ms ≤ 0.2% ≥ 950 ≤ 80%
用户登录接口 安全前置 ≤ 450ms ≤ 0.3% ≥ 2100 ≤ 70%

异常模式识别信号

压测中出现以下任意一种现象,即触发“暂停上线”熔断机制:

  • 连续3个5分钟窗口内,P99延迟突增超基准值120%,且伴随GC pause > 500ms;
  • Redis连接池耗尽率持续≥95%达2分钟以上,同时缓存击穿请求占比>15%;
  • 数据库慢查询数量在压测峰值期每分钟>8条,其中含未走索引的UPDATE语句;
  • 线程池拒绝队列堆积深度突破corePoolSize × 3,且RejectedExecutionException日志每秒≥2条。

红线决策树(Mermaid流程图)

flowchart TD
    A[压测结束] --> B{P95响应时间达标?}
    B -->|否| C[立即终止上线流程]
    B -->|是| D{错误率≤阈值?}
    D -->|否| C
    D -->|是| E{数据库慢查≤8条/分钟?}
    E -->|否| C
    E -->|是| F{JVM Full GC间隔>30分钟?}
    F -->|否| C
    F -->|是| G[进入灰度发布阶段]

真实故障回溯案例

2023年某支付网关上线前压测显示TPS达标(2300),但忽略了一个关键细节:压测脚本未模拟“重复提交幂等校验”逻辑。实际生产中,因前端重试机制触发高频重复请求,导致Redis分布式锁竞争激增,SETNX失败率从0.02%飙升至37%,订单状态不一致率达1.8%。复盘发现,压测报告中redis_cmd_failed_rate指标被归类为“低优先级监控项”,未纳入决策红线清单。

日志与链路协同验证法

仅看聚合指标存在盲区。必须交叉验证三类原始数据:

  • SkyWalking链路追踪中/order/create入口Span的peer.service标签是否全部指向真实下游服务(排除Mock服务混入);
  • ELK中提取ERROR级别日志,筛选含DeadlockLoserDataAccessException关键词的堆栈,确认数据库死锁发生时段与压测峰值重合度;
  • Prometheus中process_open_fds指标在压测后期是否逼近fs.file-max的90%,该信号曾提前23分钟预警某文件描述符泄漏问题。

红线文档签署机制

所有参与方需在《上线决策确认书》电子签章栏完成强制动作:

  • 架构师确认“熔断策略已注入Service Mesh控制面”;
  • DBA签字“主从延迟监控已接入Zabbix,阈值设为800ms”;
  • SRE勾选“全链路日志采样率已调至100%,保留7天”;
  • 测试负责人上传压测原始JTL文件哈希值至Git LFS并附校验命令:
    sha256sum jmeter-results-20240522.jtl

记录 Go 学习与使用中的点滴,温故而知新。

发表回复

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