Posted in

Go语言实现vMotion热迁移监控系统:实时追踪、告警、回滚三件套

第一章:Go语言实现vMotion热迁移监控系统:实时追踪、告警、回滚三件套

现代虚拟化平台中,vMotion热迁移虽保障业务连续性,但缺乏细粒度可观测性易导致隐性风险——迁移卡顿、网络抖动、存储延迟超标等常被日志淹没。本系统基于Go语言构建轻量级监控代理,直连vCenter REST API与vSphere SDK for Go,实现毫秒级状态采集、策略驱动告警与原子化回滚闭环。

核心架构设计

系统采用三层解耦结构:

  • 采集层:并发轮询/rest/vcenter/vm/{vm_id}/guest/heartbeat/rest/vcenter/vm/{vm_id}/migration端点,采样间隔可配置(默认200ms);
  • 决策层:基于迁移阶段(preparing → migrating → committing → done)与性能指标(CPU ready time > 50ms、network latency > 15ms、disk I/O latency > 100ms)触发多级告警;
  • 执行层:调用POST /rest/vcenter/vm/{vm_id}/migration/cancel强制终止异常迁移,并通过GET /rest/vcenter/vm/{vm_id}/power校验电源状态确保回滚生效。

实时追踪实现

以下Go代码片段启动迁移状态监听器,自动关联VM名称与迁移任务ID:

func watchMigration(ctx context.Context, client *rest.Client, vmID string) {
    ticker := time.NewTicker(200 * time.Millisecond)
    defer ticker.Stop()
    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            // 获取当前迁移任务(仅返回active状态)
            resp, _ := client.Get(fmt.Sprintf("/rest/vcenter/vm/%s/migration", vmID))
            var migration struct {
                Value []struct {
                    MigrationID string `json:"migration"`
                    State       string `json:"state"` // "RUNNING", "SUCCESS", "ERROR"
                    Progress    int    `json:"progress"`
                } `json:"value"`
            }
            json.Unmarshal(resp.Body, &migration)
            if len(migration.Value) > 0 && migration.Value[0].State == "RUNNING" {
                log.Printf("[INFO] VM %s migrating: %d%%", vmID, migration.Value[0].Progress)
            }
        }
    }
}

告警与回滚策略

告警阈值通过YAML配置管理,支持动态热加载:

指标类型 阈值 告警等级 回滚动作
迁移耗时 >180s CRITICAL 自动取消迁移任务
网络丢包率 >3% WARNING 发送Slack通知并标记
目标主机CPU负载 >95% ERROR 强制回滚至源主机

当检测到CRITICAL事件时,系统同步执行回滚并记录审计日志至本地SQLite数据库,确保操作可追溯。

第二章:vMotion底层机制与Go语言对接原理

2.1 VMware vSphere API架构解析与Go SDK选型对比

vSphere 提供三层 API 接口:SOAP(传统)、REST (vCenter 6.5+ 新增) 和 vSphere Automation SDK(基于 OpenAPI)。其中 Go 生态主要依赖 SOAP 协议栈,因 REST API 功能覆盖尚不完整(如无法创建 Datastore)。

主流 Go SDK 对比

SDK 名称 维护状态 依赖协议 并发支持 社区活跃度
govmomi 活跃(VMware 官方维护) SOAP over HTTPS ✅ 原生支持 高(GitHub ⭐ 2.4k+)
vsphere-go-sdk 归档(已停止更新) REST ❌ 无连接池

初始化客户端示例(govmomi)

import "github.com/vmware/govmomi"

client, err := govmomi.NewClient(ctx, &url.URL{
    Scheme: "https",
    Host:   "vc.example.com",
    Path:   "/sdk",
}, true) // true = skip TLS verify
if err != nil {
    panic(err)
}

NewClient 构造函数需传入 *url.URL 实例,Path="/sdk" 是 vSphere SOAP 端点固定路径;true 参数启用证书跳过,仅限测试环境——生产必须配置 tls.Config 并校验 CA。

架构通信流向

graph TD
    A[Go App] -->|SOAP XML over HTTPS| B[vCenter SOAP Provider]
    B --> C[Managed Object Browser MOB]
    C --> D[ESXi Hosts / Datastores / VMs]

2.2 vMotion生命周期事件捕获:Task、Event与PropertyCollector实战

vMotion 迁移过程产生丰富可观测信号,需协同利用三类机制实现精准捕获。

事件捕获的三层能力对比

机制 实时性 粒度 持久性 典型用途
Task 异步 操作级 短期 迁移启停状态跟踪
Event 准实时 阶段级 中期 DrsVmMigratedEvent
PropertyCollector 毫秒级 属性级(如 runtime.powerState 持久可轮询 迁移中虚拟机状态流监控

PropertyCollector 实战代码示例

# 创建 PropertyCollector 并监听 runtime.host 和 runtime.powerState 变化
pc = si.content.propertyCollector
obj_spec = vim.PropertyCollector.ObjectSpec(obj=vm, syncMode=True)
prop_spec = vim.PropertyCollector.PropertySpec(
    type=vim.VirtualMachine,
    pathSet=["runtime.host", "runtime.powerState"],
    all=False
)
filter_spec = vim.PropertyCollector.FilterSpec(
    objectSet=[obj_spec], propSet=[prop_spec]
)
pc_filter = pc.CreateFilter(filter_spec, True)

该代码注册一个动态过滤器,仅订阅虚拟机运行时关键属性变更;syncMode=True 确保首次获取全量快照,后续仅推送 delta;pathSet 明确限定字段,避免带宽与解析开销。

数据同步机制

graph TD
    A[vMotion 开始] --> B[Task 状态变为 'running']
    B --> C[Event: DrsVmMigratingEvent]
    C --> D[PropertyCollector 推送 host 变更]
    D --> E[Event: DrsVmMigratedEvent]

2.3 Go协程安全的实时事件流处理模型设计

为支撑高并发、低延迟的事件消费,本模型采用“生产者-分发器-工作协程池”三级架构,所有共享状态均通过 sync.Map 或通道(channel)隔离。

核心组件职责

  • 生产者:通过无缓冲 channel 向分发器推送 Event 结构体
  • 分发器:基于事件类型哈希选择工作协程,避免锁竞争
  • 工作协程池:每个协程独占 *sync.Mutex 保护的本地状态,不共享写入资源

事件结构定义

type Event struct {
    ID        string    `json:"id"`        // 全局唯一,用于幂等校验
    Type      string    `json:"type"`      // "user_login", "payment_success"
    Payload   []byte    `json:"payload"`   // 原始JSON载荷
    Timestamp time.Time `json:"timestamp"` // 服务端接收时间
}

ID 是幂等性基石;Type 决定路由策略;Payload 延迟解析以降低分发开销;Timestamp 支持乱序检测与水印推进。

协程安全保障机制

机制 作用 实现方式
通道背压 防止生产者过载 使用带缓冲 channel(cap=1024)
本地状态分片 消除写竞争 Type % N 映射到独立协程
原子计数器 实时监控吞吐与积压 atomic.Int64 记录待处理数
graph TD
    A[Producer] -->|Event| B{Dispatcher}
    B --> C[Worker #1]
    B --> D[Worker #2]
    B --> E[Worker #N]
    C --> F[Local State + Mutex]
    D --> G[Local State + Mutex]
    E --> H[Local State + Mutex]

2.4 迁移上下文元数据建模:虚拟机状态、源/目标主机、网络拓扑同步

迁移上下文元数据是热迁移可靠性的核心契约,需原子化表达三类关键维度:

  • 虚拟机运行时状态:CPU寄存器快照、内存脏页位图、设备I/O队列
  • 宿主环境上下文:源/目标主机的CPU微架构兼容性、NUMA节点拓扑、KVM/QEMU版本指纹
  • 网络拓扑一致性:vNIC绑定的OVS桥ID、VLAN/VXLAN标签、ARP表项生命周期

数据同步机制

采用双阶段元数据提交协议,保障跨组件视图一致:

# migration_context.py —— 上下文快照生成逻辑
def capture_migration_context(vm_id: str) -> dict:
    return {
        "vm_state": {
            "cpu_regs": qemu_monitor("info registers"),  # 寄存器快照(含RIP/RSP)
            "dirty_bitmap": get_dirty_pages(vm_id, "bitmap")  # 内存脏页位图(按2MB大页对齐)
        },
        "host_topology": {
            "source": get_host_fingerprint("src-host-01"),  # 返回{cpu_model, numa_nodes, kvm_version}
            "target": get_host_fingerprint("dst-host-03")
        },
        "network_mapping": resolve_vnic_mappings(vm_id)  # 映射vNIC→物理端口+VLAN+QoS策略
    }

get_host_fingerprint() 提取 /sys/devices/system/cpu/cpu*/topology//proc/sys/kernel/kptr_restrict 等内核态特征;resolve_vnic_mappings() 查询libvirt XML + OVSDB,确保目标端口具备相同VLAN trunk能力。

元数据校验流程

graph TD
    A[采集源主机上下文] --> B[序列化为Protobuf v3]
    B --> C[签名哈希并广播至目标主机]
    C --> D{目标校验通过?}
    D -- 是 --> E[加载网络策略并预分配资源]
    D -- 否 --> F[中止迁移,返回不兼容码0x7E]
字段 类型 必填 说明
vm_state.memory_size_mb uint32 实际分配内存,非预留值,影响脏页传输窗口
host_topology.target.numa_distance[0][1] uint8 若非0,触发NUMA亲和性重映射
network_mapping.vlan_tag uint16 与目标OVS port trunk配置强匹配

2.5 性能压测与延迟敏感型指标采集(如CPU/Mem迁移速率、停机时间)

关键指标定义与采集粒度

  • CPU迁移速率:单位时间内vCPU在物理核间切换次数(次/秒),反映调度抖动;
  • 内存迁移带宽:热迁移阶段每秒传输的脏页数据量(MB/s);
  • 停机时间(Downtime):从暂停源VM到目标VM恢复执行的时间窗口(μs级精度必需)。

实时采集工具链

# 使用perf record捕获微秒级停机事件(KVM/QEMU环境)
perf record -e 'kvm:kvm_exit' -C 0 -g -- sleep 0.1
# -e 指定KVM退出事件(含vCPU暂停点);-C 0 绑定到主调度核确保时序保真

该命令精准捕获KVM退出上下文,其中kvm_exit事件在vCPU暂停瞬间触发,配合--sleep 0.1短窗口可规避长周期噪声干扰。

延迟敏感型指标对比表

指标 采集方式 推荐采样率 允许抖动阈值
停机时间 perf + tracepoint 100 kHz
CPU迁移速率 /proc/sched_debug 10 Hz ±3%
内存迁移速率 QEMU query-migrate 1 Hz ±8%

数据同步机制

graph TD
    A[QEMU Monitor] -->|JSON-RPC| B{采集代理}
    B --> C[Ring Buffer]
    C --> D[内核eBPF过滤器]
    D --> E[用户态聚合器]
    E --> F[TSDB写入]

eBPF过滤器在内核态完成毫秒级停机事件聚合,避免用户态上下文切换引入的测量偏差。

第三章:实时追踪引擎核心实现

3.1 基于vim25.Client的增量事件轮询与WebSocket长连接双模架构

数据同步机制

为兼顾兼容性与实时性,架构采用双模事件获取策略:vSphere 6.7+ 优先启用 WebSocket 长连接;旧版本回退至基于 vim25.Client.WaitForUpdatesEx() 的增量轮询。

模式选择逻辑

def select_event_mode(client: vim25.Client, api_version: str) -> str:
    # 检查服务端是否支持 eventHub(vSphere 6.7+)
    if hasattr(client.service_content, "eventManager") and \
       hasattr(client.service_content.eventManager, "eventHub"):
        return "websocket"  # 启用长连接
    return "polling"  # 回退轮询

逻辑分析:通过反射检查 eventManager.eventHub 属性存在性判断 WebSocket 支持能力;api_version 仅作辅助校验,实际以服务端返回的 service content 结构为准。

双模特性对比

特性 WebSocket 模式 轮询模式
延迟 500ms–5s(可配置间隔)
连接开销 单 TCP 连接复用 每次 HTTP(S) 请求新建连接
vSphere 最低版本 6.7 5.5
graph TD
    A[启动事件监听] --> B{支持eventHub?}
    B -->|是| C[建立WebSocket连接<br>监听/vim/event]
    B -->|否| D[调用WaitForUpdatesEx<br>带changeVersion令牌]
    C --> E[接收JSON事件流]
    D --> F[解析UpdateSet增量]

3.2 迁移轨迹可视化数据管道:从Raw Event到时序指标(Prometheus Exposition Format)

数据同步机制

原始事件(Raw Event)经 Kafka 消费后,由 Flink 实时解析并打上 migration_idsource_clustertarget_clustertimestamp 标签,确保维度可追溯。

指标聚合逻辑

每 15 秒窗口内统计:

  • migration_duration_seconds_sum(总耗时)
  • migration_events_total(事件数)
  • migration_status{state="success"}(状态计数)

Prometheus 格式转换示例

# HELP migration_duration_seconds_sum Total duration of migration tasks
# TYPE migration_duration_seconds_sum counter
migration_duration_seconds_sum{migration_id="mig-7a2f",source_cluster="prod-us1",target_cluster="prod-eu2"} 428.65

# HELP migration_events_total Total number of migration events processed
# TYPE migration_events_total counter
migration_events_total{migration_id="mig-7a2f",source_cluster="prod-us1",target_cluster="prod-eu2"} 127

此格式严格遵循 Prometheus Exposition Format v1.0.0# HELP 为元信息,# TYPE 定义指标类型(counter/gauge),后续行为键值对+浮点数值,标签必须为 ASCII 字符且无空格。

关键字段映射表

Raw Event 字段 Prometheus 标签/指标名 类型 说明
event_time timestamp float Unix 毫秒时间戳,用于 @ 修饰符
status migration_status{state="..."} gauge 状态快照(pending/running/success/failed
graph TD
    A[Raw Event JSON] --> B[Flink Streaming Job]
    B --> C[Enrich + Window Aggregation]
    C --> D[Exposition Format Serializer]
    D --> E[HTTP /metrics endpoint]

3.3 分布式追踪集成:OpenTelemetry Span注入与vMotion链路染色

在虚拟化环境中,vMotion 迁移会中断传统 Trace 上下文传递。OpenTelemetry 通过 Span 注入机制,在迁移前将当前 SpanContext 序列化为 tracestate 字段,并写入 vSphere Custom Attributes。

Span 注入关键代码

from opentelemetry.trace import get_current_span
from opentelemetry.propagate import inject

def inject_span_for_vmotion(vm):
    span = get_current_span()
    carrier = {}
    inject(setter=vsphere_attr_setter, carrier=carrier)  # 自定义 setter 写入 VM 属性
    vm.set_custom_value("otel_tracestate", carrier["tracestate"])

inject() 调用 OpenTelemetry 标准传播器,vsphere_attr_settertracestate 值持久化至 vSphere 层,确保迁移后可被目标 ESXi 主机读取还原。

vMotion 链路染色流程

graph TD
    A[源ESXi执行vMotion] --> B[读取VM Custom Attr: otel_tracestate]
    B --> C[重建SpanContext并续传Trace]
    C --> D[目标ESXi继续上报Span]
染色字段 类型 用途
tracestate string 跨vMotion保留供应商上下文
vmotion_id uuid 关联迁移事件的唯一标识

第四章:智能告警与原子化回滚体系

4.1 多维度异常检测规则引擎:阈值、趋势突变、依赖服务健康度联合判定

传统单维阈值告警易受毛刺干扰,本引擎融合三类信号实现置信加权判定。

联合判定逻辑

  • 阈值层:实时指标超预设静态/动态阈值(如 P95 延迟 > 800ms)
  • 趋势层:基于滑动窗口的二阶差分检测斜率突变(Δ²y/Δt² > 0.3)
  • 依赖层:调用链中下游服务健康度

规则融合代码示例

def is_anomalous(metrics, deps_health):
    # metrics: {'latency_ms': 850, 'qps': 1200}
    # deps_health: {'auth-svc': 0.82, 'db-proxy': 0.95}
    threshold_alert = metrics['latency_ms'] > 800
    trend_alert = abs(compute_acceleration(metrics['latency_series'])) > 0.3
    dep_alert = any(h < 0.9 for h in deps_health.values())
    return sum([threshold_alert, trend_alert, dep_alert]) >= 2  # 至少两路触发

compute_acceleration() 对最近60秒延迟序列做二阶差分;deps_health 来自分布式追踪聚合结果;>=2 实现“少数服从多数”容错机制。

判定权重配置表

维度 权重 触发条件示例
阈值超限 0.4 CPU > 90% 连续30s
趋势突变 0.35 错误率斜率骤增 > 5×均值
依赖失衡 0.25 关键下游健康度
graph TD
    A[原始指标流] --> B{阈值过滤}
    A --> C{趋势分析}
    A --> D{依赖健康度查询}
    B --> E[布尔信号]
    C --> E
    D --> E
    E --> F[加权投票引擎]
    F --> G[最终异常标记]

4.2 告警分级与静默策略:基于vCenter权限域与业务SLA标签的动态路由

告警不再统一推送,而是依据双重上下文动态决策:vCenter中定义的权限域(如/Datacenter/Prod-Cluster)决定可操作边界,Kubernetes中注入的sla-level: gold等标签标识业务等级。

动态路由核心逻辑

def route_alert(alert):
    # 从vCenter API获取告警关联VM所属权限域
    domain = get_vcenter_domain(alert.vm_id)  # e.g., "Prod-Cluster"
    # 从CMDB同步SLA标签(通过VC→vSphere Tag→K8s Annotation映射)
    sla = get_sla_label(alert.vm_name)        # e.g., "gold"
    return f"{domain}.{sla}"  # 输出路由键:Prod-Cluster.gold

该函数将告警映射至唯一路由键,供消息队列(如RabbitMQ Topic Exchange)精准分发。get_vcenter_domain需缓存vCenter树状结构以降低API压力;get_sla_label依赖Tag-to-Annotation同步控制器。

静默策略匹配表

SLA等级 响应SLA 静默窗口(工作日) 允许静默角色
gold 15m 00:00–06:00 SRE-Prod, Platform-Admin
silver 60m 02:00–05:00 SRE-Dev, Cluster-Owner

路由决策流程

graph TD
    A[原始告警] --> B{含VM ID?}
    B -->|是| C[查vCenter权限域]
    B -->|否| D[降级为全局告警]
    C --> E[查CMDB SLA标签]
    E --> F[组合路由键]
    F --> G[匹配静默规则]
    G --> H[投递至对应告警通道]

4.3 回滚操作原子性保障:vSphere Task事务回溯 + 虚拟机快照一致性校验

vSphere 中的回滚并非简单撤销,而是依托 Task 对象的事务状态追踪与快照链的强一致性校验协同实现。

数据同步机制

Task 生命周期(queued → running → success/failed)被持久化至 vCenter DB,并关联 entity_idsnapshot_id。失败时触发自动回溯:

# 伪代码:基于Task状态触发快照一致性校验
if task.state == "failed":
    snap = get_latest_snapshot(vm_id)  # 依据task.entity_id反查VM
    if not verify_snapshot_integrity(snap, expected_state_hash):
        raise AtomicRollbackViolation("快照元数据与事务日志不匹配")

verify_snapshot_integrity() 检查快照磁盘链 .vmdkparentCID 连续性、内存快照 .vmem 校验和,及 snapshotInfo.xmlcreateTime 与 Task endTime 的时间窗口偏差(≤500ms)。

关键保障维度

维度 机制 验证方式
事务可见性 Task状态双写(DB + VC memory cache) Compare DB timestamp vs. API /task/{id} response
快照拓扑完整性 基于 CID 的只读链式引用 vmkfstools -D disk.vmdk \| grep parentCID
时间锚点对齐 Task endTime 与快照 createTime 纳秒级比对 abs(endTime - createTime) < 5e8 (500ms)

回滚执行流程

graph TD
    A[Task 状态变为 failed] --> B[查询关联 VM 最新快照]
    B --> C{快照完整性校验通过?}
    C -->|是| D[挂载快照为当前运行态]
    C -->|否| E[触发 vSphere Health Check 并告警]

4.4 回滚失败熔断机制:自动触发vCenter日志取证与Go panic recovery兜底流程

当回滚操作因vCenter连接中断、任务超时或资源锁死而连续失败3次,系统立即激活熔断器,终止后续重试并启动双轨取证:

自动vCenter日志拉取

// 触发vCenter事件日志与task-history快照采集(5分钟窗口)
logReq := &LogCollectionRequest{
    Host:     "vc01.example.com",
    Duration: 5 * time.Minute,
    Filters:  []string{"TaskEvent", "VmReconfigureEvent"},
}
// 参数说明:Duration控制日志时间范围,Filters限定取证事件类型,避免全量日志阻塞通道

Go panic 安全恢复

  • 启动独立recover goroutine隔离主流程
  • 保存panic堆栈至本地ring buffer(容量128KB)
  • 注入唯一traceID关联vCenter日志批次

熔断状态决策表

条件 动作 超时阈值
连续失败 ≥3次 激活熔断 + 日志取证 90s/次
vCenter无响应 切换备用API endpoint 30s
graph TD
    A[回滚失败] --> B{失败计数≥3?}
    B -->|是| C[触发熔断]
    C --> D[并发拉取vCenter日志]
    C --> E[启动panic recover协程]
    D & E --> F[生成取证报告+traceID]

第五章:总结与展望

技术栈演进的实际影响

在某大型电商平台的微服务重构项目中,团队将原有单体架构迁移至基于 Kubernetes 的云原生体系。迁移后,平均服务部署耗时从 47 分钟降至 92 秒,CI/CD 流水线失败率下降 63%。关键变化在于:容器镜像统一采用 distroless 基础镜像(仅含运行时依赖),配合 Kyverno 策略引擎自动校验镜像签名与 CVE-2023-27531 等高危漏洞补丁状态。下表对比了迁移前后核心指标:

指标 迁移前(单体) 迁移后(K8s+Service Mesh)
日均发布次数 1.2 23.8
平均故障恢复时间(MTTR) 42 分钟 87 秒
配置变更错误率 14.7% 0.9%

生产环境灰度策略落地细节

某金融级支付网关采用 Istio + Argo Rollouts 实现渐进式发布。每次新版本上线,系统自动执行以下动作序列:

  1. 将 2% 流量路由至新 Pod(基于 Header x-canary: true
  2. 启动 Prometheus 自定义指标监控:http_request_duration_seconds_bucket{le="0.2",canary="true"} 与基线比对
  3. 若 P95 延迟增幅超 15% 或错误率突破 0.05%,立即触发 rollback
  4. 全量切流前强制通过混沌工程测试:使用 Chaos Mesh 注入网络延迟(100ms ±30ms)及 CPU 压力(85% 持续 5 分钟)
# Argo Rollouts 的 AnalysisTemplate 片段(生产环境实配)
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
spec:
  metrics:
  - name: latency-check
    provider:
      prometheus:
        address: http://prometheus.monitoring.svc:9090
        query: |
          avg(rate(http_request_duration_seconds_bucket{le="0.2",canary="true"}[5m])) 
          / 
          avg(rate(http_request_duration_seconds_bucket{le="0.2",canary="false"}[5m]))

架构治理的持续性挑战

尽管自动化程度提升,但团队仍面临三类高频问题:

  • 配置漂移:开发人员绕过 GitOps 流程直接 kubectl edit 修改 ConfigMap,导致环境不一致;解决方案是启用 OPA Gatekeeper 策略 k8s-no-direct-configmap-edit,拒绝所有非 FluxCD 控制器发起的更新请求
  • 依赖幻影:某 Java 服务在本地构建成功,但 CI 环境因 Maven 仓库镜像未同步 spring-boot-starter-webflux:3.2.0 而失败;最终通过 Nexus Repository Manager 部署私有代理仓库并启用 stagingrelease 仓库隔离策略解决
  • 可观测性盲区:gRPC 流式响应的端到端追踪缺失;通过在 Envoy Filter 中注入 OpenTelemetry SDK,并将 grpc-statusgrpc-message 字段映射为 Span 属性,实现 100% 流式调用链覆盖

未来技术验证路线图

当前已启动三项关键技术预研:

  • 使用 eBPF 替代 iptables 实现 Service Mesh 数据平面(已在测试集群验证,连接建立延迟降低 41%)
  • 基于 WASM 插件扩展 Envoy 功能(已实现 JWT 令牌动态解密插件,性能损耗
  • 探索 Rust 编写的轻量级 Operator(用于管理边缘节点上的 SQLite 本地缓存服务)

这些实践表明,架构升级的价值必须通过可量化的业务指标来验证,而非单纯追求技术先进性。

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

发表回复

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