Posted in

【Golang可观测性基建搭建指南】:OpenTelemetry + Jaeger + Prometheus + Grafana一体化部署(含K8s Helm Chart)

第一章:Golang可观测性基建的核心价值与架构全景

在云原生与微服务深度演进的今天,Golang 因其高并发、低延迟与部署轻量等特性,成为基础设施与中间件服务的首选语言。然而,单体应用的调试经验无法平移至分布式 Go 服务中——一次跨服务调用可能横跨数十个 Pod、多个可用区,且生命周期以毫秒计。此时,“可观测性”不再仅是日志查看工具,而是系统健康状态的神经系统。

核心价值:从被动响应到主动掌控

可观测性在 Go 生态中体现为三大支柱的协同增强:

  • 指标(Metrics):实时反映服务吞吐、延迟、错误率(如 http_request_duration_seconds_bucket),支撑 SLO 达标监控;
  • 日志(Logs):结构化 JSON 日志(推荐使用 zerologzap)支持字段级检索与上下文串联;
  • 链路追踪(Tracing):通过 OpenTelemetry SDK for Go 注入 span,还原请求全路径,定位瓶颈环节。

架构全景:分层可插拔设计

现代 Go 可观测性基建采用标准化分层架构:

层级 关键组件 职责说明
Instrumentation go.opentelemetry.io/otel/sdk/metric 自动/手动埋点,生成原始遥测数据
Exporter OTLP exporter + gRPC endpoint 将数据以协议标准格式推送至后端
Collector OpenTelemetry Collector(独立进程) 接收、过滤、批处理、路由,解耦应用与存储
Backend Prometheus + Loki + Tempo(或 Grafana Cloud) 存储、查询、可视化三位一体

快速启动示例

在 Go 应用中启用 OpenTelemetry 指标采集只需三步:

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/metric"
    "go.opentelemetry.io/otel/sdk/resource"
    semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

func initMeterProvider() {
    // 创建 OTLP 导出器(指向本地 collector)
    exporter, _ := otlpmetricgrpc.New(context.Background())
    // 构建指标 SDK
    mp := metric.NewMeterProvider(
        metric.WithResource(resource.MustNewSchema1(
            semconv.ServiceNameKey.String("auth-service"),
        )),
        metric.WithReader(metric.NewPeriodicReader(exporter)),
    )
    otel.SetMeterProvider(mp)
}

该初始化代码将自动采集运行时指标(如 Go GC、goroutine 数),并支持自定义业务指标(如 user_login_total)。所有数据经 OTLP 协议发送至 Collector,实现零侵入式接入与统一治理。

第二章:OpenTelemetry在Go服务中的深度集成与最佳实践

2.1 OpenTelemetry Go SDK原理剖析与Tracing初始化设计

OpenTelemetry Go SDK 的核心是 sdk/trace 包,其初始化本质是构建可组合的 TracerProvider 实例。

初始化流程关键步骤

  • 创建 sdktrace.TracerProvider(含 SpanProcessorSpanExporter
  • 注册 otel.TracerProvider 全局实例
  • 通过 otel.Tracer() 获取 Tracer 实例,绑定 TracerProvider

TracerProvider 构建示例

// 构建带 BatchSpanProcessor 的 TracerProvider
tp := sdktrace.NewTracerProvider(
    sdktrace.WithSampler(sdktrace.AlwaysSample()),
    sdktrace.WithSpanProcessor(
        sdktrace.NewBatchSpanProcessor(exporter),
    ),
)
otel.SetTracerProvider(tp)

WithSampler 控制采样策略;NewBatchSpanProcessor 异步批处理 Span 并推送至 exporterSetTracerProvider 将其实例注册为全局默认提供者,供 otel.Tracer() 动态解析。

核心组件关系(简化)

graph TD
    A[otel.Tracer] --> B[TracerProvider]
    B --> C[SpanProcessor]
    C --> D[SpanExporter]
    C --> E[In-Memory Buffer]

2.2 自动化Instrumentation与手动埋点的协同策略(HTTP/gRPC/DB)

在可观测性实践中,自动化 Instrumentation 提供基础覆盖,而手动埋点弥补语义盲区。二者需分层协作,而非互斥。

协同原则

  • 自动化捕获协议层指标(如 HTTP 状态码、gRPC 延迟、DB 执行时长)
  • 手动埋点注入业务上下文(如 order_idtenant_id、支付阶段标记)
  • 共享统一 Trace ID 与 Span Context,确保跨组件链路贯通

数据同步机制

# OpenTelemetry Python 示例:手动注入业务属性到自动创建的 Span
from opentelemetry import trace
from opentelemetry.trace import SpanKind

span = trace.get_current_span()
if span and span.is_recording():
    span.set_attribute("payment.status", "initiated")  # 业务状态
    span.set_attribute("order.id", "ORD-789456")       # 关键业务ID

此代码在自动创建的 HTTP/gRPC/DB Span 上追加业务维度。is_recording() 防止在采样关闭时无效写入;set_attribute() 支持字符串/数字/布尔类型,兼容后端分析系统(如 Jaeger、Tempo)。

组件类型 自动化覆盖项 推荐手动增强点
HTTP method, status_code, path user_id, feature_flag
gRPC service, method, code workflow_id, retry_attempt
DB db.statement, db.name query_purpose, tenant_scope
graph TD
    A[HTTP Handler] -->|Auto| B[Span: /api/pay]
    B --> C[Manual: set_attribute<br>“payment.flow”=“3ds”]
    C --> D[gRPC Client]
    D -->|Auto| E[Span: payment.Service/Process]
    E --> F[Manual: set_attribute<br>“risk.score”=0.82]

2.3 Context传播机制详解与跨协程Span生命周期管理

在 Kotlin 协程中,CoroutineContext 是 Span 传递的载体,其 Element 扩展能力天然支持 OpenTracing 兼容的 TracingContext 插入。

数据同步机制

Span 必须随协程上下文自动继承、挂起恢复时无缝延续:

val tracedScope = CoroutineScope(Dispatchers.Default + TracingContext(span))
launch(tracedScope.coroutineContext) {
    // span 自动绑定到当前协程,且在 withContext 切换后仍存活
    val childSpan = span.spawnChild("db.query") // 创建子 Span
    withContext(Dispatchers.IO) {
        childSpan.setTag("db.type", "postgresql")
        // childSpan 生命周期独立于父协程调度器切换
    }
    childSpan.finish()
}

逻辑分析TracingContext 实现 AbstractCoroutineContextElement,通过 fold() 遍历链式传播;spawnChild() 基于当前 Span 的 traceId/spanId 生成新 Span,确保跨调度器一致性。

Span 生命周期关键约束

  • ✅ 挂起前未 finish 的 Span 在恢复后仍有效
  • ❌ 不可跨 runBlocking 外部线程手动传递(破坏结构化并发)
  • ⚠️ coroutineScope { } 内新建协程会继承父 Span,但 GlobalScope 不继承
场景 Span 是否延续 原因
withContext(IO) Context 元素深度复制
async { } 继承启动协程的完整 Context
GlobalScope.launch 无父 Context 可继承
graph TD
    A[Root Span] --> B[launch { span.spawnChild } ]
    B --> C[withContext(IO)]
    C --> D[DB 调用]
    D --> E[finish childSpan]

2.4 Metrics采集建模:从Counter/Gauge/Histogram到业务语义指标定义

监控不是堆砌数字,而是将系统行为翻译为可推理的业务语言。基础指标类型仅提供“计量原语”:

  • Counter:单调递增(如请求总数),适用于累计事件;
  • Gauge:瞬时快照值(如当前活跃连接数),支持增减;
  • Histogram:分桶统计分布(如HTTP延迟P95),需预设bucket边界。

从原始指标到业务语义的跃迁

需在采集层注入领域上下文。例如电商下单成功率,不能仅用http_request_total{status=~"2.."}除以总量——它忽略了幂等重试、库存预占失败等业务路径。

# Prometheus client Python 示例:定义带业务标签的复合指标
from prometheus_client import Counter, Histogram

# 语义化Counter:按业务动作归因
order_submitted = Counter(
    'ecom_order_submitted_total', 
    'Orders submitted (pre-validation)', 
    ['channel', 'is_retry']  # ← 业务维度,非基础设施维度
)

# 语义化Histogram:绑定业务SLA目标
payment_latency = Histogram(
    'ecom_payment_processing_seconds',
    'Payment processing time by provider',
    ['provider', 'result'],  # result ∈ {'success', 'declined', 'timeout'}
    buckets=(0.1, 0.3, 0.6, 1.0, 2.0)  # ← 对齐支付SLA承诺(≤1s达标)
)

逻辑分析:order_submittedis_retry=True 标签使重试率可直接计算;payment_latencyresult 标签与 buckets 共同支撑“支付成功率+时效达标率”双SLI计算,无需后期聚合打标。

建模关键约束

维度 基础指标实践 业务语义建模要求
标签设计 instance, job channel, order_type, fraud_risk_level
命名前缀 http*, process* ecom, payment, inventory_
生命周期 长期保留原始数据 按业务事件流自动关联生命周期(如订单ID贯穿全链路)
graph TD
    A[原始埋点] --> B[指标类型选择<br>Counter/Gauge/Histogram]
    B --> C[业务维度注入<br>标签语义化 + 命名空间隔离]
    C --> D[SLI/SLO对齐<br>如“支付3秒内成功≥99.5%”]
    D --> E[告警/看板直出<br>无需SQL二次加工]

2.5 Exporter选型对比与OTLP协议调优(gRPC vs HTTP,TLS/Compression配置)

协议特性对比

特性 gRPC (HTTP/2) HTTP/1.1
多路复用 ✅ 原生支持 ❌ 需多个连接
流式传输 ✅ 支持 streaming ❌ 仅请求-响应
默认压缩 ✅ Protocol Buffers ❌ 需手动启用 gzip
TLS 兼容性 ✅ 强制或可选 ✅ 独立配置

gRPC Exporter 调优示例

# otel-collector config.yaml
exporters:
  otlp/mygrpc:
    endpoint: "otel-collector:4317"
    tls:
      insecure: false  # 启用 TLS 验证
      ca_file: "/etc/ssl/certs/ca.pem"
    compression: "gzip"  # 支持 gzip/zstd(v0.98+)

insecure: false 强制服务端证书校验,避免中间人攻击;compression: "gzip" 在高基数指标场景下可降低 40–60% 网络负载,但增加 CPU 开销约 8–12%。

数据同步机制

graph TD
  A[Exporter] -->|gRPC streaming| B[Collector]
  A -->|HTTP batch POST| C[Collector]
  B --> D[Queue → Export]
  C --> D

gRPC 流式通道天然适配 trace span 实时推送;HTTP 更适合低频、批量化 metric 上报。

第三章:分布式链路追踪体系构建:Jaeger高可用部署与Go端对接

3.1 Jaeger各组件(Agent/Collector/Query/Ingester)职责与通信拓扑解析

Jaeger采用分层解耦架构,各组件职责明确、通信边界清晰:

  • Agent:部署在宿主机或Sidecar,负责接收Thrift/UDP格式Span,批量转发至Collector;
  • Collector:持久化入口,校验、采样、转换Span后写入后端存储(如Cassandra/Elasticsearch);
  • Query:提供HTTP API与UI,从存储读取数据并聚合渲染;
  • Ingester(可选):基于Kafka消费Span,替代Collector直连存储,提升水平扩展性与背压能力。

数据同步机制

当启用Kafka时,Collector将Span序列化为jaeger-span主题消息:

# collector.yaml 片段
kafka:
  producer:
    brokers: ["kafka:9092"]
    topic: jaeger-span
    encoding: proto  # 支持 json/proto/thrift

encoding: proto显著降低网络开销,需确保Ingester配置匹配解码器;若不一致将导致反序列化失败并丢弃消息。

组件通信拓扑

graph TD
  A[Client SDK] -->|Thrift/UDP| B[Agent]
  B -->|Thrift/TCP| C[Collector]
  C -->|Kafka| D[Ingester]
  D --> E[(Storage)]
  C -.->|Direct write| E
  F[Query] -->|Read-only| E
组件 协议 默认端口 关键依赖
Agent UDP/Thrift 6831/6832 Collector
Collector HTTP/gRPC 14267/14268 Kafka/Storage
Query HTTP 16686 Storage
Ingester Kafka Kafka + Storage

3.2 基于K8s StatefulSet+HPA的Jaeger集群弹性部署实践

Jaeger后端组件(如jaeger-collectorjaeger-query)对有序性与稳定网络标识有强依赖,StatefulSet天然保障Pod名称、存储卷及DNS记录的一致性。

核心资源配置要点

  • 使用volumeClaimTemplates绑定独立PVC,确保每个Collector实例拥有专属缓冲存储
  • 设置podManagementPolicy: OrderedReady,避免滚动更新引发采样丢失

HPA策略设计

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: jaeger-collector-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: StatefulSet
    name: jaeger-collector  # 必须指向StatefulSet而非Deployment
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

逻辑分析:HPA直接作用于StatefulSet需显式指定apiVersion: apps/v1kind: StatefulSetaverageUtilization基于容器请求值计算,要求Collector容器resources.requests.cpu已明确定义,否则扩缩容失效。

扩缩容行为对比

场景 StatefulSet + HPA Deployment + HPA
Pod重建时IP/DNS 保持不变(如 collector-0.jaeger-collector-ns.svc) 动态变更
存储卷复用 ✅ PVC自动绑定原PV ❌ 需手动管理或依赖动态供给
graph TD
  A[Prometheus采集CPU指标] --> B{HPA Controller}
  B -->|触发扩容| C[StatefulSet增加replicas]
  C --> D[新Pod按序启动:collector-2]
  D --> E[通过Headless Service被ingester发现]

3.3 Go服务与Jaeger后端的零侵入对接:OTel Collector桥接与采样策略调优

OTel Collector 配置桥接 Jaeger

receivers:
  otlp:
    protocols:
      grpc:
exporters:
  jaeger:
    endpoint: "jaeger-collector:14250"
    tls:
      insecure: true
service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [jaeger]

该配置使 Go 应用仅需对接标准 OTLP gRPC 端点(localhost:4317),无需任何 Jaeger SDK 依赖。Collector 充当协议转换层,将 OTLP 格式透明转为 Jaeger Thrift/gRPC 协议。

采样策略调优对比

策略类型 适用场景 采样率控制粒度
always_sample 调试与关键链路 全链路无损
trace_id_ratio 生产环境降噪 按 TraceID 哈希随机
parentbased_traceidratio 保留有标记的请求链路 继承父 Span 决策 + 比率兜底

数据同步机制

// Go 应用仅初始化 OTel SDK(零侵入)
import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"

exp, _ := otlptracegrpc.New(context.Background(),
  otlptracegrpc.WithEndpoint("localhost:4317"),
  otlptracegrpc.WithInsecure(), // 开发环境简化
)

此客户端完全解耦 Jaeger 实现细节;所有采样、批处理、重试均由 Collector 统一管理,应用侧无感知。

graph TD A[Go App] –>|OTLP/gRPC| B[OTel Collector] B –>|Jaeger/gRPC| C[Jaeger Collector] C –> D[Jaeger UI / Storage]

第四章:指标监控与可视化闭环:Prometheus + Grafana一体化落地

4.1 Prometheus Operator在K8s中部署Go服务指标采集栈(ServiceMonitor/Probe配置)

Prometheus Operator 通过声明式 CRD 简化指标采集配置。核心是将 Go 服务暴露的 /metrics 端点与 ServiceMonitor 关联。

ServiceMonitor 配置示例

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: go-app-monitor
  labels: {release: prometheus}
spec:
  selector:
    matchLabels: {app: go-app}  # 匹配对应Service的label
  endpoints:
  - port: metrics
    interval: 30s
    scheme: http

该资源告知 Prometheus:从标签为 app: go-app 的 Service 中,每30秒通过 metrics 端口抓取 HTTP 指标。selector 必须与目标 Service 的 label 完全一致,否则无法发现端点。

Probe 用于黑盒探测

适用于健康检查或外部服务指标采集,支持 HTTP、TCP、DNS 等协议。

字段 说明
targetPort 目标服务端口(如 8080)
interval 探测间隔,默认 1m
prober 指定探针服务地址(如 blackbox-exporter:9115
graph TD
  A[Go App /metrics] --> B[Service]
  B --> C[ServiceMonitor]
  C --> D[Prometheus Target]
  D --> E[TSDB 存储]

4.2 Go runtime指标深度解读与自定义业务指标暴露(/metrics端点优化与版本兼容)

Go runtime 指标(如 go_goroutinesgo_memstats_alloc_bytes)由 runtime/metrics 包原生暴露,但默认未集成至 /metrics。需显式注册:

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

func init() {
    // 注册标准 runtime 指标(v1.21+ 推荐方式)
    prometheus.MustRegister(
        prometheus.NewGoCollector(
            prometheus.WithGoCollectorRuntimeMetrics(
                prometheus.GoRuntimeMetricsRule{Matcher: prometheus.Any},
            ),
        ),
    )
}

此代码启用全量 runtime 指标采集;WithGoCollectorRuntimeMetrics 控制指标粒度,Any 表示启用所有稳定指标(避免使用 All,因含实验性指标,破坏版本兼容性)。

自定义业务指标示例

  • http_request_duration_seconds_bucket(直方图)
  • order_processed_total{status="success"}(带标签计数器)

版本兼容关键点

Go 版本 runtime/metrics 稳定性 /metrics 默认暴露
不可用 需手动桥接 expvar
1.19–1.20 实验性(前缀 /x/
≥ 1.21 稳定(/runtime/ 命名空间) 是(配合 promhttp
graph TD
    A[HTTP /metrics 请求] --> B{Go v1.21+?}
    B -->|是| C[自动注入 runtime/metrics]
    B -->|否| D[回退至 expvar 桥接]
    C --> E[Prometheus 格式序列化]
    D --> E

4.3 Grafana仪表盘开发:从预置Dashboards到可复用的Go可观测性Panel模板库

Grafana原生Dashboard JSON配置冗长且难以维护。为提升复用性,团队将高频监控场景(如HTTP延迟分布、错误率热力图)抽象为Go结构体模板。

模板化Panel定义示例

// HTTP延迟P95面板模板
type HTTPDurationPanel struct {
    Title       string `json:"title"`
    DataSource  string `json:"datasource"`
    Legend      string `json:"legend"`
    Unit        string `json:"unit"` // "ms"
    Thresholds  []Threshold `json:"thresholds"`
}

// Threshold定义告警阈值区间
type Threshold struct {
    Color string `json:"color"` // "red", "orange"
    Value float64 `json:"value"` // 200.0
}

该结构支持json.Marshal直出Grafana兼容JSON;DataSourceUnit字段确保跨环境一致性;Thresholds数组实现动态阈值渲染。

模板能力对比表

能力 静态JSON Dashboard Go Panel模板库
环境变量注入 ❌ 手动替换 os.Getenv()注入
类型安全校验 ❌ 运行时失败 ✅ 编译期检查
多版本语义化复用 ❌ 复制粘贴易出错 v1.HTTPDurationPanel

构建流程

graph TD
A[定义Go Panel结构] --> B[注入环境/租户参数]
B --> C[生成JSON并校验schema]
C --> D[CI自动发布至Grafana API]

4.4 告警规则工程化:基于Prometheus Alertmanager实现SLI/SLO驱动的分级告警

传统阈值告警易产生噪声,而SLI(Service Level Indicator)与SLO(Service Level Objective)为告警注入业务语义。将SLO达标率(如“99.5%请求延迟

SLI指标建模示例

# alert_rules.yml —— 基于SLO余量(Burn Rate)的分级触发
- alert: SLO_BurnRate_Critical
  expr: (rate(http_request_duration_seconds_bucket{le="0.2"}[1h]) / rate(http_requests_total[1h])) < 0.995
  for: 5m
  labels:
    severity: critical
    slo_id: "latency-p99-200ms"
  annotations:
    summary: "SLO burn rate exceeds 30x — {{ $value | humanizePercentage }}"

该规则计算1小时内满足SLI的请求占比,for: 5m避免瞬时抖动误报;severity标签驱动Alertmanager路由策略,实现告警分级分派。

告警分级路由逻辑

severity 接收方 响应时限 升级机制
warning Slack #oncall 15min 无自动升级
critical PagerDuty + SMS 5min 3次未确认则转主管
graph TD
  A[Prometheus 触发告警] --> B{Alertmanager 路由}
  B -->|severity=critical| C[PagerDuty + 自动Call]
  B -->|severity=warning| D[Slack + 邮件摘要]
  C --> E[Ops团队执行SLO根因分析]

第五章:未来演进与企业级落地思考

混合云环境下的模型服务编排实践

某头部券商在2023年完成大模型推理平台升级,将Llama-3-70B量化版本部署于本地GPU集群(8×A100),同时将长尾低频问答请求自动路由至公有云弹性推理实例(AWS g5.12xlarge)。通过自研的Kubernetes CRD ModelService 实现跨云服务发现与权重动态调度,SLA从92.4%提升至99.7%,月均节省GPU闲置成本237万元。其核心配置片段如下:

apiVersion: ai.example.com/v1
kind: ModelService
metadata:
  name: risk-assessment-v2
spec:
  primaryEndpoint: "http://llm-infra.internal:8080"
  fallbackEndpoint: "https://risk-llm.us-east-1.amazonaws.com"
  timeoutSeconds: 15
  fallbackThreshold: 0.85 # 当本地P95延迟>15s且成功率<85%时触发切换

多模态流水线的可观测性增强

制造业客户在质检场景中集成视觉大模型(Qwen-VL)与OCR引擎,构建端到端缺陷识别流水线。为解决模型漂移导致的误检率上升问题,团队在Prometheus中新增17个自定义指标,包括model_input_drift_score{model="qwen-vl", stage="preprocess"}ocr_confidence_distribution_bucket。下表展示连续三周关键指标变化趋势:

周次 平均输入漂移分 OCR置信度 人工复核率
W1 0.12 18.3% 12.7%
W2 0.31 29.5% 24.1%
W3 0.44 41.2% 38.9%

当W3指标突破阈值后,系统自动触发数据重采样任务并通知MLOps工程师。

合规驱动的模型血缘追溯体系

金融监管新规要求所有AI决策路径可审计。某城商行采用Apache Atlas构建全链路血缘图谱,覆盖从原始票据扫描图像(存储于MinIO)、预处理参数(SHA256校验值存入HashiCorp Vault)、微调检查点(HuggingFace Hub commit ID)到生产API响应(OpenTelemetry traceID嵌入HTTP header)。以下mermaid流程图描述一次信贷审批请求的血缘关联:

flowchart LR
    A[PDF扫描件<br/>minio://docs/loan-2024-05-22.pdf] --> B[OCR文本提取<br/>model: layoutlmv3<br/>version: 2.1.4]
    B --> C[风险特征向量<br/>transformer: bert-base-finetuned-credit]
    C --> D[审批决策<br/>rule-engine v3.7 + LLM fallback]
    D --> E[审计日志<br/>trace_id: 0x7f8a2b1c]
    E --> F[监管报送接口<br/>CBRC-API v2.0]

边缘-中心协同推理架构演进

电网设备巡检项目面临带宽受限(单基站峰值≤5Mbps)与实时性要求(端到端≤800ms)双重约束。解决方案采用三层分割策略:YOLOv8s主干网络前4层部署于Jetson Orin边缘节点执行轻量特征提取;中间3层由5G MEC服务器承载;最后分类头与后处理逻辑运行于中心云。实测表明,在200台终端并发场景下,平均延迟稳定在732±41ms,较全云方案降低62%。

人机协作工作流的组织适配

某三甲医院上线临床辅助诊断系统后,发现医生平均单次交互耗时达14分钟,远超预期。经Usability Lab测试发现,核心瓶颈在于系统未适配临床决策节奏。改造后引入“三段式会话”机制:首屏仅显示Top3鉴别诊断及依据关键词(

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

发表回复

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