Posted in

工业IoT数据上云卡顿?Go语言+eBPF实现网络栈级QoS策略(实测抖动降低至<83μs)

第一章:工业IoT数据上云卡顿的根因诊断与QoS需求建模

工业现场设备产生的时序数据(如PLC周期采样、振动传感器高频流、边缘AI推理结果)在上云过程中常表现为间歇性延迟突增、数据包丢失率波动或端到端吞吐量骤降。这类“卡顿”并非单一网络抖动所致,而是多域耦合失配的结果:边缘侧资源受限导致缓冲区溢出、传输层TCP重传机制与工业实时性冲突、云平台消息队列消费滞后、以及安全隧道(如TLS 1.3握手+IPSec封装)引入的不可忽略处理开销。

数据采集层瓶颈识别

使用iotop -o -b -n 1实时捕获边缘网关中高I/O等待进程;结合cat /proc/interrupts | grep -E "(eth|enp|irq)"分析中断分布是否集中于单个CPU核——若网卡中断90%以上绑定至CPU0,而应用线程被调度至其他核,则将引发跨核缓存失效与上下文切换放大延迟。

网络传输路径深度探测

执行分段式链路质量测绘:

# 步骤1:测量本地出口到云接入点(如阿里云IoT Core endpoint)的RTT基线  
ping -c 20 iot-as-mqtt.cn-shanghai.aliyuncs.com  

# 步骤2:分离TCP建连与数据传输耗时(需启用TCP timestamp选项)  
tcpdump -i eth0 -w mqtt_handshake.pcap 'host iot-as-mqtt.cn-shanghai.aliyuncs.com and port 443' &  
curl -v --connect-timeout 5 https://iot-as-mqtt.cn-shanghai.aliyuncs.com/health  
# 后续用Wireshark分析SYN/SYN-ACK/ACK时延及TLS handshake各阶段耗时  

工业场景QoS属性量化映射

数据类型 允许最大端到端延迟 丢包容忍度 采样周期稳定性要求 典型协议适配建议
运动控制指令 ≤10ms 0% ±50μs MQTT QoS2 + 时间敏感网络TSN
设备健康状态上报 ≤5s ±500ms MQTT QoS1 + 自适应心跳间隔
历史日志批量上传 ≤30min 无硬性要求 HTTP/2分块上传 + 断点续传

建立QoS需求模型需将物理约束(如PLC扫描周期=2ms)转化为通信约束:设控制环路总允许延迟为T_max,则云端决策下发+边缘执行反馈的双向链路预算不得超过T_max − 2×(本地控制周期)。此约束直接决定MQTT QoS等级、TLS会话复用策略及边缘缓存深度配置。

第二章:Go语言构建高并发低延迟IoT数据通道

2.1 Go协程调度模型与实时性保障机制分析

Go 运行时采用 M:N 调度模型(M 个 OS 线程映射 N 个 goroutine),由 GMP(Goroutine、Machine、Processor)三元组协同工作,兼顾吞吐与响应。

核心调度组件职责

  • G(Goroutine):轻量栈(初始2KB),用户态协程
  • M(Machine):绑定 OS 线程的执行上下文
  • P(Processor):逻辑处理器,持有本地运行队列(LRQ)与全局队列(GRQ)

抢占式调度触发点

// 示例:系统调用阻塞时的 M-P 解绑与再绑定
func netpollblock(pd *pollDesc, mode int, waitio bool) {
    // 当 goroutine 在 syscalls 中阻塞,M 会解绑 P 并让出 P 给其他 M 复用
    // 避免 P 空转,提升并发利用率
}

该机制确保高负载下仍能快速响应新 goroutine,避免单个长阻塞任务拖垮整体调度公平性。

实时性增强策略对比

机制 延迟敏感度 适用场景 是否默认启用
协程抢占(基于时间片) CPU 密集型
网络/IO 非阻塞轮询 高频短连接服务 ✅(netpoll)
runtime.LockOSThread 极高 实时音视频处理 ❌(需显式调用)
graph TD
    A[新 Goroutine 创建] --> B{P 本地队列有空位?}
    B -->|是| C[加入 LRQ 尾部]
    B -->|否| D[入全局队列 GRQ]
    C & D --> E[M 循环窃取:LRQ → GRQ → 其他 P 的 LRQ]
    E --> F[抢占检查:每 10ms 检查是否需调度]

2.2 基于netpoll的零拷贝Socket层优化实践

传统 read()/write() 系统调用在高并发场景下存在内核态/用户态多次拷贝与上下文切换开销。netpoll 机制绕过 socket 缓冲区,直接映射网卡 Ring Buffer 至用户空间,结合 AF_XDPio_uring 实现零拷贝收发。

零拷贝数据通路设计

// 使用 io_uring 提交 recv 无拷贝请求(IORING_OP_RECV_ZC)
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv_zc(sqe, sockfd, NULL, 0, MSG_WAITALL, 0);
io_uring_sqe_set_flags(sqe, IOSQE_FIXED_FILE);

recv_zc 不分配用户缓冲区,由内核返回指向 packet 数据页的指针;IOSQE_FIXED_FILE 复用预注册 fd,避免每次系统调用查表开销。

性能对比(10K 连接,1KB 消息)

方式 吞吐量 (Gbps) CPU 占用率 (%) 内存拷贝次数
传统 syscalls 4.2 89 4
netpoll + io_uring 11.7 31 0
graph TD
    A[网卡 DMA 写入 Ring Buffer] --> B{netpoll 触发}
    B --> C[io_uring 提交 ZC recv]
    C --> D[内核返回 page 地址 + offset]
    D --> E[用户态直接解析报文]

2.3 Go runtime对工业时序数据吞吐的压测验证(10k+设备/秒)

为验证Go runtime在高并发时序写入场景下的稳定性,我们构建了基于runtime.GOMAXPROCS(32)sync.Pool复用[]byte缓冲区的采集代理。

数据同步机制

采用无锁环形缓冲区 + 批量Flush策略,每50ms触发一次WriteBatch

// 每设备独立buffer,避免竞争
type DeviceBuffer struct {
    data *sync.Pool // New: func() interface{} { return make([]byte, 0, 256) }
}

sync.Pool显著降低GC压力;实测GC pause从12ms降至≤180μs。

压测关键指标(单节点,48核/192GB)

设备接入速率 CPU均值 P99写入延迟 内存增长
12,000/s 78% 8.3ms

并发调度路径

graph TD
    A[设备UDP包] --> B{net.Conn.Read}
    B --> C[goroutine池解码]
    C --> D[RingBuffer.Append]
    D --> E[Timer Tick → Batch Flush]
    E --> F[TSDB WriteAsync]

2.4 与MQTT/OPC UA协议栈的深度集成与内存复用设计

为降低边缘网关在多协议并发场景下的内存开销,本设计将 MQTT 客户端会话上下文与 OPC UA 会话缓冲区统一纳管至共享内存池(shared_io_buffer_t),避免重复分配。

内存池结构设计

字段名 类型 说明
base_ptr uint8_t* 物理起始地址
offset_mqtt size_t MQTT 协议栈专用偏移
offset_opcua size_t OPC UA 编码/解码专用偏移

数据同步机制

// 复用同一buffer处理两种协议的序列化入口
static inline int encode_to_shared_buffer(
    shared_io_buffer_t *pool,
    const void *msg,
    protocol_type_t proto) {
    uint8_t *dst = (proto == PROTO_MQTT) 
        ? pool->base_ptr + pool->offset_mqtt
        : pool->base_ptr + pool->offset_opcua;
    return proto == PROTO_MQTT 
        ? mqtt_encode(dst, msg)  // 输出至MQTT偏移区
        : opcua_encode(dst, msg); // 输出至OPC UA偏移区
}

该函数通过运行时协议类型判断动态定位写入区域,确保两协议数据互不覆盖;offset_mqttoffset_opcua 在初始化阶段由协议栈协商确定,支持零拷贝转发。

graph TD
    A[MQTT Publish] --> B{共享内存池}
    C[OPC UA ReadRequest] --> B
    B --> D[统一DMA引擎]
    D --> E[硬件加速加密/签名]

2.5 生产环境Go服务热更新与QoS策略动态加载实现

策略热加载核心机制

采用 fsnotify 监听策略配置文件(如 qos.yaml)变更,触发原子性重载:

watcher, _ := fsnotify.NewWatcher()
watcher.Add("config/qos.yaml")
for {
    select {
    case event := <-watcher.Events:
        if event.Op&fsnotify.Write == fsnotify.Write {
            newCfg := loadQoSCfg("config/qos.yaml") // 原子解析
            atomic.StorePointer(&globalQoSCfg, unsafe.Pointer(&newCfg))
        }
    }
}

逻辑分析fsnotify.Write 过滤仅响应写入事件;atomic.StorePointer 保证配置指针切换的无锁可见性;loadQoSCfg 内部校验 YAML 结构合法性,失败时保留旧配置。

QoS策略维度与生效优先级

维度 示例值 动态生效方式
请求速率限流 100rps 即时更新令牌桶参数
超时控制 timeout: 800ms 修改 context.WithTimeout
熔断阈值 errorRate: 0.3 重置滑动窗口计数器

数据同步机制

  • 使用内存映射(mmap)加速大配置文件读取
  • 双缓冲区设计:加载中写入bufB,运行中始终读bufA,切换时原子交换指针
graph TD
    A[配置变更事件] --> B[异步解析至bufB]
    B --> C{校验通过?}
    C -->|是| D[原子切换bufA ↔ bufB]
    C -->|否| E[日志告警,维持原bufA]
    D --> F[新QoS规则生效]

第三章:eBPF驱动的网络栈级流量整形与优先级调度

3.1 XDP/eBPF TC ingress/egress钩子在工业流量识别中的精准锚定

工业协议(如 Modbus TCP、PROFINET、EtherCAT)常嵌套于标准 IP/UDP/TCP 流中,传统用户态解析存在延迟与丢包风险。XDP 和 TC egress/ingress 钩子提供内核协议栈最前端的零拷贝处理能力。

钩子定位对比

钩子位置 触发时机 适用场景 是否支持重写
XDP 驱动层接收后、IP 栈前 超低延迟过滤/丢弃 ✅(仅 skb 改写受限)
TC ingress IP 层之后、路由前 协议字段深度解析(如 Modbus 功能码) ✅(完整 skb 操作)
TC egress 发送路径最后阶段 工业报文优先级标记(802.1Q/pcp)

典型 eBPF 程序锚定逻辑(TC ingress)

SEC("classifier")
int industrial_classifier(struct __sk_buff *skb) {
    void *data = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;
    struct iphdr *iph = data;
    if (data + sizeof(*iph) > data_end) return TC_ACT_OK;
    if (iph->protocol == IPPROTO_TCP) {
        struct tcphdr *tcph = (void *)(iph + 1);
        if ((void *)(tcph + 1) > data_end) return TC_ACT_OK;
        if (ntohs(tcph->dest) == 502) { // Modbus TCP 默认端口
            bpf_skb_set_tstamp(skb, bpf_ktime_get_ns(), CLOCK_MONOTONIC);
            return TC_ACT_REDIRECT; // 交由专用用户态 worker 处理
        }
    }
    return TC_ACT_OK;
}

该程序在 TC ingress 钩子中完成端口+协议双重校验,避免误匹配非工业流量;bpf_skb_set_tstamp 提供纳秒级时间戳,满足等时性分析需求;TC_ACT_REDIRECT 实现软中断上下文到用户态 AF_XDP ring 的无锁移交。

数据流闭环示意

graph TD
    A[网卡 DMA] --> B[XDP_PASS]
    B --> C[TC ingress hook]
    C --> D{Modbus TCP?}
    D -->|Yes| E[打时间戳 + 重定向]
    D -->|No| F[继续协议栈]
    E --> G[AF_XDP ring]
    G --> H[工业协议解析引擎]

3.2 基于tc-bpf的微秒级带宽预留与抖动抑制策略部署

传统队列调度难以在微秒级粒度上协同保障带宽下限与抖动上限。tc-bpf 通过在内核 qdisc 层嵌入可编程 eBPF 程序,实现数据包到达时的实时决策。

核心机制:双环缓冲+时间戳驱动调度

  • 每流维护两个环形缓冲区:reserve_q(预留带宽专用)与 burst_q(弹性突发缓冲)
  • 包进入时依据流ID哈希、当前系统单调时钟及预设抖动窗口(如 50μs)动态分流

eBPF 流量整形代码片段

// tc-bpf 程序入口:attach 到 clsact egress
SEC("classifier")
int bpf_tc_classify(struct __sk_buff *skb) {
    struct flow_key key = {};
    bpf_skb_load_bytes(skb, ETH_HLEN, &key, sizeof(key)); // 提取五元组
    u64 now = bpf_ktime_get_ns(); // 纳秒级高精度时钟
    u64 jitter_win = 50000; // 50μs 抖动容忍窗口(纳秒)

    struct flow_state *state = bpf_map_lookup_elem(&flow_states, &key);
    if (!state || now - state->last_sent < jitter_win) {
        bpf_skb_redirect(skb, skb->ifindex, BPF_F_INGRESS); // 暂存或重排
        return TC_ACT_STOLEN;
    }
    state->last_sent = now;
    return TC_ACT_OK;
}

逻辑分析:该程序在 clsact 的 egress hook 执行,避免排队延迟;bpf_ktime_get_ns() 提供 sub-μs 时间分辨率;jitter_win 直接约束相邻发包最小间隔,硬性抑制抖动;TC_ACT_STOLEN 表示包已由BPF接管处理,不进入内核队列。

性能对比(典型 10Gbps NIC)

指标 HTB + SFQ tc-bpf 策略
最小抖动 82 μs 12 μs
带宽预留误差 ±9.3% ±0.7%
CPU 开销/流 1.2 μs 0.38 μs
graph TD
    A[包到达 clsact egress] --> B{查 flow_state}
    B -->|存在且未超抖动窗| C[暂存至 burst_q]
    B -->|空闲或超窗| D[标记为可发,更新 last_sent]
    D --> E[经 qdisc 排队发送]

3.3 eBPF Map与Go用户态协同:实时反馈闭环控制架构

eBPF Map 是内核与用户态间唯一安全、高效的数据共享通道,其类型选择直接决定闭环控制的实时性与表达能力。

核心 Map 类型对比

类型 适用场景 多CPU安全 Go访问开销
BPF_MAP_TYPE_HASH 动态键值统计(如PID→latency) 低(syscall + mmap)
BPF_MAP_TYPE_PERCPU_ARRAY 每CPU计数器聚合 ✅✅ 极低(固定偏移读取)
BPF_MAP_TYPE_RINGBUF 高吞吐事件流(推荐替代perf buffer) 中(需消费游标管理)

Go端Map同步示例

// 打开已加载的eBPF Map(假设为BPF_MAP_TYPE_HASH)
mapFD, _ := ebpf.LoadPinnedMap("/sys/fs/bpf/latency_map", nil)
defer mapFD.Close()

var latency uint64
err := mapFD.Lookup(uint32(pid), &latency) // 键为PID,值为纳秒延迟
if err == nil {
    if latency > 10_000_000 { // >10ms 触发自适应限流
        triggerThrottle(pid)
    }
}

逻辑分析Lookup() 原子读取内核侧最新值;uint32(pid) 作为键需严格匹配eBPF端定义;latency 类型与eBPF结构体字段对齐(如__u64),否则触发E2BIG错误。该调用无锁、零拷贝,延迟

闭环控制流程

graph TD
    A[eBPF程序捕获TCP重传事件] --> B[更新BPF_MAP_TYPE_HASH:pid→retrans_cnt]
    B --> C[Go定时器每100ms扫描Map]
    C --> D{retrans_cnt > threshold?}
    D -->|是| E[调用netlink修改cgroup CPU.max]
    D -->|否| F[维持当前QoS策略]
    E --> A

第四章:Go+eBPF联合QoS策略在边缘网关的端到端落地

4.1 工业现场拓扑建模:TSN交换机、PLC、云平台三域协同策略分发

工业现场需构建跨域统一拓扑视图,实现TSN交换机(确定性转发)、PLC(实时控制逻辑)与云平台(全局策略决策)的语义对齐。

拓扑元数据同步机制

采用轻量级YAML Schema描述设备能力与连接关系:

# device_topology.yml
tsn_switch:
  id: "sw-001"
  qos_profiles: ["audio", "control"]  # 支持的流量整形模板
plc:
  id: "plc-205"
  cycle_time_ms: 10
  tsn_enabled: true

该结构被云平台解析后生成策略约束集,驱动TSN交换机TCAM表项下发与PLC周期参数动态校准。

协同策略分发流程

graph TD
  A[云平台] -->|策略模板+QoS约束| B(TSN交换机)
  A -->|执行指令+时间戳| C(PLC)
  B -->|时间同步状态| A
  C -->|运行反馈+抖动数据| A

关键参数映射表

云侧策略字段 TSN交换机动作 PLC响应行为
latency_max=100μs 启用CBS+ATS整形 调整任务调度优先级
reliability=99.999% 配置帧复制与消除(FRER) 触发冗余通道切换逻辑

4.2 实测对比:启用前后P99抖动从427μs降至82.6μs(含硬件时间戳校准)

数据采集与校准关键步骤

  • 使用 PTP Hardware Clock (PHC) 同步 NIC 时间戳,避免软件时钟漂移引入误差
  • 每次测量前执行 phc_ctl -d /dev/ptp0 set 0 强制校准偏移量至±50ns内

核心优化代码片段

// 启用内核旁路+硬件时间戳捕获(eBPF + XDP)
SEC("xdp")  
int xdp_timestamp(struct xdp_md *ctx) {  
    __u64 ts = bpf_ktime_get_ns(); // 粗粒度起点  
    __u64 hw_ts = bpf_xdp_get_buff_len(ctx); // 实际调用PHC寄存器读取(需驱动支持)  
    bpf_map_update_elem(&ts_map, &ctx->rx_queue_index, &hw_ts, BPF_ANY);  
    return XDP_PASS;  
}

此处 bpf_xdp_get_buff_len() 是定制内核补丁提供的伪函数,真实实现为 rdtscp + MMIO 访问 Intel I225-V 的 TSSN 寄存器,延迟固定为13.2ns(实测),规避了clock_gettime(CLOCK_TAI)的syscall开销(平均860ns)。

抖动对比结果(单位:μs)

场景 P50 P90 P99 P99.9
未启用校准 12.4 217.6 427.0 1120.3
启用PHC校准 9.8 41.2 82.6 203.7

时间路径优化示意

graph TD
    A[应用层发包] --> B[传统SO_TIMESTAMPNS]
    B --> C[syscall + VDSO路径 → ~860ns抖动]
    A --> D[XDP+PHC硬件寄存器直读]
    D --> E[13.2ns确定性延迟]

4.3 策略灰度发布机制:基于gRPC Streaming的eBPF程序热替换

传统eBPF程序更新需卸载再加载,导致策略中断。本机制通过gRPC双向流实现无停机热替换:

数据同步机制

客户端持续监听服务端推送的策略版本与校验哈希,仅当新版本sha256sum不同时触发加载:

// eBPF侧校验逻辑(用户态加载器调用)
int bpf_program__load_xattr(struct bpf_program *prog,
                            const struct bpf_load_program_attr *attr,
                            __u32 *prog_fd) {
    // attr->expected_attach_type = BPF_TRACE_FENTRY;
    // attr->attach_btf_id = kfunc_id; // 支持kfunc热插拔
    return libbpf_bpf_prog_load(attr);
}

该调用启用BPF_F_REPLACE标志,复用原有map FD并保留连接跟踪状态;expected_attach_type确保语义一致性,避免挂载错位。

流控与回滚保障

阶段 超时阈值 回滚条件
加载验证 800ms 校验失败或perf事件丢弃
流量切分 动态 错误率 > 0.5% 持续10s
graph TD
    A[客户端发起Streaming] --> B[服务端推送v2策略元数据]
    B --> C{校验哈希匹配?}
    C -->|否| D[调用bpf_prog_load with BPF_F_REPLACE]
    C -->|是| E[跳过加载,复用当前程序]
    D --> F[注入perf_event_open观测指标]

4.4 安全加固:eBPF verifier白名单校验与Go侧RBAC策略绑定

eBPF程序加载前,内核verifier会严格校验指令安全性。我们扩展其白名单机制,仅允许调用预审通过的辅助函数(如 bpf_get_current_pid_tgid),并禁止 bpf_probe_read 等高危操作。

白名单校验规则示例

// bpf_program.c:受限eBPF程序片段
SEC("tracepoint/syscalls/sys_enter_openat")
int trace_openat(struct trace_event_raw_sys_enter *ctx) {
    u64 pid = bpf_get_current_pid_tgid(); // ✅ 允许:已列入白名单
    bpf_printk("PID: %d", (u32)pid);      // ✅ 允许:安全日志输出
    // bpf_probe_read(&buf, sizeof(buf), addr); // ❌ 拦截:未授权内存读取
    return 0;
}

逻辑分析:verifier在check_call()阶段比对func_id与白名单数组;bpf_get_current_pid_tgid对应ID为172,位于allowed_helper_ids[]中;非法调用触发-EACCES错误并终止加载。

Go侧RBAC策略联动

角色 eBPF程序类型 允许加载的辅助函数
monitor tracepoint bpf_get_current_pid_tgid, bpf_ktime_get_ns
auditor kprobe 上述 + bpf_get_current_comm
admin unrestricted 全量白名单(含网络钩子类)

策略绑定流程

graph TD
    A[Go HTTP请求] --> B{RBAC鉴权}
    B -->|role=monitor| C[加载monitor白名单]
    B -->|role=auditor| D[加载auditor白名单]
    C & D --> E[eBPF verifier校验]
    E -->|通过| F[加载到内核]
    E -->|失败| G[返回403 Forbidden]

第五章:工业IoT云边协同QoS体系的演进路径

从单点SLA保障到全域QoS编排的范式迁移

某汽车制造集团在部署焊装车间数字孪生系统时,初期采用传统云中心统一调度模式:所有传感器数据(12,800+个IO点,采样频率200Hz)直传公有云进行实时建模。结果导致端到端时延中位数达412ms(超工艺要求的≤50ms阈值),且云侧GPU资源争抢引发模型推理抖动率高达37%。2022年该集团重构架构,在产线边缘节点(NVIDIA Jetson AGX Orin集群)部署轻量化LSTM异常检测模型,并通过KubeEdge实现QoS策略下沉——将振动传感器流按“关键/非关键”打标,关键流启用TSN时间敏感网络+UDP-FEC前向纠错,非关键流降频至10Hz并启用MQTT QoS0。实测端到端P95时延压缩至38ms,模型服务可用性提升至99.992%。

动态资源契约机制的工程实践

下表对比了三代QoS保障机制的核心指标演进:

版本 资源契约粒度 带宽保障方式 故障恢复时效 典型部署周期
V1.0(2019) 区域级(整条产线) 静态预留20%带宽 平均8.2分钟 3周
V2.0(2021) 设备组级(AGV集群) SDN流表动态限速 平均47秒 3天
V3.0(2023) 单任务级(视觉质检AI推理) eBPF程序实时流量整形 平均1.8秒 2小时

某光伏组件厂在EL缺陷检测场景中,基于V3.0机制为每台AOI相机绑定独立eBPF流量控制器,当检测到网络拥塞时自动触发分级降级:先降低图像分辨率(4096×3000→2048×1500),再暂停非关键元数据上传,确保主检测流带宽不低于85Mbps。该策略使单台设备在5G切片中断期间仍能维持92%的缺陷识别准确率。

多维QoS度量仪表盘的落地配置

# 工业现场QoS健康度监控配置片段(Prometheus+Grafana)
- job_name: 'edge-qos-metrics'
  static_configs:
  - targets: ['10.20.30.101:9100', '10.20.30.102:9100']
  metrics_path: '/qos/metrics'
  params:
    qos_profile: ['motion_control', 'safety_shutoff', 'energy_monitoring']

跨域QoS策略协同的拓扑验证

graph LR
A[PLC控制器] -->|TSN时间戳标记| B(边缘网关)
B --> C{QoS策略引擎}
C -->|关键指令流| D[本地安全PLC]
C -->|诊断数据流| E[区域云平台]
C -->|固件升级流| F[中心云仓库]
D -->|心跳反馈| C
E -->|策略优化建议| C
F -->|版本策略包| C

某钢铁企业冷轧产线在实施该拓扑后,将安全急停指令的传输可靠性从99.2%提升至99.9994%,同时将设备预测性维护数据上传延迟波动范围收窄至±3ms内。其QoS策略引擎每日自动分析27TB边缘日志,生成13类资源重分配建议,其中82%被运维团队采纳执行。

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

发表回复

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