Posted in

Prometheus+Golang深度协同方案(2024最新实践白皮书)

第一章:Prometheus+Golang深度协同方案(2024最新实践白皮书)

Prometheus 与 Golang 的原生契合性在云原生可观测性体系中持续深化。Go 语言内置的 net/http/pprof 和 expvar 机制、轻量级协程模型,以及对 Prometheus 官方客户端库(prometheus/client_golang)的一等公民支持,使其成为构建高精度指标服务的理想载体。

指标暴露的最佳实践

在 Go 应用中嵌入 Prometheus 指标端点,推荐使用 http.Handler 显式注册 /metrics 路径,并启用标准运行时指标:

import (
    "net/http"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    // 注册 Go 运行时指标(GC、goroutines、memory 等)
    prometheus.MustRegister(prometheus.NewGoCollector())
    // 注册进程指标(CPU、内存、启动时间)
    prometheus.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))

    http.Handle("/metrics", promhttp.Handler()) // 标准指标暴露端点
    http.ListenAndServe(":8080", nil)
}

自定义指标建模原则

避免滥用 Counter 或 Gauge:

  • 请求总量 → Counter(单调递增,支持 rate() 计算 QPS)
  • 当前活跃连接数 → Gauge(可增可减)
  • 请求耗时分布 → Histogram(推荐带 le 标签的直方图,兼容 histogram_quantile())

Prometheus 配置协同要点

确保 scrape_configs 中启用服务发现与健康检查:

字段 推荐值 说明
scrape_timeout 10s 避免因 Go 应用 GC STW 导致超时
metric_relabel_configs drop 内部调试标签 如 __meta_kubernetes_pod_label_env="dev"
honor_labels true 尊重 Go 应用主动设置的 prometheus.Labels

动态指标生命周期管理

使用 prometheus.Unregister() 显式注销已弃用指标,防止内存泄漏:

// 创建可撤销的 Counter
reqCounter := prometheus.NewCounterVec(
    prometheus.CounterOpts{Namespace: "app", Name: "requests_total"},
    []string{"handler", "status"},
)
prometheus.MustRegister(reqCounter)
// 后续可通过 reqCounter.Unregister() 安全移除

第二章:Golang原生监控能力与Prometheus集成原理

2.1 Go运行时指标体系解析与暴露机制

Go 运行时通过 runtime 和 debug 包暴露关键性能指标,构成轻量级、无依赖的原生监控基础。

核心指标分类

  • 内存类:MemStats.Alloc, Sys, HeapObjects
  • GC 类:NumGC, PauseTotalNs, NextGC
  • 协程类:Goroutines, NumCgoCall

指标暴露方式

import "runtime/debug"

func exposeMetrics() {
    ms := debug.ReadGCStats(&debug.GCStats{})
    fmt.Printf("Last GC: %v, Pauses: %d\n", ms.LastGC, len(ms.Pause))
}

debug.ReadGCStats 返回精确到纳秒的 GC 暂停历史;ms.Pause 是环形缓冲区(默认200项),需注意时间戳为绝对纳秒值,需与 ms.LastGC 对齐分析。

标准指标映射表

指标名 来源包 单位 更新频率
go_goroutines runtime 个 实时(读取即得)
go_memstats_alloc_bytes_total runtime/metrics bytes 每次 GC 后更新
graph TD
    A[程序启动] --> B[runtime/metrics 注册]
    B --> C[每 5s 采样一次]
    C --> D[通过 /debug/metrics HTTP 接口暴露]
    D --> E[Prometheus 抓取]

2.2 Prometheus Client Go库核心组件源码级剖析

Metrics注册与收集机制

prometheus.MustRegister() 底层调用 defaultRegisterer.Register(),将 Collector 接口实例注入全局 Registry。关键路径:

// registry.go 中 Register 方法核心逻辑
func (r *Registry) Register(c Collector) error {
    r.mtx.Lock()
    defer r.mtx.Unlock()
    // 检查重复注册(name + help 唯一性)
    if _, exists := r.collectorsByID[collectorID(c)]; exists {
        return fmt.Errorf("duplicate metrics collector registration attempted")
    }
    r.collectors = append(r.collectors, c)
    r.collectorsByID[collectorID(c)] = struct{}{}
    return nil
}

collectorID(c) 基于指标名称与帮助文本哈希生成唯一标识,避免命名冲突。

核心组件职责划分

  • Counter:原子递增,底层使用 uint64 + sync/atomic
  • Gauge:支持增减、设值,基于 float64 + sync.RWMutex
  • Histogram:分桶统计,维护 buckets 切片与 sum/count 原子变量

指标暴露流程

graph TD
A[HTTP Handler] --> B[Registry.Collect]
B --> C[Collector.Describe]
C --> D[Collector.Collect]
D --> E[Prometheus text format]
组件 线程安全 序列化开销 典型用途
Counter ✅ 低 请求总数、错误数
Histogram ✅ 中 延迟分布
Summary ⚠️(采样) 高 分位数实时计算

2.3 自定义指标注册、生命周期管理与内存安全实践

指标注册的正确时机

自定义指标应在应用初始化早期注册,避免并发注册冲突。推荐在 init() 函数或 main() 开头完成注册:

var (
    // 使用全局变量确保单例,但需原子访问
    reqLatency = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "Latency distribution of HTTP requests",
            Buckets: prometheus.DefBuckets, // 预设指数级桶区间
        },
        []string{"method", "status"},
    )
)

func init() {
    prometheus.MustRegister(reqLatency) // 唯一注册入口,不可重复调用
}

MustRegister() 在重复注册时 panic,强制开发者显式控制注册路径;HistogramVec 支持多维标签,但每个唯一标签组合对应独立时间序列,需警惕高基数风险。

生命周期与内存安全

指标对象应与应用生命周期一致,禁止在 goroutine 中动态创建/销毁指标(易引发泄漏或竞态)。以下为安全实践清单:

  • ✅ 在 main() 启动时注册,进程退出前保持存活
  • ❌ 禁止在 HTTP handler 内调用 NewCounter()(每请求新建 → 内存泄漏)
  • ⚠️ 若需动态标签,使用 WithLabelValues() 复用已注册指标,而非重建
场景 是否安全 原因
全局 init 注册 ✅ 单次、线程安全
Handler 内 new Vec ❌ 指标对象持续增长,OOM
WithLabelValues 调用 ✅ 复用底层 metric,零分配

指标清理的隐式约束

Prometheus 客户端库不提供注销接口,指标一旦注册即永久存在。若需条件性启用指标,应通过标签值(如 enabled="true")逻辑过滤,而非尝试卸载:

graph TD
    A[应用启动] --> B[注册所有可能指标]
    B --> C[运行时通过标签控制采集]
    C --> D[进程退出:OS 自动回收内存]

2.4 指标命名规范、标签设计与高基数风险规避

命名应遵循 namespace_subsystem_metric_name{labels} 模式

例如:http_server_request_duration_seconds_count{method="GET",status="200",route="/api/users"}
✅ 含义清晰、层级分明;❌ 避免 user_login_success_total(缺失命名空间)或 req_dur_ms(缩写歧义)。

标签设计的黄金法则

  • 必选维度:service、env、instance(用于下钻定位)
  • 禁止动态值标签:如 user_id="u12345"、request_id="abc-xyz" → 直接触发高基数灾难
  • 推荐使用预聚合维度:将 user_id 映射为 user_tier="premium" 或 region="us-east"

高基数陷阱与防御实践

# ❌ 危险:cardinality ≈ 10k+ (每用户一标签)
http_requests_total{user_id=~".+"}

# ✅ 安全:固定低基数标签 + 外部关联
http_requests_total{user_group="enterprise"}

逻辑分析:user_id 作为标签会使 Prometheus 时间序列数爆炸式增长,导致内存溢出与查询延迟飙升;改用 user_group(仅 3–5 个枚举值)可将基数控制在百级以内。

维度类型 示例 基数风险 推荐做法
静态环境 env="prod" 低 ✅ 允许
动态ID trace_id="..." 极高 ❌ 禁用,改用日志关联
枚举分类 payment_method="credit_card" 中低 ✅ 控制在
graph TD
    A[原始请求] --> B{是否含高基数字段?}
    B -->|是| C[剥离至日志/追踪系统]
    B -->|否| D[注入预定义标签]
    D --> E[写入TSDB]
    C --> F[通过UID关联指标与日志]

2.5 HTTP/OTLP双通道暴露策略与TLS/gRPC生产适配

在可观测性数据采集层,需同时满足调试便捷性与生产安全性:HTTP/OTLP 用于开发与防火墙穿透(如 http://localhost:4318/v1/traces),gRPC/OTLP 则承载高吞吐、低延迟的生产流量(https://otel-collector.example.com:4317)。

双通道配置示例

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      http:  # 启用HTTP通道(默认4318)
        endpoint: "0.0.0.0:4318"
      grpc:  # 启用gRPC通道(默认4317)
        endpoint: "0.0.0.0:4317"
        tls:
          cert_file: /etc/tls/tls.crt
          key_file: /etc/tls/tls.key

逻辑分析:http 协议无TLS强制要求,适合内网调试;grpc 块中嵌套 tls 配置,启用双向证书校验,确保传输机密性与服务端身份可信。cert_file 与 key_file 必须由PKI系统签发并挂载至容器。

生产就绪关键约束

  • ✅ gRPC通道必须启用 TLS(禁用明文 insecure: true)
  • ❌ HTTP通道不可暴露于公网(应通过反向代理+IP白名单收敛)
  • 🔐 所有出口连接需配置 mTLS(客户端证书由CA统一签发)
通道类型 默认端口 加密要求 典型用途
HTTP/OTLP 4318 可选 本地开发、CI日志
gRPC/OTLP 4317 强制TLS 生产服务间链路
graph TD
  A[应用端] -->|HTTP POST /v1/traces| B(OTel Collector HTTP)
  A -->|gRPC Stream| C{OTel Collector gRPC}
  C -->|mTLS验证| D[CA证书吊销检查]
  D --> E[解密并路由至exporter]

第三章:高性能可观测性架构设计

3.1 高并发场景下指标采集的零GC优化实践

在每秒数万次指标上报的压测中,传统 new Metric() 方式触发频繁 Young GC,STW 延迟飙升至 80ms+。核心破局点在于对象复用与栈上分配规避逃逸。

数据同步机制

采用 ThreadLocal + 环形缓冲区实现无锁批量写入:

// 每线程独享缓冲区,避免 new 对象 & 竞争
private static final ThreadLocal<Buffer> BUFFER = ThreadLocal.withInitial(Buffer::new);

static class Buffer {
    private final Metric[] ring = new Metric[1024]; // 预分配数组,不扩容
    private int head = 0, tail = 0;

    void add(long value) {
        ring[tail % ring.length] = reuseOrNew(value); // 复用旧对象实例
        tail++;
    }
}

reuseOrNew() 通过对象池回收 Metric 实例,ring 数组在编译期确定大小,JVM 可判定其不会逃逸至堆外,触发栈上分配(Escape Analysis 启用前提)。

关键参数对照表

参数 优化前 优化后 效果
GC 次数/分钟 120+ 减少 98%
单次采集耗时 15μs 1.2μs 提升 12.5×
graph TD
    A[采集请求] --> B{ThreadLocal Buffer}
    B --> C[复用Metric对象]
    C --> D[环形数组本地写入]
    D --> E[批量Flush至共享队列]
    E --> F[后台线程聚合输出]

3.2 分布式Trace与Metrics联动建模(OpenTelemetry桥接)

OpenTelemetry 提供统一的信号采集能力,使 Trace 与 Metrics 在语义层面天然可关联。关键在于利用 trace_id 和 span_id 作为跨信号关联锚点。

数据同步机制

通过 OTLPExporter 同时上报 Trace 和 Metrics,并在 Metrics 的 attributes 中注入上下文标识:

from opentelemetry.metrics import get_meter
from opentelemetry.trace import get_current_span

meter = get_meter("service.metrics")
request_counter = meter.create_counter("http.requests.total")

# 关联当前 trace 上下文
current_span = get_current_span()
trace_id = current_span.get_span_context().trace_id if current_span else None

request_counter.add(1, {
    "http.status_code": "200",
    "trace_id_hex": f"{trace_id:032x}" if trace_id else "unknown"
})

逻辑分析:trace_id_hex 以 32 位十六进制字符串形式注入 Metric 标签,确保可在后端(如 Prometheus + Tempo)中通过 trace_id 联查 Span 详情。参数 trace_id 来自 W3C 标准上下文,保证跨服务一致性。

关联建模能力对比

能力维度 仅 Metrics Trace + Metrics 联动
异常根因定位 ❌ ✅(按 trace_id 下钻)
SLI/SLO 计算精度 中 高(绑定真实请求链路)
graph TD
    A[HTTP Handler] --> B[Start Span]
    B --> C[Record Metric with trace_id]
    C --> D[Export via OTLP]
    D --> E[Tempo/Prometheus 联合查询]

3.3 多租户隔离与动态指标过滤的Middleware实现

核心设计原则

  • 租户上下文透传:基于 X-Tenant-ID 请求头注入 TenantContext;
  • 指标维度解耦:将 tenant_id、service_name、env 等作为可插拔过滤键;
  • 零侵入式集成:通过 Go HTTP Middleware 实现,不修改业务 handler。

关键中间件实现

func TenantMetricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 提取租户标识,fallback 到 default
        tenantID := r.Header.Get("X-Tenant-ID")
        if tenantID == "" {
            tenantID = "default"
        }

        // 动态注入指标标签(Prometheus 兼容)
        ctx := context.WithValue(r.Context(), 
            metrics.TenantKey, tenantID)

        // 支持运行时指标白名单过滤(如仅采集 prod 环境的支付服务)
        env := r.URL.Query().Get("env")
        service := r.URL.Query().Get("service")
        if env != "" || service != "" {
            ctx = context.WithValue(ctx, metrics.FilterKey, map[string]string{
                "env":     env,
                "service": service,
            })
        }

        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

逻辑分析:该中间件在请求进入链路前完成两层增强:

  1. TenantKey 绑定确保后续所有指标打标 tenant_id 标签,实现存储与查询级隔离;
  2. FilterKey 携带动态条件,供下游指标收集器(如 Prometheus Exporter)按需采样,避免全量上报。

过滤策略对照表

场景 过滤参数示例 效果
多租户资源隔离 X-Tenant-ID: t-001 所有指标自动附加 tenant_id="t-001"
灰度环境专项监控 ?env=staging&service=auth 仅采集 staging 环境 auth 服务指标

数据流示意

graph TD
    A[HTTP Request] --> B[X-Tenant-ID / Query Params]
    B --> C[Middleware 注入 Context]
    C --> D[Handler 中 metrics.Record]
    D --> E[Exporter 按 tenant_id + filter 动态聚合]

第四章:企业级落地工程化方案

4.1 Kubernetes Operator自动化部署Golang服务监控栈

Kubernetes Operator通过自定义控制器将监控栈部署逻辑封装为声明式API,显著降低Golang服务可观测性落地门槛。

核心架构设计

Operator监听MonitorStack自定义资源(CR),动态协调Prometheus、Grafana、Exporter组件生命周期:

# monitorstack.example.com/v1alpha1
apiVersion: monitoring.example.com/v1alpha1
kind: MonitorStack
metadata:
  name: golang-service
spec:
  prometheus: { replicas: 2, retention: "7d" }
  grafana: { adminPasswordSecret: "grafana-cred" }
  exporters:
    - name: golang-app
      port: 9090
      metricsPath: "/metrics"

此CR定义了面向Golang应用的监控栈拓扑。Operator解析后生成对应StatefulSet、Service及RBAC策略;retention控制TSDB数据保留周期,adminPasswordSecret确保凭据安全注入。

组件协同流程

graph TD
  A[MonitorStack CR] --> B{Operator Reconcile}
  B --> C[验证Golang Exporter健康端点]
  C --> D[部署Prometheus Rule + ServiceMonitor]
  D --> E[自动发现Pod并抓取/metrics]

关键优势对比

能力 传统Helm部署 Operator方案
配置变更响应 手动升级或CI触发 实时Watch+Reconcile
Golang指标适配 需手动编写ServiceMonitor 自动注入label selector
故障自愈 依赖外部告警与运维 Pod异常时自动重启Exporter

Operator通过client-go监听事件,结合prometheus-operator SDK构建闭环控制流,使Golang服务从代码提交到指标可视化全程自动化。

4.2 基于Go Generate的指标契约代码自动生成体系

传统硬编码指标定义易引发契约漂移。Go //go:generate 提供声明式触发点,将 OpenMetrics Schema(YAML)转化为强类型 Go 结构体与校验逻辑。

自动生成流程

# 在 metrics.go 中声明
//go:generate go run github.com/your-org/metricgen --schema=metrics.yaml --output=generated_metrics.go

该指令在 go generate 执行时调用 CLI 工具解析 YAML,生成含 Validate() 方法的结构体。

核心生成能力

  • ✅ 指标元数据(名称、类型、单位、标签集)→ struct 字段 + JSON tag
  • ✅ 标签约束(必需/可选/枚举值)→ Validate() 中字段级校验逻辑
  • ✅ Prometheus 兼容命名规范 → 自动下划线转 snake_case 并校验合法性

生成契约示例

输入字段 生成 Go 类型 校验逻辑
latency_ms LatencyMs float64 if m.LatencyMs < 0 { return errors.New("latency_ms must be non-negative") }
// generated_metrics.go(片段)
type HTTPRequest struct {
    StatusCode int    `json:"status_code" prom:"label"`
    DurationMs int64  `json:"duration_ms" prom:"unit:milliseconds"`
}
func (m *HTTPRequest) Validate() error {
    if m.StatusCode < 100 || m.StatusCode > 599 {
        return fmt.Errorf("status_code out of range [100,599]")
    }
    return nil
}

此结构体由工具生成,Validate() 确保运行时指标数据符合契约,避免非法值流入监控管道。

graph TD
A[metrics.yaml] --> B{metricgen CLI}
B --> C[Parse Schema]
C --> D[Generate Struct + Validate]
D --> E[generated_metrics.go]
E --> F[Build-time embed]

4.3 Prometheus Rule动态加载与Golang热重载机制实现

Prometheus 原生不支持规则文件的运行时热更新,需结合 Go 的文件监听与规则解析能力构建轻量级热重载通道。

核心设计思路

  • 监听 rules/*.yml 文件变更(fsnotify)
  • 解析新规则并校验语法(prometheus/promql/parser)
  • 原子替换内存中 RuleGroups 实例,避免中断评估

规则加载流程

// 启动热重载监听器
watcher, _ := fsnotify.NewWatcher()
watcher.Add("rules/")
for {
    select {
    case event := <-watcher.Events:
        if event.Op&fsnotify.Write == fsnotify.Write {
            rules, err := loadAndValidateRules("rules/") // 加载+语法/语义校验
            if err == nil {
                atomic.StorePointer(&currentRules, unsafe.Pointer(&rules))
            }
        }
    }
}

loadAndValidateRules 调用 rulefmt.ParseFile() 解析 YAML,并使用 promql.NewEngine().NewTestQuery() 预编译表达式,确保无语法错误;atomic.StorePointer 保证规则切换的内存可见性与无锁原子性。

热重载关键参数对比

参数 说明 推荐值
--web.enable-admin-api 启用 /api/v1/admin 接口 false(生产禁用)
reload-interval 主动轮询间隔(备用兜底) 30s
max-rule-groups 单次加载最大分组数 100
graph TD
    A[文件系统变更] --> B{fsnotify事件}
    B --> C[解析YAML规则]
    C --> D[语法校验+预编译]
    D --> E{校验通过?}
    E -->|是| F[原子替换规则指针]
    E -->|否| G[记录错误日志]

4.4 监控告警闭环:从Prometheus Alertmanager到Go服务自动响应

告警路由与Webhook分发

Alertmanager通过webhook_configs将匹配的告警推送到自研Go服务的/alert端点,支持重试、去重和分组策略。

Go服务接收与解析

func AlertHandler(w http.ResponseWriter, r *http.Request) {
    var alerts struct {
        Alerts []struct {
            Status string `json:"status"` // firing/resolved
            Labels map[string]string `json:"labels"`
            Annotations map[string]string `json:"annotations"`
        } `json:"alerts"`
    }
    json.NewDecoder(r.Body).Decode(&alerts)
    // 解析后触发对应业务响应逻辑
}

该结构体严格对应Alertmanager v0.27+的Webhook v2协议;Status决定执行恢复或故障流程,Labels.alertname是关键路由键。

自动响应决策矩阵

告警名称 触发动作 超时阈值 降级开关
HighCPUUsage 自动扩容Pod副本数 90s ✅
DBConnectionFail 切换读写分离只读节点 30s ❌

流程协同视图

graph TD
    A[Prometheus] -->|firing alert| B[Alertmanager]
    B -->|HTTP POST /alert| C[Go Webhook Service]
    C --> D{解析status & labels}
    D -->|firing| E[执行预案:扩容/切换/通知]
    D -->|resolved| F[清理资源/关闭工单]

第五章:总结与展望

技术栈演进的实际影响

在某大型电商平台的微服务重构项目中,团队将原有单体架构迁移至基于 Kubernetes 的云原生体系。迁移后,平均部署耗时从 47 分钟压缩至 92 秒,CI/CD 流水线成功率由 63% 提升至 99.2%。关键变化在于:容器镜像统一采用 distroless 基础镜像(大小从 856MB 降至 12MB),配合 Argo CD 实现 GitOps 自动同步;服务间通信全面启用 gRPC-Web + TLS 双向认证,API 延迟 P95 降低 41%,且全年未发生一次因证书过期导致的级联故障。

生产环境可观测性闭环建设

该平台落地了三层次可观测性体系:

  • 日志层:Fluent Bit 边车采集 + Loki 归档(保留 90 天),支持结构化字段实时过滤(如 status_code="503" service="payment-gateway");
  • 指标层:Prometheus Operator 管理 237 个自定义指标,其中 http_request_duration_seconds_bucket{le="0.2",service="inventory"} 直接触发自动扩缩容;
  • 追踪层:Jaeger 集成 OpenTelemetry SDK,单次订单链路平均跨度达 17 个服务,异常根因定位时间从小时级缩短至 83 秒。

下表对比了迁移前后核心 SLO 达成率:

SLO 指标 迁移前 迁移后 提升幅度
API 可用率(99.9%) 99.21% 99.98% +0.77pp
部署失败率 37% 0.8% -36.2pp
故障平均恢复时间(MTTR) 28min 3.1min -89%

工程效能度量驱动持续改进

团队建立 DevEx(Developer Experience)仪表盘,每日追踪 12 项过程指标。例如:pr_merge_time_median(PR 合并中位时长)从 18.3 小时降至 2.7 小时,直接归因于引入自动化测试覆盖率门禁(要求新增代码行覆盖 ≥85%)及预提交检查流水线。Mermaid 图展示了当前 CI 流水线的关键路径:

flowchart LR
    A[Git Push] --> B[Pre-commit Hook]
    B --> C[单元测试+静态扫描]
    C --> D{覆盖率≥85%?}
    D -->|是| E[构建镜像]
    D -->|否| F[阻断推送]
    E --> G[推送到 Harbor]
    G --> H[Argo CD 触发同步]

安全左移的落地实践

在金融子系统中,SAST 工具 SonarQube 与 Jenkins Pipeline 深度集成,所有 PR 必须通过 OWASP Top 10 漏洞扫描(含 SQLi、XSS 检测规则 217 条)。2023 年共拦截高危漏洞 412 个,其中 327 个在开发阶段修复,漏洞平均修复周期为 1.8 天——较传统渗透测试模式缩短 14.3 倍。关键改进包括:自定义规则引擎识别 Spring Boot Actuator 敏感端点暴露、动态生成 SBOM 清单并校验 CVE-2023-38545 等已知组件风险。

未来技术验证路线图

团队已启动三项关键技术验证:

  1. 使用 eBPF 实现无侵入式网络策略审计,替代 Istio Sidecar 的 CPU 开销;
  2. 在 Kafka 消费者组中试点 Quarkus 原生镜像,内存占用目标压降至 64MB;
  3. 构建 LLM 辅助的故障诊断知识库,接入历史 28,417 条告警工单与根因分析报告。

不张扬,只专注写好每一行 Go 代码。

发表回复

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