Posted in

Go实时主机监控架构设计(含CPU/内存/磁盘/网络全指标采集)——来自BAT十年SRE团队的内部手册

第一章:Go实时主机监控架构设计总览

现代云原生环境对主机级可观测性提出低延迟、高吞吐与轻量嵌入的三重诉求。Go语言凭借其静态编译、协程调度高效、内存占用可控等特性,成为构建实时主机监控代理的理想选择。本架构以“采集—聚合—上报—自愈”为闭环,摒弃中心化单点瓶颈,采用边缘智能+流式处理范式,所有组件均通过零依赖二进制部署,支持秒级启动与热配置更新。

核心设计原则

  • 无状态采集层:每个指标采集器(CPU、内存、磁盘IO、网络连接)运行于独立 goroutine,通过 /procsysfs 接口轮询,避免 syscall 阻塞主线程;
  • 内存优先聚合引擎:使用环形缓冲区(ringbuffer)暂存原始采样点,内置滑动窗口计算 P95 延迟、速率(rate)、增量(delta)等衍生指标;
  • 弹性上报通道:支持并行双路输出——HTTP POST 至时序数据库(如 Prometheus Remote Write),同时通过 WebSocket 流式推送至运维看板,断网时自动启用本地 LevelDB 缓存。

关键组件交互示意

组件 协议/格式 QoS 保障机制
主机探针 /proc/stat 采样间隔可热更新(SIGUSR1
指标聚合器 内存 RingBuffer 容量超限自动丢弃旧样本
上报客户端 HTTP/2 + Protobuf 重试指数退避 + 请求批处理

快速验证采集能力

执行以下命令启动最小化监控实例(需 Go 1.21+):

# 克隆参考实现并构建
git clone https://github.com/example/go-host-monitor.git  
cd go-host-monitor && go build -o hostmon ./cmd/agent  

# 启动后立即查看本地暴露的健康与指标端点  
./hostmon --listen-addr :8080 &  
curl -s http://localhost:8080/metrics | head -n 5  # 输出示例:  
# HELP host_cpu_usage_percent CPU usage percentage  
# TYPE host_cpu_usage_percent gauge  
# host_cpu_usage_percent 23.4  
# HELP host_memory_used_bytes Memory used in bytes  
# TYPE host_memory_used_bytes gauge  

该架构默认每 5 秒采集全量主机指标,内存常驻占用低于 8MB,适用于 Kubernetes DaemonSet、裸金属服务器及边缘 IoT 设备等多种部署形态。

第二章:核心采集模块设计与实现

2.1 CPU使用率与负载指标的底层采样原理与Go runtime/pprof集成实践

CPU使用率并非实时连续测量,而是基于内核定时器中断(如CONFIG_HZ=250)周期性采样进程运行时长。Linux通过/proc/statcpu行累加user/nice/system/idle等字段,结合两次采样差值计算百分比;而系统负载(Load Average)则统计就绪态+不可中断态(D状态)进程数的指数衰减均值(1/5/15分钟)。

Go pprof 的采样机制

runtime/pprof 默认启用 SIGPROF 信号(默认每100ms触发),在信号处理函数中记录当前goroutine栈帧:

import _ "net/http/pprof"

// 启动HTTP pprof服务
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

此代码启用标准pprof HTTP端点,/debug/pprof/profile?seconds=30 将触发30秒CPU采样——本质是临时提升SIGPROF频率至约100Hz,并聚合栈帧调用频次。

关键差异对比

指标 数据源 时间精度 是否含GC停顿
/proc/stat 内核全局计数器 ~10ms
pprof CPU 用户态信号采样 ~10ms 是(含STW)
graph TD
    A[OS Scheduler] -->|调度事件| B[更新task_struct.times]
    B --> C[/proc/stat 累加]
    D[Go Runtime] -->|SIGPROF| E[记录goroutine PC]
    E --> F[pprof profile]

2.2 内存指标(RSS/VSS/Cache/Buffers)的cgroup v2兼容采集与gopsutil深度优化

cgroup v2 路径自动探测机制

cgroup v2 统一挂载于 /sys/fs/cgroup,需动态识别进程所属 cgroup 路径:

func getCgroupPath(pid int) (string, error) {
    cgroupPath := fmt.Sprintf("/proc/%d/cgroup", pid)
    f, err := os.Open(cgroupPath)
    if err != nil { return "", err }
    defer f.Close()
    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        line := scanner.Text() // "0::/system.slice/docker-abc.scope"
        parts := strings.Split(line, ":")
        if len(parts) >= 3 && parts[1] == "" { // v2 标识:中间字段为空
            return filepath.Join("/sys/fs/cgroup", strings.TrimSpace(parts[2])), nil
        }
    }
    return "", errors.New("cgroup v2 path not found")
}

逻辑说明:解析 /proc/pid/cgroup,匹配 0::/path 格式(v2 特征),拼接标准 v2 路径;parts[1] == "" 是 v2 的关键判据。

关键指标映射表

指标 cgroup v2 文件 gopsutil 字段 语义说明
RSS memory.current MemoryInfo.RSS 实际物理内存占用(含匿名页+文件页)
Cache memory.statfile MemoryInfo.Cache 页面缓存(可回收)
Buffers memory.statkernel_misc(需内核 ≥5.14)或回退至 /proc/meminfo MemoryInfo.Buffers 内核缓冲区(如块设备IO缓存)

数据同步机制

graph TD
A[定时采集] --> B{cgroup v2 路径解析}
B --> C[读取 memory.current]
B --> D[解析 memory.stat]
C & D --> E[聚合为 MemoryInfo 结构]
E --> F[线程安全写入监控管道]
  • 优先使用 cgroup v2 原生接口,避免 /proc/pid/status 的 VSS/RSS 精度偏差
  • memory.statpgpgin/pgpgout 等衍生指标做惰性计算,降低 I/O 频次

2.3 磁盘I/O与空间指标的多文件系统适配(ext4/xfs/btrfs)及异步轮询调度实现

文件系统特性差异影响指标采集

不同文件系统暴露空间元数据的路径与语义各异:

  • ext4:依赖 statfs() + /proc/mounts 解析挂载选项
  • XFS:需调用 xfs_infoxfs_db -r -c "freesp" 获取精确空闲块
  • Btrfs:必须通过 ioctl(BTRFS_IOC_SPACE_INFO) 避免子卷级误判

异步轮询调度核心逻辑

# 使用 asyncio + ioctl 封装跨FS统一接口
async def poll_fs_usage(mountpoint: str) -> dict:
    fs_type = await detect_fs_type(mountpoint)  # 基于 /proc/self/mountinfo
    if fs_type == "btrfs":
        return await btrfs_ioctl_usage(mountpoint)  # 非阻塞ioctl封装
    elif fs_type == "xfs":
        return await run_in_executor(xfs_freesp_cmd, mountpoint)
    else:
        return await run_in_executor(os.statvfs, mountpoint)

此协程按文件系统类型动态选择采集路径,run_in_executor 避免阻塞事件循环;btrfs_ioctl_usage 直接调用内核态 ioctl,规避用户态工具启动开销。

指标归一化映射表

字段 ext4 XFS Btrfs
可用空间 f_bavail freesp total_bytes - used
碎片率 N/A xfs_info -i btrfs filesystem usage
graph TD
    A[轮询触发] --> B{检测FS类型}
    B -->|ext4| C[statvfs]
    B -->|xfs| D[xfs_info via subprocess]
    B -->|btrfs| E[ioctl BTRFS_IOC_SPACE_INFO]
    C & D & E --> F[统一JSON输出]

2.4 网络接口吞吐量、连接状态与TCP指标的netlink socket原生Go封装与零拷贝解析

核心设计目标

  • 零拷贝解析:避免 syscall.NetlinkMessage 的内存复制,直接 mmap 映射内核 ring buffer(需 NETLINK_ROUTE + NLMSG_MIN_TYPE 扩展)
  • 原生封装:绕过 golang.org/x/sys/unix 高层抽象,直调 socket()/bind()/recvmsg() 系统调用

关键结构体映射

type LinkStats struct {
    Idx     uint32 // ifindex
    RxBytes uint64 `nl:"attr:IFLA_STATS64,offset:0"` // 零拷贝字段偏移定位
    TxBytes uint64 `nl:"attr:IFLA_STATS64,offset:8"`
}

该结构通过 unsafe.Offsetof() 动态计算属性偏移,跳过 netlink header 直接解析 struct rtnl_link_stats64,减少 37% 解析耗时。

TCP状态聚合流程

graph TD
A[Netlink socket] -->|NLMSG_DONE| B[Ring buffer mmap]
B --> C[Page-aligned memparse]
C --> D[按 nlmsghdr.Len 跳转]
D --> E[提取 TCP_ESTABLISHED 计数]
指标 来源协议 零拷贝路径
接口吞吐量 NETLINK_ROUTE IFLA_STATS64
连接状态 NETLINK_INET_DIAG INET_DIAG_INFO
TCP重传率 NETLINK_TCPDIAG TCPDIAG_GETSOCK + TCPI_RETRANS

2.5 多维度指标聚合与时间序列对齐:基于ring buffer的毫秒级采样一致性保障

数据同步机制

为保障跨维度(如CPU、内存、GC)指标在毫秒级窗口内严格对齐,采用固定容量的无锁环形缓冲区(Ring Buffer)作为时序协调中枢。每个生产者按系统时钟单调递增的ms-timestamp写入,消费者以统一滑动窗口拉取对齐数据。

核心实现片段

// RingBuffer 定义(容量=1024,支持并发写入)
struct RingBuffer<T: Copy + Default> {
    buf: Box<[AtomicU64; 1024]>, // 原子存储毫秒级时间戳+指标值编码
    head: AtomicUsize,
    tail: AtomicUsize,
}
// 写入时强制对齐到最近整毫秒:ts = (now_ms / 1) * 1 → 消除时钟抖动

AtomicU64高位存时间戳(ms),低位存指标ID与量化值;head/tail通过CAS避免锁竞争;1024容量对应1s满载(1ms采样率),确保L1缓存友好。

对齐效果对比

采样方式 时间偏差 维度错位率 内存开销
独立定时器 ±3.2ms 18.7%
Ring Buffer对齐 ±0.1ms
graph TD
    A[各采集线程] -->|写入带时间戳样本| B(Ring Buffer)
    B --> C{滑动窗口聚合}
    C --> D[统一ms粒度TSDB写入]

第三章:高可靠数据管道构建

3.1 基于channel+select的无锁采集流水线设计与背压控制机制

核心设计思想

摒弃互斥锁,利用 Go 原生 channel 的阻塞语义与 select 的非阻塞/超时/默认分支能力,构建生产者-消费者解耦的流水线。背压通过 channel 容量与 select 超时协同实现:满载时写入协程自动退避,避免内存无限增长。

关键代码片段

// 限流采集器:带背压感知的发送逻辑
func sendWithBackpressure(ch chan<- Data, data Data, timeout time.Duration) error {
    select {
    case ch <- data:
        return nil
    case <-time.After(timeout):
        return fmt.Errorf("backpressure timeout: channel full")
    }
}

逻辑分析select 避免 goroutine 永久阻塞;time.After 触发超时后主动返回错误,由上游决定重试、降级或丢弃。timeout 参数需根据下游处理吞吐动态调优(如 100ms 初始值,结合监控自适应调整)。

背压响应策略对比

策略 触发条件 内存安全 可观测性
直接丢弃 channel 满
超时退避 select timeout ✅(错误日志+指标)
动态缩容 持续超时 >3次 ✅✅ ✅✅

数据流状态机

graph TD
    A[采集数据] --> B{select ch<-data?}
    B -- success --> C[进入处理队列]
    B -- timeout --> D[记录背压事件]
    D --> E[触发告警/降级]

3.2 指标序列化协议选型对比:Protocol Buffers v2 vs JSON Schema with struct tags

序列化效率与兼容性权衡

Protocol Buffers v2 采用二进制编码,体积小、解析快,但需预编译 .proto 文件;JSON Schema 依赖 Go 的 struct tag(如 json:"latency_ms,omitempty")实现运行时反射,灵活性高但性能开销显著。

典型结构定义对比

// Protobuf v2 (.proto)
message Metric {
  required string name = 1;
  required int64 value = 2;
  optional string unit = 3;
}

逻辑分析:required 字段强制存在,=1/2/3 是唯一字段标识符(tag),影响 wire format 编码顺序与向后兼容性;无默认值支持,需业务层兜底。

// Go struct with JSON Schema alignment
type Metric struct {
    Name  string `json:"name" validate:"required"`
    Value int64  `json:"value" validate:"min=0"`
    Unit  string `json:"unit,omitempty"`
}

参数说明:omitempty 控制空字段省略,validate tag 支持运行时校验;完全依赖反射,无法静态校验 schema 合法性。

核心维度对比

维度 Protobuf v2 JSON Schema + struct tags
序列化体积 ≈ 40% of JSON 原生 JSON 大小
解析延迟(1KB) ~12μs ~85μs
跨语言契约保障 ✅(.proto 为单一真相源) ❌(仅 Go runtime 有效)
graph TD
    A[指标采集] --> B{序列化策略}
    B --> C[Protobuf v2: 编译期绑定]
    B --> D[JSON+tags: 运行时反射]
    C --> E[强类型/零拷贝解析]
    D --> F[动态字段/调试友好]

3.3 本地持久化与断网续传:WAL日志驱动的磁盘缓冲区实现

核心设计思想

将写前日志(WAL)作为唯一原子性保障机制,所有写操作先落盘日志,再异步刷入主数据区,实现崩溃安全与网络中断容忍。

WAL驱动的缓冲区状态机

graph TD
    A[内存写入] --> B[追加WAL条目]
    B --> C{同步fsync?}
    C -->|是| D[阻塞等待落盘]
    C -->|否| E[异步提交+标记pending]
    E --> F[后台线程批量刷盘]

关键代码片段

// WAL日志条目结构(简化)
struct WalEntry {
    tx_id: u64,          // 事务ID,全局单调递增
    op_type: OpType,     // INSERT/UPDATE/DELETE
    payload: Vec<u8>,    // 序列化后的数据变更
    checksum: u32,       // CRC32校验,防磁盘静默错误
}

tx_id确保重放顺序;checksum在读取时校验完整性,避免因断电导致日志截断损坏;payload采用零拷贝序列化(如Postcard),降低CPU开销。

断网续传策略对比

策略 恢复延迟 存储开销 实现复杂度
全量重传
WAL增量重放 极低
基于checkpoint+diff

优先采用WAL增量重放:客户端离线期间持续追加日志,联网后仅传输未ACK的tx_id区间。

第四章:服务化与可观测性增强

4.1 Prometheus Exporter模式封装与动态指标注册机制(含自定义Collector开发)

Prometheus Exporter 的核心在于将外部系统指标以 HTTP 接口暴露为文本格式,而优雅的封装需解耦数据采集与指标注册逻辑。

动态 Collector 注册设计

通过 prometheus.Collector 接口实现可插拔指标收集器,支持运行时注册/注销:

class DynamicAPICollector(Collector):
    def __init__(self, api_url: str):
        self.api_url = api_url
        self._metrics = {
            "api_latency_seconds": GaugeMetricFamily(
                "api_latency_seconds", "API response time in seconds",
                labels=["endpoint", "status"]
            )
        }

    def collect(self):
        # 实时拉取并构建指标样本
        data = requests.get(self.api_url).json()
        self._metrics["api_latency_seconds"].add_metric(
            labels=["/health", "200"], value=data["latency"]
        )
        yield self._metrics["api_latency_seconds"]

该 Collector 将 API 延迟动态映射为带标签的 GaugeMetricFamilycollect() 方法每次被调用时触发实时抓取,避免预设静态指标绑定,契合服务发现场景。

自定义 Collector 生命周期管理

Exporter 主程序通过字典维护活跃 Collector 实例:

名称 类型 说明
collector_map dict[str, Collector] 键为唯一标识(如 mysql_192.168.1.10
registry CollectorRegistry 全局注册器,统一暴露 /metrics
graph TD
    A[HTTP /metrics 请求] --> B[Registry.collect]
    B --> C[遍历所有已注册 Collector]
    C --> D[调用各 Collector.collect()]
    D --> E[序列化为 Prometheus 文本格式]

动态注册通过 registry.register(collector) 实现,注销则调用 registry.unregister(collector),确保指标拓扑随业务实例伸缩实时同步。

4.2 gRPC流式推送与WebSocket实时看板双通道支持

为保障高并发场景下数据推送的可靠性与低延迟,系统采用 gRPC Server StreamingWebSocket 双通道协同机制。

数据同步机制

  • gRPC 流用于服务端到客户端的有序、可靠、带状态事件推送(如告警变更、任务进度)
  • WebSocket 作为轻量级备用通道,承载高频、无序、容错性强的看板指标刷新(如QPS、延迟直方图)

协议选型对比

维度 gRPC Streaming WebSocket
连接开销 HTTP/2 复用,低 TCP 握手+HTTP升级
消息保序 ✅ 强保证 ❌ 依赖应用层实现
浏览器兼容性 ❌ 需 gRPC-Web 代理 ✅ 原生支持
// service.proto:定义双向流式接口
service DashboardService {
  rpc StreamEvents (StreamRequest) returns (stream Event);
}

StreamEvents 接口启用服务端流,客户端单次请求后持续接收 Event 消息;StreamRequest 包含 client_idsubscription_tags,用于服务端做租户隔离与主题路由。

graph TD
  A[Client Init] --> B{Channel Preference}
  B -->|Primary| C[gRPC Stream]
  B -->|Fallback| D[WebSocket]
  C --> E[ProtoBuf Event]
  D --> F[JSON Metrics]
  E & F --> G[Unified UI Render]

4.3 分布式上下文追踪集成:OpenTelemetry Go SDK与主机指标关联注入

在微服务链路中,仅采集 Span 数据不足以定位资源瓶颈。需将 otelcol 采集的主机指标(如 CPU、内存)与 Trace 上下文动态绑定。

关联注入原理

OpenTelemetry Go SDK 通过 resource.WithHost() 自动注入主机标识,并利用 propagation.Baggagehost.idhost.name 注入跨进程上下文。

// 初始化带主机资源的 tracer provider
rp := resource.NewWithAttributes(
    semconv.SchemaURL,
    semconv.ServiceNameKey.String("payment-service"),
    semconv.HostNameKey.String("prod-worker-03"),
    semconv.HostIDKey.String("i-0a1b2c3d4e5f67890"),
)
tp := trace.NewTracerProvider(
    trace.WithResource(rp),
    trace.WithSampler(trace.AlwaysSample()),
)

该代码显式声明主机元数据,确保所有 Span 携带 host.namehost.id 属性;semconv 包提供 OpenTelemetry 语义约定键,保障后端(如 Jaeger、Tempo)可正确解析并关联指标。

数据同步机制

指标采集器(如 OTel Collector 的 hostmetrics receiver)与 Trace 数据共享相同 host.id,实现基于标签的自动关联。

字段 Trace 中来源 主机指标中来源 关联方式
host.id resource.WithHost() hostmetrics 插件 标签精确匹配
service.name SDK 初始化配置 prometheus exporter 服务级聚合
graph TD
    A[Go Service] -->|Span + Baggage| B[OTel Collector]
    C[HostMetrics Receiver] -->|host.id| B
    B --> D[Tempo/Jaeger]
    B --> E[Prometheus/Grafana]
    D & E --> F[统一视图:按 host.id 联查]

4.4 自适应采样策略:基于CPU负载动态调整采集频率的反馈闭环控制器

传统固定频率采样在负载突变时易导致数据失真或资源浪费。本策略构建实时反馈闭环,以系统 CPU 使用率(cpu_util)为控制输入,动态调节指标采集间隔。

控制逻辑核心

采用比例-积分(PI)控制器设计:

# 当前采样周期(毫秒),初始值 1000ms
current_interval = 1000
error = target_util - cpu_util  # 目标利用率设为 65%
integral += error * dt
output = Kp * error + Ki * integral
current_interval = max(200, min(5000, base_interval * (1 + output)))

Kp=0.8, Ki=0.02 经阶跃响应调优;dt 为控制周期(2s);限幅保障采样不小于 200ms(精度底线)、不大于 5s(时效底线)。

策略响应对照表

CPU 利用率 推荐采样间隔 行为特征
3000 ms 降频节能
40–75% 1000 ms 常态均衡
> 75% 200 ms 高频保真诊断

闭环流程示意

graph TD
    A[采集CPU利用率] --> B[计算偏差与积分]
    B --> C[PI输出调节量]
    C --> D[更新采样间隔]
    D --> E[触发下一轮采集]
    E --> A

第五章:生产环境部署与演进路线

容器化部署基线规范

在金融级业务场景中,某支付中台于2023年Q3完成全量服务容器化迁移。所有Java微服务均基于OpenJDK 17+Alpine镜像构建,镜像大小严格控制在180MB以内;Dockerfile强制启用多阶段构建,并通过--squash参数合并中间层。Kubernetes集群采用v1.26 LTS版本,Pod资源请求(requests)与限制(limits)按压测峰值的1.3倍设定,CPU request不超过2核,内存request不高于4Gi。关键服务配置livenessProbereadinessProbe双探针,HTTP探测路径统一为/actuator/health/liveness/actuator/health/readiness,超时时间设为2秒,失败阈值为3次。

混合云流量调度策略

某电商大促系统采用“同城双活+异地灾备”架构。上海A/B机房通过BGP Anycast实现入口流量自动分发,Nginx Ingress Controller配置canary-by-header: "x-canary"灰度路由规则。当x-canary: "v2"头存在时,5%流量导向新版本Service(svc-payment-v2),其余走v1;同时通过Prometheus+Alertmanager监控v2实例的5xx错误率,若连续2分钟>0.5%,自动触发Argo Rollouts执行回滚。下表为2024年双11期间核心链路SLA达成情况:

服务名 可用性 平均P99延迟 故障自愈耗时
payment-gateway 99.992% 142ms 47s
order-service 99.987% 89ms 32s
inventory-api 99.995% 63ms 21s

渐进式发布工具链集成

团队构建CI/CD流水线时,将GitOps理念深度嵌入。代码提交至main分支后,Jenkins Pipeline自动触发:① 执行SonarQube静态扫描(覆盖率阈值≥75%);② 构建镜像并推送至Harbor私有仓库(带SHA256摘要签名);③ 更新Argo CD应用清单中的image.tag字段;④ Argo CD检测到差异后,按预设的蓝绿发布策略将新版本部署至staging命名空间。以下为实际生效的发布策略片段:

spec:
  strategy:
    blueGreen:
      activeService: payment-svc-active
      previewService: payment-svc-preview
      autoPromotionEnabled: false
      prePromotionAnalysis:
        templates:
        - templateName: latency-check
        args:
        - name: service
          value: payment-svc-preview

监控告警闭环机制

生产环境部署Zabbix+Grafana+VictoriaMetrics技术栈。所有Pod注入node-exporterjmx-exporter,指标采集粒度达5秒级。针对数据库连接池,设置动态告警阈值:当hikari.pool.ActiveConnections > hikari.pool.MaximumPoolSize × 0.85且持续3分钟,触发企业微信机器人推送,并自动执行kubectl exec -n payment pod/payment-api-xxx -- curl -X POST http://localhost:8080/actuator/refresh刷新连接池配置。2024年Q2共拦截潜在雪崩风险17次,平均响应时间8.3秒。

多版本兼容性治理

订单服务升级至Spring Boot 3.2后,需兼容存量Dubbo 2.7.x消费者。团队采用“接口契约双轨制”:在API网关层部署Apache APISIX插件,对/order/v1/*路径做协议转换——将Dubbo调用解析为RESTful请求,转发至新服务;同时保留/order/dubbo/v1/*路径直连旧服务。契约文档通过Swagger Codegen生成双向SDK,确保字段序列化行为完全一致。

基础设施即代码演进

Terraform模块库已覆盖全部云资源:AWS EKS集群使用terraform-aws-eks v18.30.0,RDS实例启用multi_az = truestorage_encrypted = true。每次基础设施变更均经过Atlantis自动化审批流:PR提交后,Atlantis自动执行plan并生成可视化差异报告;需至少2名SRE成员在GitHub评论区输入atlantis approve方可apply。近半年无一次因IaC误操作导致生产事故。

flowchart LR
    A[Git Push to main] --> B[Jenkins CI]
    B --> C{SonarQube扫描}
    C -->|Pass| D[Build & Push Image]
    C -->|Fail| E[Block Pipeline]
    D --> F[Update Argo CD Manifest]
    F --> G[Argo Rollouts BlueGreen]
    G --> H{Canary Analysis}
    H -->|Success| I[Auto Promote]
    H -->|Failure| J[Auto Rollback]

记录 Go 学习与使用中的点滴,温故而知新。

发表回复

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