Posted in

CS客户端上线即告警?Go日志埋点、链路追踪、指标上报一体化可观测体系搭建(Prometheus+OTLP+Loki)

第一章:CS客户端可观测性建设的挑战与目标

传统CS(Client-Server)架构中,客户端长期被视为“黑盒”——运行环境异构(Windows/macOS/Linux/移动端)、网络链路不可控、无统一日志采集入口、用户行为难以归因。这导致故障排查严重依赖用户反馈,平均MTTR(平均修复时间)常超过2小时,性能劣化问题有近65%无法复现。

客户端可观测性的核心挑战

  • 数据采集受限:浏览器沙箱、App签名机制、隐私合规(如GDPR/《个人信息保护法》)限制埋点深度;
  • 环境碎片化:同一版本客户端在不同GPU驱动、系统内核、安全软件拦截下表现迥异;
  • 链路断层:服务端Tracing ID无法透传至客户端初始请求,前后端调用无法串联;
  • 资源敏感性:CPU占用超3%或内存增长超10MB即可能引发用户投诉,监控探针需亚毫秒级开销。

关键建设目标

保障用户体验可量化、故障定位可闭环、性能优化可验证。具体包括:

  • 实现端到端链路追踪,支持从用户点击→网络请求→渲染帧率→异常堆栈的全路径关联;
  • 建立轻量级运行时指标采集框架,覆盖FPS、首屏耗时、JS错误率、网络失败率等12项核心指标;
  • 支持按用户ID、设备指纹、会话ID动态开启调试模式,避免全量日志污染生产环境。

快速验证采集能力的实践步骤

# 1. 在客户端构建阶段注入可观测性SDK(以Web为例)
npm install @acme/observability-sdk --save-prod

# 2. 初始化时绑定关键上下文(自动注入trace_id)
import { init } from '@acme/observability-sdk';
init({
  endpoint: 'https://otel.acme.com/v1/logs',
  sampleRate: 0.1, // 仅采样10%用户,平衡精度与成本
  sessionTimeout: 30 * 60 * 1000 // 会话超时30分钟自动续期
});

# 3. 验证上报:触发一次手动事件并检查控制台输出
window.observability.log('user_action', { button: 'submit', step: 'checkout' });

执行后,可在开发者工具Console中观察到结构化日志对象,并确认trace_id与服务端网关日志一致,即完成链路打通基础验证。

第二章:Go日志埋点体系设计与落地实践

2.1 日志结构化规范与OpenTelemetry日志模型对齐

OpenTelemetry 日志模型将日志视为带属性(attributes)、时间戳(timestamp)、严重级别(severity)和主体消息(body)的结构化事件,摒弃纯文本拼接范式。

核心字段映射关系

OpenTelemetry 字段 典型结构化日志字段 说明
body message 原始语义内容,不参与过滤
severity_text level "ERROR""INFO",需标准化为 OTel 定义值
attributes fields / extra 所有上下文键值对(如 trace_id, span_id, user_id

日志采集层适配示例(Python)

import logging
from opentelemetry.sdk._logs import LoggingHandler
from opentelemetry.trace import get_current_span

# 注入 trace context 到日志属性
def inject_otel_context(record):
    span = get_current_span()
    if span and span.is_recording():
        record.otel_trace_id = span.get_span_context().trace_id
        record.otel_span_id = span.get_span_context().span_id
    return record

# 使用 LoggingHandler 自动填充 OTel 标准字段
handler = LoggingHandler()
logger = logging.getLogger("app")
logger.addHandler(handler)
logger.setLevel(logging.INFO)

该代码通过 LoggingHandler 将 Python LogRecord 自动转换为 OTel LogRecordinject_otel_context 确保 trace_id/span_id 作为 attributes 注入,实现分布式追踪上下文与日志的天然对齐。

数据同步机制

graph TD
    A[应用日志 emit] --> B{LogRecord 构建}
    B --> C[注入 trace_id/span_id]
    B --> D[标准化 severity_text]
    C & D --> E[OTel Exporter 序列化]
    E --> F[后端接收:Loki/OTLP-HTTP]

2.2 基于zerolog+OTLP的日志采集器封装与上下文透传

为实现高吞吐、低开销的可观测日志链路,我们封装了 LogCollector 结构体,统一管理 zerolog 实例与 OTLP Exporter 生命周期。

核心封装结构

type LogCollector struct {
    logger *zerolog.Logger
    exporter sdklog.Exporter
}
  • logger:基于 zerolog.New() 构建,禁用时间字段(由 OTLP collector 统一注入),启用 With().Str("service.name", ...) 预置服务上下文;
  • exporterotlploghttp.New() 实例,配置 Endpoint, Headers(含 Authorization)及 Compression(gzip)。

上下文透传机制

通过 zerolog.With().Ctx(ctx) 拦截 trace.SpanContext,自动注入 trace_idspan_id 字段。
支持 context.WithValue(ctx, log.KeyRequestID, "req-abc") 扩展业务标识。

OTLP 日志字段映射表

zerolog 字段 OTLP 属性名 类型
service.name service.name string
trace_id trace_id bytes
span_id span_id bytes
level severity_text string
graph TD
A[应用调用 logger.Info] --> B{LogCollector.Wrap()}
B --> C[注入 trace_id/span_id]
C --> D[序列化为 OTLP LogRecord]
D --> E[HTTP POST to Collector]

2.3 客户端关键路径(登录、鉴权、心跳、消息收发)的精准埋点策略

为保障端到端可观测性,需在四类核心路径注入上下文感知型埋点,避免简单日志打点导致的链路断裂。

埋点设计原则

  • 统一携带 trace_idsession_iddevice_fingerprint
  • 所有事件标记 stage(如 "pre_auth""post_decrypt"
  • 错误事件强制附加 error_codenetwork_type

登录流程埋点示例

// 登录请求发起前(含预检)
analytics.track('login_start', {
  trace_id: getTraceId(),
  stage: 'pre_connect',
  has_cached_token: !!localStorage.getItem('auth_cache'),
  latency_ms: performance.now() // 相对时间戳,用于计算耗时差
});

逻辑分析:performance.now() 提供高精度单调递增时间戳,配合后续 login_success 事件的时间戳可精确计算网络+业务耗时;has_cached_token 辅助归因冷热启动差异。

关键路径事件映射表

路径 触发时机 必填字段
鉴权 JWT 解析完成且校验通过 alg, exp_delta_s
心跳 ACK 收到后 rtt_ms, server_ts_drift
消息接收 解密并校验签名后 msg_type, decrypt_time_ms
graph TD
  A[login_start] --> B{auth_result}
  B -->|success| C[auth_success]
  B -->|fail| D[auth_fail]
  C --> E[heartbeat_init]
  E --> F[message_received]

2.4 异步批量压缩上传与失败回退重试机制实现

核心设计目标

  • 吞吐优先:单次提交 50–200 个文件,自动 ZIP 压缩后异步上传
  • 故障韧性:网络中断、鉴权过期、服务端限流等场景下自动分级重试
  • 状态可溯:每批次生成唯一 batch_id,支持断点续传与幂等重入

重试策略矩阵

错误类型 重试次数 退避间隔(秒) 是否降级压缩
网络超时 3 1, 2, 4
401/403 鉴权失效 1 0 是(跳过压缩)
429 限流 2 5, 10

关键执行流程

async def upload_batch(files: List[Path], batch_id: str) -> bool:
    try:
        zip_data = await compress_async(files)  # 异步内存压缩,避免阻塞事件循环
        resp = await http_client.post(
            "/api/v1/upload/batch",
            data=zip_data,
            headers={"X-Batch-ID": batch_id, "Content-Encoding": "gzip"},
            timeout=60,
        )
        return resp.status_code == 200
    except (TimeoutError, HTTPStatusError) as e:
        return await fallback_retry(batch_id, files, e)  # 触发策略化回退

compress_async 使用 concurrent.futures.ThreadPoolExecutor 封装 zipfile.ZipFile,规避 GIL;X-Batch-ID 用于服务端去重与日志关联;Content-Encoding: gzip 为传输层二次压缩(ZIP 内容已压缩,此处为防御性冗余)。

graph TD
    A[开始上传批次] --> B{压缩成功?}
    B -->|是| C[发起HTTP请求]
    B -->|否| D[跳过压缩,原始分片上传]
    C --> E{响应2xx?}
    E -->|是| F[标记成功]
    E -->|否| G[匹配错误码→查重试表]
    G --> H[执行对应退避+降级]
    H --> I{达最大重试?}
    I -->|是| J[写入失败队列待人工介入]
    I -->|否| C

2.5 日志采样率动态调控与敏感字段自动脱敏实战

日志治理需兼顾可观测性与数据安全。采样率过高消耗资源,过低则丢失关键线索;敏感字段(如身份证、手机号、token)若未脱敏,将违反《个人信息保护法》。

动态采样策略设计

基于QPS与错误率双指标实时调整采样率:

  • 正常流量(错误率
  • 高负载(QPS > 5000)→ 降为 1%
  • 错误激增(错误率 ≥ 5%)→ 升至 100%(全量捕获)
def calc_sample_rate(qps: int, error_rate: float) -> float:
    if error_rate >= 0.05:
        return 1.0  # 全量
    elif qps > 5000:
        return 0.01
    else:
        return 0.1

逻辑分析:函数返回浮点型采样率(0.0–1.0),供 OpenTelemetry SDK 的 TraceIdRatioBasedSampler 直接使用;参数 qps 来自 Prometheus 指标拉取,error_rate 由 Span 统计聚合得出。

敏感字段识别与脱敏流程

采用正则+词典双模匹配,支持热更新规则:

字段类型 示例模式 脱敏方式
手机号 1[3-9]\d{9} 138****1234
身份证 \d{17}[\dXx] 110101*********001X
graph TD
    A[原始日志] --> B{匹配敏感规则?}
    B -->|是| C[正则提取+掩码替换]
    B -->|否| D[直通输出]
    C --> E[结构化日志对象]
    D --> E

第三章:分布式链路追踪在CS客户端的轻量化嵌入

3.1 OpenTelemetry SDK在资源受限客户端的裁剪与初始化优化

在嵌入式设备、IoT终端或低端移动设备中,OpenTelemetry SDK默认配置易引发内存溢出与启动延迟。需从依赖、组件与生命周期三方面协同裁剪。

裁剪策略优先级

  • 移除非必要导出器(如OTLP/gRPC,保留otlphttp+gzip压缩)
  • 禁用未启用的信号(仅启用traces,关闭metricslogs
  • 替换sync.Map为轻量map+读写锁(适用于单写多读场景)

初始化时序优化

sdktrace.NewTracerProvider(
    trace.WithSampler(trace.NeverSample()), // 启动阶段禁采样,降低CPU开销
    trace.WithSyncer(otlphttp.NewClient(
        otlphttp.WithEndpoint("collector:4318"),
        otlphttp.WithCompression(otlphttp.Gzip), // 减少传输体积30%+
    )),
)

WithSampler(NeverSample)避免初始化期间创建Span上下文;WithCompression(Gzip)将HTTP payload压缩率提升至65%,显著缓解弱网络下的阻塞。

组件 默认内存占用 裁剪后占用 降幅
TracerProvider ~1.2 MB ~380 KB 68%
Exporter Pool ~420 KB ~95 KB 77%
graph TD
    A[SDK Init] --> B{资源探测}
    B -->|Mem<2MB| C[启用精简模式]
    B -->|Else| D[加载全量组件]
    C --> E[跳过后台flush goroutine]
    C --> F[使用stack-based SpanContext]

3.2 跨进程/跨网络调用(HTTP/gRPC/WebSocket)的Span自动注入与传播

分布式追踪的核心挑战在于上下文跨边界连续性。OpenTracing 与 OpenTelemetry 均通过 TraceContext 在协议头中透传 trace-idspan-idtraceflags 等字段实现链路贯通。

HTTP 请求头传播

GET /api/v1/users HTTP/1.1
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE

traceparent 遵循 W3C Trace Context 标准:version-traceid-parentid-traceflagstracestate 支持多供应商元数据扩展。

gRPC 元数据注入(Go 示例)

// 客户端拦截器中注入 SpanContext
md := metadata.MD{}
propagator.Inject(context.Background(), otel.GetTextMapPropagator(), TextMapCarrier{md})
ctx = metadata.NewOutgoingContext(ctx, md)

TextMapCarrier 实现 TextMapPropagator.Inject() 接口,将当前 Span 的上下文序列化为键值对写入 gRPC metadata.MD

协议传播能力对比

协议 标准头支持 上下文透传方式 自动注入成熟度
HTTP/1.1 ✅ W3C traceparent header 高(框架广泛支持)
gRPC ✅ Metadata binary/text metadata 中高(需拦截器)
WebSocket ⚠️ 自定义 Sec-WebSocket-Protocol 或初始消息体 低(需手动集成)
graph TD
    A[Client Span] -->|Inject traceparent| B[HTTP Request]
    B --> C[Server Entry]
    C -->|Extract & Start New Span| D[Server Span]
    D -->|Propagate via gRPC MD| E[Downstream Service]

3.3 前端-客户端-后端全链路TraceID对齐与异常根因定位案例

数据同步机制

前端通过 X-Trace-ID 请求头透传 TraceID,客户端(如 Android/iOS SDK)自动注入,后端 Spring Cloud Sleuth 统一采样并注入 MDC。

// 在网关层统一生成/透传 TraceID
if (request.getHeader("X-Trace-ID") == null) {
    MDC.put("traceId", IdUtil.fastSimpleUUID()); // 生成唯一 traceId
} else {
    MDC.put("traceId", request.getHeader("X-Trace-ID"));
}

逻辑分析:避免多端 ID 不一致导致链路断裂;IdUtil.fastSimpleUUID() 保证低冲突、高吞吐;MDC 确保日志上下文绑定。

异常定位流程

graph TD
A[前端报错:支付超时] –> B{日志平台按 traceId 检索}
B –> C[客户端上报:网络请求 504]
B –> D[后端服务:DB 连接池耗尽]
C & D –> E[根因:订单服务 DB 连接泄漏]

关键字段映射表

端侧 字段名 示例值
Web 前端 X-Trace-ID tr-7f8a2b1c-d9e0-4a55
Android SDK trace_id 同上,自动注入
Spring Boot traceId (MDC) 同上,Logback 渲染

第四章:多维指标采集与Prometheus生态协同上报

4.1 客户端核心指标定义:连接状态、RTT、丢包率、内存/CPU占用、GC频次

客户端健康度监控依赖五类实时可观测指标,彼此耦合影响用户体验。

关键指标语义与采集方式

  • 连接状态CONNECTED/RECONNECTING/DISCONNECTED 三态机,由心跳超时与ACK确认联合判定
  • RTT(Round-Trip Time):基于双向时间戳的滑动窗口中位数,排除异常毛刺(>3σ)
  • 丢包率(sent - acked) / sent × 100%,每5秒聚合一次,支持QUIC与TCP双栈适配

内存与GC监控示例

// 通过JMX获取堆内GC频次(单位:次/分钟)
long gcCount = ManagementFactory.getGarbageCollectorMXBeans()
    .stream()
    .mapToLong(GarbageCollectorMXBean::getCollectionCount)
    .sum();

该值需结合G1YoungGenG1OldGen分代统计,避免误判Minor GC高频为内存泄漏。

指标 健康阈值 采集周期 异常响应动作
CPU占用 10s 触发线程快照分析
堆内存使用率 30s 启动弱引用清理
GC频次 60s 上报OOM风险等级

指标关联性建模

graph TD
    A[连接断开] --> B{RTT > 500ms?}
    B -->|Yes| C[触发链路探测]
    B -->|No| D[检查本地GC频次]
    D --> E[GC频次突增 → 内存压力]
    E --> F[降级非核心上报通道]

4.2 基于Prometheus Client Go定制轻量Exporter与指标生命周期管理

核心指标注册模式

使用 prometheus.NewRegistry() 隔离指标命名空间,避免全局污染;配合 promauto.With() 实现带标签自动注册,支持运行时动态注入实例标识。

指标生命周期控制

// 定义可回收的计数器(带自定义清理钩子)
var reqCounter = promauto.With(prometheus.Labels{"service": "api"}).NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total HTTP requests processed",
    },
    []string{"method", "status"},
)

// 手动注销(如服务热下线时)
func cleanupMetrics() {
    prometheus.Unregister(reqCounter) // 显式解注册,释放内存引用
}

Unregister() 是关键:它断开指标与注册表的弱引用,防止 Goroutine 泄漏和内存持续增长;promauto.With() 中的 Labels 仅影响新指标创建,不干预已注册实例。

指标状态迁移流程

graph TD
    A[初始化] --> B[注册至Registry]
    B --> C[采集周期内活跃]
    C --> D{服务退出/重载?}
    D -->|是| E[调用Unregister]
    D -->|否| C
    E --> F[GC可回收]

常见指标类型对比

类型 适用场景 是否支持原子增减 生命周期敏感度
Counter 累计事件(请求量)
Gauge 瞬时值(内存占用) 高(需显式清理)
Histogram 分位统计(延迟) ❌(仅Observe)

4.3 OTLP Metrics协议适配与与Prometheus Remote Write网关集成

OTLP(OpenTelemetry Protocol)Metrics 是云原生可观测性的标准传输格式,而 Prometheus 生态广泛依赖 Remote Write 协议。二者语义差异显著:OTLP 使用 MetricData 结构化模型(含 Sum, Gauge, Histogram 等数据点类型),而 Remote Write 仅支持 TimeSeries 数组及固定标签格式。

数据同步机制

需在网关层完成三重转换:

  • 时间戳对齐(OTLP 的 time_unix_nano → Prometheus 的毫秒级 timestamp
  • 类型映射(如 OTLP Sumis_monotonic 决定是否转为 Counter)
  • 标签归一化(resource.attributes + scope.attributes + metric.attributes 合并去重)

关键配置示例

# otel-collector exporter 配置(适配 Remote Write)
exporters:
  prometheusremotewrite:
    endpoint: "http://prom-gw:9090/api/v1/write"
    timeout: 5s
    # 自动将 OTLP metric name 转为 __name__ 标签
    send_timestamps: true

此配置启用时间戳透传,并触发 collector 内置的 otlp2prom 转换器;send_timestamps: true 是保证时序对齐的关键开关,否则 Prometheus 将使用接收时间,破坏原始采样一致性。

转换项 OTLP 字段 Remote Write 表现
指标名称 metric.name __name__ 标签
原始单位 metric.unit 注释 # UNIT(非标签)
单调性标识 sum.is_monotonic 影响 counter 类型判定
graph TD
  A[OTLP MetricData] --> B{Type Router}
  B -->|Sum| C[Convert to Counter/Gauge]
  B -->|Histogram| D[Expand Buckets → TimeSeries]
  C & D --> E[Label Merge & Timestamp Normalize]
  E --> F[Remote Write Batch]

4.4 指标标签维度建模(OS/Arch/Version/NetworkType)与高基数规避实践

指标打标需兼顾可查询性与存储效率。OS、Arch、Version、NetworkType 四类标签天然存在高基数风险——尤其 Version(如 1.23.4-alpha.7+build.20240521)和 NetworkType(含运营商自定义值如 CMCC-5G-VoLTE-Pro)。

标签归一化策略

  • OS:映射为枚举值(android, ios, windows, linux
  • Arch:标准化为 amd64, arm64, riscv64
  • Version:语义化截断为 MAJOR.MINOR(如 1.23),弃用 patch 及构建后缀
  • NetworkType:正则归并 5G|4G|WiFi|Unknown

示例:Go 中的标签压缩逻辑

func normalizeLabels(labels map[string]string) map[string]string {
    out := make(map[string]string)
    out["os"] = normalizeOS(labels["os"])        // android → "android"
    out["arch"] = normalizeArch(labels["arch"])  // arm64v8 → "arm64"
    out["version"] = semver.MajorMinor(labels["version"]) // "1.23.4" → "1.23"
    out["network"] = normalizeNetwork(labels["network"])  // "UNICOM-4G" → "4G"
    return out
}

semver.MajorMinor 调用 github.com/Masterminds/semver/v3 库,确保语义版本安全截断;normalizeNetwork 使用预编译正则缓存,避免重复编译开销。

基数控制效果对比

维度 原始基数均值 归一化后基数 降幅
Version 1,247 23 98.2%
NetworkType 389 7 98.2%
graph TD
    A[原始指标流] --> B{标签解析}
    B --> C[OS/Arch 归一化]
    B --> D[Version 截断]
    B --> E[NetworkType 正则归并]
    C & D & E --> F[低基数标签集]
    F --> G[TSDB 写入]

第五章:一体化可观测平台的价值闭环与演进方向

从故障响应到业务影响反推的闭环实践

某头部电商在大促期间接入一体化可观测平台后,将平均故障定位时间(MTTD)从 23 分钟压缩至 4.7 分钟。关键在于打通了链路追踪(OpenTelemetry)、指标(Prometheus + VictoriaMetrics)、日志(Loki + LogQL)与业务事件(订单履约状态变更、支付成功率波动)四维数据源。当支付失败率突增时,平台自动关联出下游风控服务 Pod 的 CPU Throttling 指标异常,并定位到其依赖的 Redis 集群连接池耗尽——该问题在传统单点监控中需跨 5 个系统人工拼接线索。

多租户 SLO 自动化治理机制

平台为 12 个核心业务域配置了差异化 SLO:例如“商品详情页首屏加载 P95 ≤ 800ms”,其达标率直接挂钩发布灰度开关。当 SLO 连续 15 分钟低于 99.5%,系统自动触发三阶段动作:① 冻结对应服务的新镜像部署;② 向负责人推送含 Flame Graph 和 Top-N 耗时 Span 的诊断报告;③ 将根因标签(如 db.query.slow: pg_orders)写入 CMDB 关联资产。过去半年,该机制拦截了 37 次潜在线上事故。

可观测性即代码(O11y-as-Code)落地形态

团队将可观测策略定义为 GitOps 流水线一等公民:

# slo/product-detail.yaml
service: product-detail
slo: p95_latency_ms
objective: "99.5"
window: "7d"
budget: 0.005
alert_on_burn_rate: 2.5

每次 PR 合并自动校验 SLO 策略与当前服务拓扑兼容性(如检测是否遗漏新引入的 gRPC 接口埋点),CI 阶段即生成 OpenTelemetry Collector 配置片段并注入到 Helm Chart 中。

AI 辅助根因推理的工程化集成

平台集成轻量化时序异常检测模型(Prophet + LSTM Ensemble),对 200+ 核心指标流实时计算残差分位数。当订单创建量骤降时,模型不仅标记 order_create_qps 异常,还输出因果图谱:

graph LR
A[CDN 缓存命中率↓12%] --> B[静态资源加载延迟↑300ms]
B --> C[前端 JS 错误率↑47%]
C --> D[下单按钮点击事件丢失]
D --> E[订单创建 QPS ↓68%]

该图谱由 3 类信号交叉验证生成:前端 RUM SDK 上报的 JS Error Stack、CDN 日志中的 cache-status 字段、以及后端服务调用链中缺失的 create_order Span。

演进阶段 技术特征 典型场景 数据时效性
基础聚合 ELK + Grafana 单点展示 服务器 CPU 报警 分钟级
深度关联 OpenTelemetry + eBPF 内核态采集 容器网络丢包定位 秒级
业务语义化 SLO 绑定业务 KPI + 自动生成 SLI 促销活动 ROI 实时归因 亚秒级

观测数据资产化运营

平台每日产出 1.2TB 原始观测数据,通过 Delta Lake 构建统一数据湖,支持按业务域隔离访问权限。风控团队基于历史告警与交易欺诈标签训练的异常模式识别模型,准确率提升至 92.3%,其特征工程直接复用平台已清洗的 http.status_codegrpc.codedb.latency_p99 等标准化字段。

混沌工程与可观测性协同验证

每月执行混沌演练时,平台自动注入故障并同步启动全链路观测:向 Istio Envoy 注入 200ms 网络延迟后,实时生成服务依赖热力图,标红显示 user-service → auth-service 调用链的错误率增幅达 17 倍,而 user-service → cache 链路无波动——这直接暴露了认证服务缺乏本地缓存兜底的设计缺陷。

开源组件与私有协议融合架构

针对 IoT 设备上报的私有二进制协议(TLV 格式),平台通过自研 Protocol Decoder 插件解析设备心跳帧,提取 battery_levelsignal_rssi 等字段并映射为 Prometheus 指标。该插件以 WebAssembly 模块形式运行于 Collector 中,支持热更新且内存占用低于 8MB,已在 5 万+ 边缘节点稳定运行。

传播技术价值,连接开发者与最佳实践。

发表回复

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