Posted in

Go语言网络编程性能基线测试(wrk+go-wrk+ghz三工具交叉验证):建立你团队的SLA黄金指标看板

第一章:Go语言网络编程性能基线测试的工程意义与SLA黄金指标体系构建

在云原生与微服务架构深度落地的背景下,Go语言因其轻量协程、零成本抽象和静态编译等特性,成为高并发网络服务的首选实现语言。然而,性能优势不等于生产就绪——缺乏可复现、可度量、可追溯的基线测试体系,将导致容量规划失准、故障归因困难、SLA承诺空洞化。

基线测试的本质是建立“可控环境下的确定性参照系”。它要求在隔离的硬件资源(如4核8G裸机或严格配额的容器)、固定Go版本(建议1.21+)及标准运行时参数(GOMAXPROCS=4, GODEBUG=schedulertrace=0)下,对典型网络工作负载(HTTP/1.1短连接、gRPC流式调用、TCP长连接心跳)执行多轮压测,采集稳定态下的核心指标。

SLA黄金指标的定义与采集逻辑

必须同时监控四维正交指标:

  • 吞吐量(TPS/QPS):单位时间成功处理请求数,反映系统最大承载能力;
  • P99延迟(ms):保障99%请求的响应时效,规避平均值掩盖尾部毛刺;
  • 错误率(%):HTTP 4xx/5xx 或 gRPC status code 非OK比例,表征服务健康水位;
  • 资源饱和度go:linkname 调用 runtime.ReadMemStats 获取 GCSysNumGC,结合 top -b -n1 | grep "go_" 判断GC压力是否引发延迟抖动。

基线测试自动化脚本示例

# 使用wrk进行HTTP基线压测(10秒预热 + 60秒采样)
wrk -t4 -c100 -d60s --latency \
    -s ./scripts/http-baseline.lua \
    http://localhost:8080/ping

其中 http-baseline.lua 需注入请求头 X-Baseline-Mode: true 并记录Go运行时指标(通过 /debug/pprof/ 接口定时抓取)。每次测试后,生成含时间戳的JSON报告,字段包括:{"timestamp":"2024-06-15T14:23:00Z","qps":4217.3,"p99_ms":24.6,"error_rate":0.02,"gc_pause_p95_ms":1.8}

黄金指标阈值设定原则

指标 生产级严苛阈值 容忍缓冲区间 触发告警条件
P99延迟 ≤30ms 30–50ms 连续3次采样超50ms
错误率 ≤0.1% 0.1–0.5% 单次采样≥0.5%
GC暂停P95 ≤2ms 2–5ms 每分钟GC次数>120次

该体系将SLA从模糊的“99.9%可用性”转化为可编程、可验证、可回滚的工程契约,使性能优化决策脱离经验主义,转向数据驱动的持续精进。

第二章:三大压测工具原理剖析与Go原生适配实践

2.1 wrk高并发事件驱动模型与Go服务端协同调优策略

wrk 基于 epoll/kqueue 实现事件驱动,单线程可维持数万连接;Go 服务端则依赖 net/http 的 goroutine-per-connection 模型。二者协同失配时易引发 goroutine 泄漏或系统调用抖动。

数据同步机制

为降低上下文切换开销,建议 Go 服务启用 GOMAXPROCS=runtime.NumCPU() 并禁用 HTTP/2(避免 wrk 的 pipeline 模式干扰):

# 启动 wrk,复用连接并限制并发粒度
wrk -t4 -c400 -d30s --latency http://localhost:8080/api \
    -H "Connection: keep-alive" \
    -H "Accept: application/json"

-t4 匹配 CPU 核心数,避免线程争抢;-c400 控制连接池规模,防止 Go 侧 net/http.Server.MaxConns 触发拒绝;keep-alive 减少 TCP 握手,对齐 Go 的连接复用逻辑。

调优参数对照表

维度 wrk 配置项 Go 服务对应参数 协同影响
连接复用 -H "Connection: keep-alive" http.Transport.MaxIdleConnsPerHost = 100 避免频繁建连导致 TIME_WAIT 暴涨
并发压力控制 -t4 -c400 GOMAXPROCS=4, http.Server.ReadTimeout=5s 对齐调度粒度,防 goroutine 积压

性能瓶颈定位流程

graph TD
    A[wrk 发起请求] --> B{连接是否复用?}
    B -->|否| C[Go 新启 goroutine + TCP 握手]
    B -->|是| D[Go 复用 conn + 读缓冲复用]
    C --> E[TIME_WAIT 爆增 → 端口耗尽]
    D --> F[goroutine 数稳定 → QPS 提升 3.2x]

2.2 go-wrk源码级定制:基于net/http/httptest的零依赖压测通道构建

传统 go-wrk 依赖真实 TCP 连接,引入网络栈开销与环境不确定性。我们剥离 net 层,直连 net/http/httptest.ServerHandler 接口,实现内存内请求闭环。

零依赖通道核心改造

// 替换原 dialer:不走 TCP,直接调用 handler.ServeHTTP
func (c *Client) doInMemory(req *http.Request) *http.Response {
    rw := httptest.NewRecorder()
    c.handler.ServeHTTP(rw, req) // 零延迟、无 socket、无 TLS 握手
    return rw.Result()
}

逻辑分析:httptest.NewRecorder() 模拟响应写入器,ServeHTTP 同步执行 Handler 逻辑(如 Gin/Echo 路由),全程在 goroutine 内存中完成;req 可复用并预设 BodyHeader 等字段,规避序列化瓶颈。

性能对比(10K 并发 GET /ping)

指标 原生 go-wrk 零依赖通道
P99 延迟 12.4 ms 0.38 ms
QPS 42,100 186,500
graph TD
    A[go-wrk 启动] --> B[构造 http.Request]
    B --> C[调用 handler.ServeHTTP]
    C --> D[httptest.ResponseWriter 捕获输出]
    D --> E[解析状态码/耗时/Body]

2.3 ghz gRPC协议栈深度验证:HTTP/2流控参数与Go net/http.Server配置映射

gRPC over HTTP/2 的性能边界常由流控(Flow Control)机制决定,而 Go 标准库 net/http.Server 并未直接暴露 HTTP/2 级别参数,需通过 http2.Server 显式配置。

关键配置映射关系

HTTP/2 协议参数 Go http2.Server 字段 默认值 影响维度
SETTINGS_INITIAL_WINDOW_SIZE InitialWindowSize 65535 单流接收窗口
SETTINGS_MAX_CONCURRENT_STREAMS MaxConcurrentStreams 1000 连接级并发流上限
SETTINGS_HEADER_TABLE_SIZE MaxHeaderListSize 0(不限) HPACK 解码内存

初始化带调优的 gRPC Server

srv := &http.Server{
    Addr: ":8080",
    Handler: grpcHandler,
}
// 启用并定制 HTTP/2 server
h2s := &http2.Server{
    MaxConcurrentStreams: 2000,
    InitialWindowSize:    2 << 17, // 256KB,提升大消息吞吐
    MaxHeaderListSize:    1048576, // 1MB header 支持
}
http2.ConfigureServer(srv, h2s)

此配置将单连接流并发能力提升至 2000,初始流窗口扩大 4 倍,显著缓解高吞吐下 STREAM_ERROR (FLOW_CONTROL_ERROR) 频发问题。InitialWindowSize 直接映射至每个新流的 WINDOW_UPDATE 初始信用额度,避免小窗口导致频繁阻塞。

流控协同时序示意

graph TD
    A[Client Send HEADERS] --> B[Server ACK + SETTINGS]
    B --> C[Server sets InitialWindowSize=256KB]
    C --> D[Client sends DATA with window credit]
    D --> E[Server consumes → sends WINDOW_UPDATE if needed]

2.4 三工具统一指标对齐:延迟分布(p50/p90/p99)、吞吐拐点、连接复用率一致性校验

为保障 APM(Arthas)、日志分析(Loki+Promtail)与链路追踪(Jaeger)三工具观测结论一致,需建立跨系统指标对齐机制。

数据同步机制

通过 OpenTelemetry Collector 统一接收并标准化指标:

# otel-collector-config.yaml
processors:
  metricstransform:
    transforms:
      - include: http.server.request.duration
        action: update
        new_name: "http_latency_ms"  # 统一命名
        operations:
          - action: add_label
            new_label: quantile
            new_value: "p99"  # 标准化分位标识

逻辑说明:metricstransform 强制重命名与打标,确保 p50/p90/p99 在各后端(Prometheus/Loki/Jaeger backend)解析时语义一致;quantile 标签值限定为规范枚举,避免 0.99/p99/99th 等异构表达。

对齐验证维度

指标类型 校验方式 容忍偏差
延迟分位值 同一 traceID 下三工具 p99 差值 ≤ 15ms
吞吐拐点 并发阶梯压测中 QPS 骤降点位置误差 ≤ 2档
连接复用率 Netty active connections / total requests ≥ 0.85

自动化校验流程

graph TD
  A[定时采集三源指标] --> B{p99延迟差 >15ms?}
  B -->|是| C[触发告警+比对trace采样]
  B -->|否| D[检查吞吐拐点偏移]
  D --> E[验证连接复用率是否持续≥0.85]

2.5 Go运行时指标注入:pprof+expvar+otel-trace三维度实时关联采样方案

为实现运行时性能可观测性的深度协同,需打通 pprof(CPU/heap profile)、expvar(自定义指标)与 otel-trace(分布式链路)三类信号的上下文绑定。

数据同步机制

通过 runtime.SetFinalizer + context.WithValue 在 trace span 创建时注入 pprof.Labelsexpvar 命名空间前缀,确保采样事件可跨组件追溯。

关键代码注入点

// 在 HTTP handler 入口统一注入关联标识
func tracedHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        span := trace.SpanFromContext(ctx)
        id := span.SpanContext().TraceID().String() // 用 TraceID 作全局锚点

        // 同步注入 pprof 标签(支持 runtime/pprof.StartCPUProfile 过滤)
        r = r.WithContext(pprof.WithLabels(ctx, pprof.Labels("trace_id", id)))

        // 注册 expvar 计数器(带 trace_id 维度)
        expvar.NewInt("req_count_" + id).Add(1)

        next.ServeHTTP(w, r)
    })
}

逻辑说明pprof.WithLabels 将 trace ID 注入 goroutine 标签,使 pprof.Lookup("goroutine").WriteTo() 输出可筛选;expvar.NewInt("req_count_"+id) 动态创建 trace 粒度计数器,避免预定义爆炸。id 作为三系统共用关联键,实现采样对齐。

三系统关联能力对比

维度 pprof expvar OTel-Trace
采样粒度 Goroutine/Heap Key-value 指标 Span + Context
关联锚点 pprof.Labels trace_id 命名前缀 SpanContext.TraceID
实时性 需显式 Start/Stop 即时更新 异步导出(
graph TD
    A[HTTP Request] --> B[Inject trace_id]
    B --> C[pprof: Label-based profiling]
    B --> D[expvar: trace_id-scoped counter]
    B --> E[OTel: Span with same TraceID]
    C & D & E --> F[统一查询:trace_id=abc123]

第三章:Go HTTP服务性能瓶颈建模与基准场景设计

3.1 CPU-bound vs I/O-bound服务的RTT-TPS双曲线特征识别

CPU-bound与I/O-bound服务在压测中呈现截然不同的RTT-TPS联合分布形态:前者RTT随TPS线性攀升,后者在TPS激增时RTT陡升并伴随显著抖动。

RTT-TPS双曲线典型形态

服务类型 RTT变化趋势 TPS饱和点特征 主要瓶颈
CPU-bound 近似线性增长 平缓拐点 指令执行/缓存争用
I/O-bound 指数级跃升+毛刺 尖锐拐点+平台坍塌 网络延迟/磁盘队列

压测数据拟合代码示例

# 使用双曲线模型 y = a/(x-b) + c 拟合RTT-TPS关系
from scipy.optimize import curve_fit
def hyperbola(x, a, b, c):
    return a / (x - b) + c  # b为理论TPS上限(渐近线)
popt, _ = curve_fit(hyperbola, tps_data, rtt_data, p0=[100, 95, 5])
# a: 曲率强度;b: 预估最大吞吐(I/O-bound通常b更小且置信区间宽)

该拟合通过渐近线横坐标b量化系统吞吐天花板,a值大小反映延迟敏感度——I/O-bound服务a常达CPU-bound的3–5倍。

graph TD
    A[原始RTT-TPS散点] --> B{拟合残差分析}
    B -->|残差正态分布| C[符合双曲线假设]
    B -->|残差周期性震荡| D[存在I/O调度干扰]
    C --> E[提取b值判断瓶颈类型]

3.2 连接池、TLS握手、Goroutine泄漏在压测中的可观测性信号提取

在高并发压测中,三类底层行为会暴露关键性能瓶颈:

  • 连接池耗尽http.DefaultTransport.MaxIdleConnsPerHost 设置过低导致请求排队;
  • TLS握手延迟ClientHelloFinished 耗时 >150ms 常见于证书链验证或 OCSP Stapling 失败;
  • Goroutine 泄漏:未关闭的 http.Response.Bodytime.Timer 持续累积。

关键指标采集示例

// 启用 HTTP client 级别指标埋点
client := &http.Client{
    Transport: &http.Transport{
        MaxIdleConnsPerHost: 100,
        TLSHandshakeTimeout: 5 * time.Second,
        // 启用连接池统计(需自定义 RoundTripper 包装)
    },
}

该配置限制单主机最大空闲连接数,避免端口耗尽;TLSHandshakeTimeout 防止 handshake 卡死阻塞整个 goroutine。实际压测中应结合 net/http/pprofruntime.NumGoroutine() 差分监控。

TLS 握手阶段可观测信号对照表

阶段 正常耗时 异常信号
ClientHello → ServerHello >100ms → 服务端负载过高或 SNI 不匹配
Certificate Verify >200ms → CA 根证书缺失或 OCSP 响应慢

Goroutine 泄漏检测流程

graph TD
    A[压测启动] --> B[记录 goroutines 初始快照]
    B --> C[每10s采集 runtime.GoroutineProfile]
    C --> D[比对堆栈中 http.readLoop / timerproc 数量趋势]
    D --> E{持续增长?}
    E -->|是| F[定位未 defer resp.Body.Close()]
    E -->|否| G[正常]

3.3 基于go tool trace的调度器阻塞链路反向定位实践

当 Goroutine 长时间处于 GwaitingGrunnable 状态却无法被调度执行时,需借助 go tool trace 反向追溯阻塞源头。

启动带追踪的程序

go run -gcflags="-l" -trace=trace.out main.go

-gcflags="-l" 禁用内联,确保函数调用栈完整;-trace 输出二进制追踪数据,供后续分析。

解析与可视化

go tool trace trace.out

自动打开 Web UI(http://127.0.0.1:XXXX),在 “Goroutine analysis” → “Blocked goroutines” 中筛选高延迟阻塞事件。

关键阻塞类型对照表

阻塞原因 trace 中状态标志 典型场景
系统调用等待 Syscall os.ReadFile, net.Conn.Read
通道收发阻塞 ChanSend, ChanRecv 无缓冲 channel 写入未读
锁竞争 MutexLock sync.Mutex.Lock() 未释放

反向定位流程

graph TD
    A[trace.out] --> B[go tool trace UI]
    B --> C{定位阻塞 Goroutine}
    C --> D[查看其 P/G/M 状态变迁]
    D --> E[回溯前序事件:谁唤醒它?谁持有锁/通道?]
    E --> F[定位到阻塞上游 Goroutine 或系统调用]

第四章:SLA黄金指标看板落地工程化路径

4.1 Prometheus+Grafana指标管道:从wrk JSON输出到时序数据库的ETL标准化

wrk 输出解析与结构化转换

wrk 生成的 JSON 报告包含嵌套的 latency, requests, errors 等字段,需扁平化为 Prometheus 可采集的指标格式:

# 示例 wrk 输出片段(经 jq 提取关键路径)
wrk -t4 -c128 -d30s http://localhost:8080/api | \
  jq -r '{ 
    reqs_total: .requests.total,
    latency_p99_ms: (.latency.p99 / 1000),
    errors_total: .errors.total
  }' | jq -r 'to_entries[] | "\(.key) \(.value)"' > metrics.prom

该命令将原始 JSON 转为 Prometheus 文本格式(如 reqs_total 12487),并支持直接被 node_exportertextfile_collector 加载。

ETL 流程编排

graph TD
  A[wrk JSON] --> B[jq 清洗/聚合]
  B --> C[Prometheus Pushgateway]
  C --> D[Prometheus Server scrape]
  D --> E[Grafana 查询展示]

标准化字段映射表

wrk 字段 Prometheus 指标名 类型 单位
requests.total http_load_reqs_total Counter requests
latency.p99 http_load_latency_p99_ms Gauge milliseconds
errors.timeout http_load_errors_timeout_total Counter count

4.2 基于go-wrk结果的自动化基线比对:GitOps驱动的性能回归门禁机制

数据同步机制

每次 CI 流水线执行 go-wrk 后,原始 JSON 报告自动推送至 Git 仓库的 perf-baselines/ 目录(按分支+环境命名),由 Argo CD 监听变更并触发比对服务。

回归门禁流程

graph TD
    A[CI 生成 go-wrk 报告] --> B[Git commit baseline.json]
    B --> C[Argo CD 检测变更]
    C --> D[启动 compare-perf 服务]
    D --> E[对比 delta > 5%?]
    E -->|是| F[拒绝合并 + 注释 PR]
    E -->|否| G[标记通过]

核心比对逻辑(Go 片段)

// compare.go:基于 P95 延迟与 RPS 双维度阈值校验
if abs(curr.RPS - base.RPS) / base.RPS > 0.05 || 
   curr.Latency.P95 > base.Latency.P95*1.05 {
    return errors.New("performance regression detected")
}

curr/base 为结构化解析后的 go-wrk 输出;0.05 为可配置的相对偏差阈值,通过 ConfigMap 注入。

门禁策略配置表

指标 基线来源 容忍阈值 触发动作
P95 Latency main 分支最新报告 +5% PR 评论+阻断
RPS tag/v1.2.0 报告 -3% 自动创建 Issue

4.3 ghz压测报告嵌入CI/CD流水线:gRPC服务契约符合性自动校验

ghz 压测结果与 gRPC 接口契约(.proto)联动校验,可拦截性能退化与语义不一致双重风险。

核心校验流程

graph TD
  A[CI触发] --> B[编译proto生成client]
  B --> C[执行ghz压测]
  C --> D[解析JSON报告+proto反射]
  D --> E[比对QPS/latency阈值 & 字段非空率]

自动化校验脚本片段

# 检查关键字段99%响应内≤200ms,且status_code=0占比≥99.5%
ghz --insecure --proto ./api.proto --call pb.User/Get -d '{"id":"u1"}' \
  --rps 100 --duration 30s --format json > report.json

jq -e '(.summary.latencyDistribution[3].value <= 200) and 
       (.summary.successRate >= 99.5)' report.json

--rps 100 模拟稳定负载;latencyDistribution[3] 对应99th百分位延迟;successRate 由 gRPC status code 统计得出。

校验维度对照表

维度 契约依据 压测指标来源 阈值示例
字段必填性 required 标签 响应体JSON Schema 100%非空
时延合规性 SLA文档 ghz latencyDistribution ≤200ms@p99
吞吐稳定性 QPS基线 ghz rps_actual 波动±5%

4.4 多环境(dev/staging/prod)性能衰减归因分析看板:GC Pause、Netpoll Wait、Syscall Latency交叉钻取

当跨环境对比延迟毛刺时,单一指标易掩盖根因。我们构建三维热力钻取视图,联动 gcpause_usnetpoll_wait_mssyscall_latency_p99_us 三列时序标签。

数据同步机制

Prometheus Remote Write + Thanos Sidecar 实现跨集群标签对齐,关键在于统一 envservicepod_template_hash 三元组。

核心查询片段(带注释)

# 联合下钻:定位 staging 中 GC pause > 5ms 且 syscall p99 > 10ms 的时段
sum by (env, job) (
  rate(go_gc_pause_total_seconds_sum{env=~"staging|prod"}[1h])
  * on(job, instance) group_left(env)
  (histogram_quantile(0.99, 
    sum by (job, le) (rate(node_system_call_latency_seconds_bucket{env="staging"}[1h]))))
) > 0.005

此查询将 GC 暂停总耗时率(秒/秒)与系统调用 P99 延迟做笛卡尔关联;group_left(env) 强制保留环境维度,避免 label mismatch;阈值 0.005 即 5ms 等效暂停占比。

归因路径示意

graph TD
  A[Prod 响应 P99 ↑30%] --> B{交叉筛选}
  B --> C[GC Pause ↑]
  B --> D[Netpoll Wait ↑]
  B --> E[Syscall Latency ↑]
  C --> F[内存分配激增 → GOGC=100]
  D --> G[epoll_wait 阻塞 → 连接未复用]
  E --> H[ext4 write latency spike → 磁盘 I/O 饱和]
环境 GC Pause P95 (ms) Netpoll Wait P95 (ms) Syscall P99 (ms)
dev 0.8 0.2 1.1
staging 3.2 1.7 4.9
prod 6.5 4.3 12.6

第五章:面向云原生演进的Go网络性能治理新范式

从单体服务到Service Mesh的连接治理跃迁

某头部电商在迁移核心订单服务至Kubernetes集群后,遭遇gRPC调用P99延迟突增300ms的问题。经eBPF追踪发现,iptables链路拦截导致TLS握手耗时激增。团队将istio-proxy升级至1.21并启用ENABLE_ENVOY_DRAINING=true,同时在Go客户端注入grpc.WithKeepaliveParams(keepalive.ClientParameters{Time: 30 * time.Second}),使长连接复用率从42%提升至89%,P99延迟回落至47ms。

基于OpenTelemetry的实时指标闭环体系

构建Go应用内嵌OTLP exporter,采集http.server.durationgo_goroutinesruntime/heap_alloc_bytes三类核心指标,通过Prometheus Remote Write直传至云厂商托管TSDB。当go_goroutines持续高于5000且http.server.duration P95 > 200ms时,自动触发告警并执行以下操作:

  • 调用pprof接口抓取goroutine dump
  • 执行kubectl exec -it <pod> -- go tool pprof -svg http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutine.svg
  • 将SVG文件上传至对象存储并推送诊断链接至企业微信机器人

面向弹性伸缩的连接池动态调优策略

场景 MaxIdleConns MaxIdleConnsPerHost IdleConnTimeout 实测效果
低峰期(QPS 20 10 30s 内存占用降37%
高峰期(QPS>5000) 200 100 90s 连接复用率92.4%
突发流量(+300%) 自动扩容至300 自动扩容至150 动态延长至120s 连接拒绝率

eBPF驱动的零侵入网络观测方案

采用cilium/ebpf库编写内核模块,捕获tcp_connecttcp_sendmsgtcp_retransmit_skb事件,生成如下mermaid流程图:

flowchart LR
    A[用户态Go程序] -->|syscall| B[eBPF kprobe tcp_connect]
    B --> C{连接建立成功?}
    C -->|是| D[记录connect_latency_us]
    C -->|否| E[记录connect_fail_reason]
    D --> F[关联PID/TID与容器标签]
    E --> F
    F --> G[推送到Loki日志流]

混沌工程验证下的熔断阈值校准

在生产环境灰度集群执行网络延迟注入实验:使用chaos-mesh配置NetworkChaos规则,模拟50ms固定延迟与15%丢包率。通过对比hystrix-gosony/gobreaker在相同负载下的表现,发现后者在连续失败率超过60%时响应更快。最终将熔断器错误阈值从默认50%调整为55%,超时时间从1000ms优化为min(1500, avg_rt*3)动态计算值。

WebAssembly边缘网关的轻量化协议处理

将HTTP/2帧解析逻辑编译为WASM模块,部署至Cloudflare Workers边缘节点。Go主服务仅处理业务逻辑,协议转换交由WASM运行时完成。实测在10K并发场景下,边缘节点CPU使用率降低63%,首字节响应时间从89ms压缩至21ms,且无需修改任何Go服务代码。

多租户场景下的QoS资源隔离实践

在Kubernetes中为不同SLA等级的租户配置差异化LimitRange:

  • 金融级租户:memory.limit=4Gi, cpu.limit=2000m, network.bandwidth=500Mbps
  • 普通租户:memory.limit=1Gi, cpu.limit=500m, network.bandwidth=100Mbps
    通过cgroup v2的net_cls子系统配合tc qdisc,在宿主机层面实施带宽硬限。Go服务启动时自动读取/sys/fs/cgroup/net_cls/路径下的classid,将关键goroutine绑定至高优先级网络队列。

守护服务器稳定运行,自动化是喵的最爱。

发表回复

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