Posted in

Go语言GC调优的最后壁垒已被攻破?中科院软件所+华为云联合团队首发:基于eBPF的实时GC行为观测框架

第一章:Go语言GC调优的范式革命

传统垃圾回收调优常陷入“参数试探—观察抖动—反复调整”的经验循环,而Go自1.20起将GC模型从“并发标记-清除”深度重构为基于软目标(soft goal)的增量式混合屏障驱动回收器,其核心范式已从“控制停顿时间”转向“协同应用负载节奏”。这一转变要求开发者放弃对GOGC魔数的执念,转而以内存增长率、分配速率与实时性约束为三维坐标系进行建模。

GC行为可观测性的根本升级

Go运行时暴露了更细粒度的指标接口:runtime.ReadMemStats()返回的NextGC字段不再代表硬阈值,而是预测下次GC启动的内存占用目标;GCCPUFraction被废弃,取而代之的是通过debug.SetGCPercent(-1)可完全禁用自动触发,强制由runtime.GC()debug.FreeOSMemory()显式驱动——这使GC真正成为应用逻辑的协作者而非干扰者。

关键调优指令与验证流程

启用实时监控需在程序启动时注入以下初始化代码:

import "runtime/debug"
// 启用详细GC日志(仅开发/压测环境)
debug.SetGCPercent(100) // 设定初始目标:堆增长100%时触发GC
// 同时开启pprof GC trace
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

执行后访问 http://localhost:6060/debug/pprof/gc 可获取每次GC的精确耗时、标记阶段CPU占比及辅助堆分配量。生产环境应改用GODEBUG=gctrace=1环境变量启动,其输出格式为:gc # @#s #%%: #+#+# ms clock, #+#/#/#+#+#+# ms cpu, #->#-># MB, # MB goal, # P,其中第三段#->#-># MB分别表示GC前堆大小、标记结束时存活对象大小、GC后堆大小。

从指标到决策的典型路径

观察现象 根本原因 推荐动作
next_gc持续远低于heap_inuse 分配速率过高或对象生命周期过长 引入对象池复用,检查goroutine泄漏
GC CPU时间占比>25% 标记工作线程不足 增加GOMAXPROCS或优化指针密度
heap_idle长期>50% 内存未及时归还OS 调用debug.FreeOSMemory()主动释放

范式革命的本质,是将GC从黑盒调度器转变为可编程的内存生命周期管理器。

第二章:eBPF赋能Go运行时观测的底层原理

2.1 eBPF程序在Go GC关键路径上的插桩机制

Go运行时GC关键路径(如gcStartsweeponemarkroot)通过runtime/trace/proc/self/maps暴露符号地址,eBPF程序利用kprobe/uprobe动态插桩。

插桩点选择策略

  • runtime.gcStart: 标记STW起点,获取gctrace上下文
  • runtime.markroot: 捕获根扫描延迟,关联P ID与标记阶段
  • runtime.sweepone: 监测堆内存回收速率,采样span状态

核心eBPF代码片段

// uprobe entry at runtime.markroot
SEC("uprobe/markroot")
int trace_markroot(struct pt_regs *ctx) {
    u64 pid = bpf_get_current_pid_tgid();
    u32 root_idx = (u32)PT_REGS_PARM2(ctx); // root job index
    bpf_map_update_elem(&markroot_hist, &pid, &root_idx, BPF_ANY);
    return 0;
}

逻辑分析:PT_REGS_PARM2(ctx)提取第2个调用参数(root_idx),反映当前扫描的根类型(栈/全局/MSpan);markroot_histBPF_MAP_TYPE_HASH映射,用于跨事件关联GC阶段与goroutine行为。

映射类型 键类型 值类型 用途
markroot_hist u64 u32 关联PID与根扫描序号
gc_phase_ts u32 struct ts 记录各GC阶段时间戳
graph TD
    A[uprobe on markroot] --> B{读取 root_idx}
    B --> C[更新 markroot_hist]
    C --> D[用户态 Go 程序轮询 map]
    D --> E[聚合 per-P 标记耗时分布]

2.2 Go runtime trace与eBPF事件协同采集的实践验证

数据同步机制

为避免时钟漂移导致事件错位,采用单调时钟对齐:Go trace 使用 runtime.nanotime(),eBPF 使用 bpf_ktime_get_ns(),二者均基于 CLOCK_MONOTONIC

协同采集代码示例

// 启动 Go trace 并注入 eBPF 时间戳锚点
trace.Start(os.Stderr)
bpfModule.MustLoad() // 加载含 kprobe/sys_enter_write 的 eBPF 程序
bpfModule.Attach()   // 关联到 write 系统调用入口

该代码确保 trace 事件流与 eBPF hook 在同一生命周期内激活;MustLoad() 验证 BTF 兼容性,Attach() 绑定至内核符号,避免采样丢失。

事件关联策略

字段 Go trace 来源 eBPF 来源
Timestamp nanotime() bpf_ktime_get_ns()
PID/TID goroutine ID + OS thread ID bpf_get_current_pid_tgid()
Stack Trace runtime/pprof bpf_get_stack() (with BPF_F_USER_STACK)
graph TD
    A[Go App] -->|runtime.traceEvent| B(Go trace Writer)
    A -->|sys_enter_write| C[eBPF kprobe]
    C --> D[BPF map: event_ringbuf]
    B --> E[Trace File]
    D --> F[Userspace Consumer]
    E & F --> G[Unified Timeline Viewer]

2.3 基于perf_event和uprobe的GC阶段毫秒级时序对齐方法

为实现JVM GC事件与内核调度行为的精确时序对齐,需突破传统日志采样(如jstat-XX:+PrintGCDetails)的毫秒级延迟瓶颈。

核心机制

利用uprobelibjvm.soG1CollectedHeap::do_collection_pause_at_safepoint等关键函数入口埋点,结合perf_event_open系统调用注册高精度硬件时间戳(CLOCK_MONOTONIC_RAW)。

关键代码示例

// 注册uprobe事件(需root权限)
int fd = perf_event_open(&(struct perf_event_attr){
    .type           = PERF_TYPE_UPROBE,
    .config         = 0,
    .uprobes_path   = "/usr/lib/jvm/java-17-openjdk-amd64/lib/server/libjvm.so",
    .probe_offset   = 0x1a2b3c, // 符号解析后偏移
    .sample_type    = PERF_SAMPLE_TID | PERF_SAMPLE_TIME,
    .disabled       = 1
}, 0, -1, -1, 0);
ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);

probe_offset需通过readelf -s libjvm.so | grep do_collection_pause动态获取;PERF_SAMPLE_TIME启用纳秒级时间戳,误差ioctl(…, PERF_EVENT_IOC_ENABLE)确保事件实时激活。

时序对齐流程

graph TD
    A[uprobe触发] --> B[perf kernel ring buffer写入]
    B --> C[用户态mmap读取]
    C --> D[与/proc/pid/schedstat比对]
    D --> E[生成GC phase timeline]

对齐精度对比

方法 时间分辨率 GC起始偏差 依赖条件
-XX:+PrintGCTimeStamps 10ms ±8ms JVM日志缓冲
perf + uprobe 32ns ±120ns root + debuginfo

2.4 GC标记、清扫、辅助回收等阶段的eBPF可观测性建模

Go 运行时 GC 的关键阶段(标记、清扫、辅助回收)可通过 eBPF 精准捕获其生命周期事件与资源开销。

核心观测点设计

  • gcStart/gcDone:标记周期起止,关联 gctrace 语义
  • markAssistStart/markAssistDone:辅助标记触发时机与持续时间
  • sweepDone:清扫完成点,反映内存释放延迟

eBPF 探针示例(基于 uprobe

// uprobe at runtime.gcStart, read GC phase metadata
int trace_gc_start(struct pt_regs *ctx) {
    u64 ts = bpf_ktime_get_ns();
    u32 phase = 0;
    bpf_probe_read_user(&phase, sizeof(phase), (void *)GO_GC_PHASE_ADDR);
    bpf_map_update_elem(&gc_events, &ts, &phase, BPF_ANY);
    return 0;
}

逻辑分析:该探针挂载于 runtime.gcStart 符号,通过 bpf_probe_read_user 安全读取 Go 运行时全局 GC 阶段变量;GO_GC_PHASE_ADDR 需在编译期通过 objdump 提取,确保跨版本兼容性。映射 gc_events 存储时间戳→阶段映射,供用户态聚合分析。

GC 阶段事件语义对照表

阶段名称 触发条件 可观测指标
标记启动 gcStart 调用 STW 持续时间、P 栈深度
辅助标记 mutator 触发 markAssist 协程阻塞时长、标记页数
清扫完成 sweepDone 执行完毕 内存归还延迟、mheap.lock 竞争

GC 事件流拓扑(简化)

graph TD
    A[mutator 分配触发 GC] --> B[gcStart → STW]
    B --> C[并发标记]
    C --> D{是否需 assist?}
    D -->|是| E[markAssistStart → 用户 Goroutine 暂停]
    D -->|否| F[后台标记线程]
    E --> G[markAssistDone]
    C --> H[sweepDone → 内存释放]

2.5 中科院软件所+华为云联合框架的内核模块与用户态解析器协同设计

该框架采用“内核轻量卸载 + 用户态智能解析”双层协同范式,突破传统单侧处理瓶颈。

协同架构核心组件

  • 内核模块 kparser_core:负责协议头快速识别、流状态标记与零拷贝内存映射
  • 用户态解析器 uapd(User-space Analysis & Dispatch):基于 eBPF 辅助校验,执行深度语义解析与策略决策

数据同步机制

// 内核侧共享环形缓冲区定义(简化)
struct __attribute__((packed)) parse_task {
    __u32 flow_id;
    __u16 pkt_offset;   // 原始包在page中的偏移
    __u8  proto_hint;   // 预判协议类型(0=unknown, 1=HTTP, 2=TLS)
};

逻辑分析:pkt_offset 避免数据复制,proto_hint 由内核首包启发式推断生成,降低用户态误解析率;字段对齐保障跨架构内存访问一致性。

模块 职责 延迟开销 安全边界
kparser_core 流分类、内存零拷贝分发 内核态隔离
uapd TLS握手解密、HTTP/3帧解析 ~12μs seccomp-bpf沙箱
graph TD
    A[原始网卡包] --> B[kparser_core:协议识别+标记]
    B --> C[共享ringbuf分发]
    C --> D[uapd:eBPF校验+深度解析]
    D --> E[策略引擎/日志/转发]

第三章:真实业务场景下的GC行为诊断实战

3.1 高并发微服务中STW异常延长的根因定位(含阿里云电商案例)

现象还原:大促期间GC停顿飙升至2.8s

阿里云某电商核心订单服务在双11压测中,G1 GC的STW时间从常规80ms骤增至2.8s,触发SLA告警。JVM参数为 -XX:+UseG1GC -Xmx8g -XX:MaxGCPauseMillis=200

根因聚焦:并发标记阶段的跨代引用扫描瓶颈

G1需扫描老年代对象对新生代的引用(Remembered Set更新),但大量异步消息回调(如RocketMQ Listener)在Old Gen持有短期存活的Lambda闭包,导致RSet写屏障高频触发与扫描放大。

// 订单回调中隐式逃逸的闭包(问题代码)
orderService.process(order).thenAccept(result -> {
    metrics.recordSuccess(); // 持有外部metrics引用 → 闭包晋升至Old Gen
});

逻辑分析:thenAccept 创建的BiConsumer实例在首次执行后未被及时回收,因metrics为单例长生命周期对象,导致该闭包被G1判定为长期存活,强制晋升至老年代;后续每次Young GC均需扫描其RSet中指向Eden区的引用,而该RSet条目达12万+,成为扫描热点。

关键证据链

指标 正常值 异常值 说明
G1MixedGCCount 3–5次/分钟 47次/分钟 混合回收频次激增
RSetScanTime 12ms 1.9s G1日志中[RS scan]耗时占比超93%

优化路径

  • ✅ 升级JDK17并启用-XX:+UseZGC(亚毫秒级STW)
  • ✅ 重构回调为无状态函数式接口,避免闭包持长生命周期引用
  • ✅ 增加-XX:G1RSetUpdatingPauseTimePercent=10动态调优RSet更新线程配额
graph TD
    A[Young GC触发] --> B{扫描Remembered Set}
    B --> C[定位12万+跨代引用条目]
    C --> D[发现Old Gen中Lambda闭包持有metrics引用]
    D --> E[闭包无法在Young GC中回收→持续拖累RSet扫描]

3.2 内存碎片化导致的频繁scavenge与eBPF内存页追踪实操

当Go运行时在小对象密集分配场景下遭遇高内存碎片,堆中大量不连续空闲页会触发高频scavenge(内存回收),反而加剧延迟抖动。

eBPF页级分配追踪

使用bpftrace实时捕获mm_page_alloc事件,定位碎片源头:

# 追踪每页分配的order与node信息
sudo bpftrace -e '
kprobe:mm_page_alloc {
  printf("page@%p order=%d node=%d\n", 
         arg0, (int)args->order, (int)args->migratetype);
}'

arg0为分配页起始地址;order表示2^order个连续页,order=0即4KB页;migratetype反映页迁移类型(如MIGRATE_UNMOVABLE易致碎片)。

关键指标对比

指标 碎片严重时 优化后
平均scavenge间隔 87ms >1200ms
order≥3分配占比 23%

内存页生命周期简图

graph TD
  A[alloc_pages] --> B{order == 0?}
  B -->|Yes| C[fast path: buddy]
  B -->|No| D[slow path: fragmentation-prone]
  D --> E[scavenge triggered]

3.3 Goroutine泄漏引发的GC压力误判:从pprof盲区到eBPF实时反演

Goroutine泄漏常被误读为GC频繁触发——因pprof goroutines profile仅捕获活跃栈快照,无法关联生命周期与阻塞根源。

pprof的静态盲区

  • 仅采样当前运行/阻塞的 goroutine,忽略已泄漏但未调度的“僵尸协程”
  • runtime.ReadMemStats 显示 NumGC 上升,实为 GC 被 runtime.GC() 显式调用或堆增长触发,与 goroutine 数量无直接因果

eBPF 实时反演关键路径

// trace_goroutine_spawn.c(eBPF C)
SEC("tracepoint/sched/sched_create_thread")
int trace_create(struct trace_event_raw_sched_create_thread *ctx) {
    u64 pid = bpf_get_current_pid_tgid() >> 32;
    bpf_map_update_elem(&spawn_ts, &pid, &ctx->common_timestamp, BPF_ANY);
    return 0;
}

逻辑分析:通过 sched_create_thread tracepoint 捕获 goroutine 创建时间戳;spawn_ts map 存储 PID→创建纳秒时间,后续结合 sched_switchgo:gc_start 事件交叉比对存活时长。参数 ctx->common_timestamp 提供高精度单调时钟,规避 gettimeofday 时钟漂移。

诊断维度对比表

维度 pprof goroutines eBPF 实时追踪
时间分辨率 秒级采样 纳秒级事件戳
生命周期覆盖 仅瞬时快照 创建→阻塞→消亡全链路
泄漏判定依据 栈深度+状态 存活时长 > 5min + 零调度

graph TD A[Go程序运行] –> B{pprof采集} B –> C[goroutine快照] C –> D[误判GC压力源] A –> E[eBPF attach tracepoints] E –> F[spawn/switch/gc_start事件流] F –> G[时序图反演泄漏goroutine] G –> H[精准定位阻塞channel/lock]

第四章:面向生产环境的GC调优闭环体系建设

4.1 基于eBPF观测数据的GC参数动态推荐引擎(GOGC/GOMEMLIMIT自适应)

该引擎通过eBPF实时采集Go运行时内存分配速率、堆增长斜率、GC暂停分布及页回收延迟等指标,驱动参数闭环调优。

数据同步机制

eBPF程序每200ms将聚合指标推至用户态ring buffer,由Go守护进程消费:

// eBPF map key-value 结构(用户态读取示例)
type GCStats struct {
    AllocRateMBPS uint32 `btf:"alloc_rate_mbps"` // 每秒新分配MB数
    HeapGrowthSlope float64 `btf:"heap_growth_slope"` // 堆大小变化率(MB/s)
    PauseP95NS    uint64 `btf:"pause_p95_ns"`       // GC暂停P95(纳秒)
}

AllocRateMBPS用于估算GOGC触发频率;HeapGrowthSlope结合GOMEMLIMIT当前值,预判OOM风险窗口;PauseP95NS触发保守降GOGC策略。

推荐策略决策流

graph TD
A[实时指标输入] --> B{HeapGrowthSlope > 8MB/s?}
B -->|是| C[下调GOGC至75]
B -->|否| D{PauseP95NS > 5ms?}
D -->|是| E[上调GOMEMLIMIT 10%]
D -->|否| F[维持当前参数]

参数映射表

场景特征 GOGC建议值 GOMEMLIMIT调整量
高分配率 + 高暂停延迟 50 +15%
稳态低增长 + 低暂停 120 -5%
内存压力突增(eBPF detect) 30 +25%

4.2 混沌工程注入下GC行为韧性评估的自动化SLO校验流程

在混沌实验触发JVM内存扰动后,需实时捕获GC指标并校验SLO合规性。核心流程由三部分协同驱动:

数据采集与特征提取

通过JMX暴露GarbageCollectorMXBean,采集CollectionCountCollectionTimeLastGcInfo.duration等关键时序信号。

// 启用低开销GC事件监听(JDK11+)
final var jvm = ManagementFactory.getPlatformMXBean(GarbageCollectorMXBean.class);
// 注册GC完成通知回调,避免轮询开销
NotificationEmitter emitter = (NotificationEmitter) jvm;
emitter.addNotificationListener((n, h) -> {
  if ("jvm.gc.completed".equals(n.getType())) { // JDK17+ GC event name
    long durationMs = ((CompositeData) n.getUserData()).get("duration");
    // → 推送至时序数据库(如Prometheus + OpenTelemetry Exporter)
  }
}, null, null);

逻辑说明:采用事件驱动替代周期采样,降低监控本身对GC行为的干扰;duration单位为毫秒,精度达纳秒级,满足P99延迟SLO校验需求。

SLO自动校验决策流

graph TD
  A[混沌注入:Heap压力] --> B[实时GC指标流]
  B --> C{P99 GC Pause ≤ 200ms?}
  C -->|Yes| D[标记SLO达标]
  C -->|No| E[触发熔断告警 + 自愈策略]

校验结果映射表

SLO指标 阈值 校验周期 失败容忍窗口
GC Pause P99 ≤200ms 30s 连续2次
Full GC频次 ≤1/5min 1min 单次即告警

4.3 与OpenTelemetry生态集成:GC指标注入Prometheus与Grafana深度看板构建

OpenTelemetry Collector 通过 prometheusremotewrite exporter 将 JVM GC 指标(如 jvm_gc_collection_seconds_count)持久化至 Prometheus。

数据同步机制

OTel Collector 配置示例:

exporters:
  prometheusremotewrite:
    endpoint: "http://prometheus:9090/api/v1/write"
    headers:
      Authorization: "Bearer ${PROM_TOKEN}"

该配置启用远程写协议,将 OTel Metrics 转为 Prometheus 格式;Authorization 头支持多租户鉴权,endpoint 必须匹配 Prometheus 的 remote_write 接收地址。

Grafana 看板关键指标维度

指标名 语义 标签筛选建议
jvm_gc_collection_seconds_count{gc="G1 Young Generation"} 年轻代GC次数 job="app-prod", instance=~"svc-.*"
jvm_gc_pause_seconds_sum GC暂停总时长 quantile="0.99"

流程概览

graph TD
  A[JVM Agent] -->|OTLP/metrics| B[OTel Collector]
  B --> C[prometheusremotewrite]
  C --> D[Prometheus TSDB]
  D --> E[Grafana Dashboard]

4.4 华为云Stack环境下多租户Go应用GC性能基线建模与横向对比分析

在华为云Stack私有云多租户场景中,各租户Go应用共享底层计算资源,但受CPU配额、内存限制及NUMA拓扑影响,GC行为呈现显著差异。

实验配置标准化

  • 统一使用 Go 1.22.5 编译,GOGC=100,禁用GODEBUG=gctrace=1
  • 每租户独占 2vCPU + 4GiB 内存(cgroups v2 memory.max + cpu.max)

GC关键指标采集脚本

# 从/proc/PID/status提取实时GC数据(需root权限)
awk '/^VmRSS:/ {rss=$2} /^Mems_allowed:/ {numa=$0} END {print "RSS:", rss, "KB; NUMA:", numa}' /proc/$(pgrep myapp)/status

该命令精准捕获进程驻留内存与NUMA亲和性,避免top等工具的采样延迟干扰基线建模。

横向对比结果(单位:ms)

租户类型 平均GC停顿 GC频率(次/分钟) P95停顿漂移
高优先级 12.3 8 ±1.7
默认租户 28.6 14 ±9.4

性能归因流程

graph TD
    A[租户CPU配额不足] --> B[STW期间GMP调度延迟]
    C[内存限流触发soft memory limit] --> D[提前触发Mark Assist]
    B & D --> E[停顿时间非线性增长]

第五章:中国原创技术栈的全球影响与未来演进

开源社区中的真实渗透力

Apache SkyWalking 作为由华为工程师主导孵化的国产可观测性项目,已连续五年位列 Apache 基金会 Top 5 活跃度项目。截至2024年Q2,其 GitHub Star 数突破28,700,全球贡献者达1,243人,其中德国、美国、印度开发者占比分别达18.6%、15.3%和12.1%。在eBay生产环境的微服务集群中,SkyWalking 替代了原有Prometheus+Grafana组合,将分布式追踪延迟降低41%,日志采样开销下降至原方案的1/7。

工业级数据库的出海实践

OceanBase 在东南亚数字银行Grab Financial Group核心账务系统中完成全量迁移,支撑单日峰值3.2亿笔交易,TPC-C基准测试达7.07亿tpmC(2023年实测)。其多租户隔离机制与金融级强一致性保障,使Grab在印尼央行合规审计中一次性通过“实时账务双写一致性”专项检查。下表对比了其与传统Oracle RAC在关键指标上的实测表现:

指标 OceanBase v4.3 Oracle RAC 19c 优势幅度
跨机房故障恢复时间 8.3秒 142秒 ↓94.2%
1TB数据在线DDL耗时 217秒 1,840秒 ↓88.2%
每TB存储年运维成本 $1,280 $4,950 ↓74.1%

AI基础设施层的范式转移

昇思MindSpore 2.3版本引入动态图编译优化器,在华为云ModelArts平台实测中,ResNet-50训练吞吐量提升2.4倍,且支持自动混合精度与梯度压缩联合调度。新加坡AI初创公司NexGenVision采用该框架重构其医疗影像分割模型训练流程,将CT肿瘤标注模型迭代周期从14天压缩至3.5天,GPU显存占用降低至PyTorch同配置的62%。

全球标准制定中的技术话语权

中国信通院牵头制定的《可信人工智能开发框架评估规范》(YD/T 4512-2023)已被ISO/IEC JTC 1 SC 42采纳为国际标准草案。该规范首次将“模型血缘可追溯性”“推理链路不可篡改性”列为强制性要求,直接推动Linux基金会LF AI & Data旗下Acumos项目新增区块链存证模块。在2024年柏林OpenInfra峰会现场,基于龙蜥Anolis OS构建的OpenStack Wallaby集群成功演示了跨大洲节点的联邦学习任务调度,延迟抖动控制在±17ms内。

graph LR
    A[杭州阿里云数据中心] -->|加密RPC调用| B(深圳腾讯云边缘节点)
    B -->|联邦聚合结果| C[新加坡星展银行AI实验室]
    C -->|模型签名回传| A
    style A fill:#4A90E2,stroke:#1a56db
    style B fill:#50C878,stroke:#228B22
    style C fill:#FF6B6B,stroke:#d32f2f

开发者生态的底层重构

Rust语言中文社区2024年度报告显示,TiKV核心模块贡献者中,中国开发者占比达63%,其主导的raft-engine存储引擎已成Rust生态事实标准。在GitHub上,由PingCAP工程师发起的rust-rocksdb fork分支被Databricks、Coinbase等217家海外企业生产环境引用,相关PR合并平均响应时间缩短至4.2小时——低于Rust官方维护团队均值6.8小时。

硬件协同创新的闭环验证

寒武纪MLU370加速卡与飞腾FT-2000+/64处理器组成的国产AI服务器,在东京大学机器人实验室部署后,实现YOLOv8s模型推理延迟稳定在12.4ms(@batch=1),功耗比NVIDIA T4低38%。该方案已通过日本JIS X 0129-2022信息安全认证,并成为日本JST(科学技术振兴机构)“智能工厂边缘AI”专项唯一指定硬件平台。

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

发表回复

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