Posted in

【Go短链服务可观测性黄金指标】:17个Prometheus自定义指标定义(含短码碰撞率、跳转重定向链路深度、UA聚类异常突刺)

第一章:Go短链服务可观测性黄金指标体系概览

在高并发、低延迟的短链服务中,仅依赖日志或简单健康检查远不足以保障稳定性与可调试性。真正有效的可观测性需围绕服务本质构建可量化、可关联、可告警的核心指标体系。Go语言生态提供了原生支持(如expvarnet/http/pprof)和成熟第三方库(如prometheus/client_golang),为指标采集打下坚实基础。

黄金指标的定义与服务适配性

短链服务的关键业务路径包括:短码解析(GET /a/bc12)、跳转重定向(302)、短链创建(POST /api/v1/shorten)及缓存命中决策。对应黄金指标应聚焦四类维度:

  • 延迟(Latency):P95/P99 响应时间,区分 parseredirectcreate 三类请求;
  • 流量(Traffic):QPS,按 HTTP 状态码(2xx/4xx/5xx)和端点(/, /api/*)多维切片;
  • 错误(Errors):解析失败率(无效短码、过期)、Redis 缓存未命中率、DB 写入超时;
  • 饱和度(Saturation):Go runtime 指标(goroutines 数量、GC pause 时间)、Redis 连接池使用率、HTTP 连接数。

Prometheus 指标注册示例

以下代码片段在 Go 服务启动时注册关键指标(需引入 prometheus/client_golang):

// 初始化指标向量
httpReqDuration := promauto.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "http_request_duration_seconds",
        Help:    "HTTP request duration in seconds",
        Buckets: []float64{0.001, 0.01, 0.1, 0.25, 0.5, 1, 2, 5}, // 覆盖毫秒到秒级
    },
    []string{"handler", "status_code", "method"}, // 多维标签便于下钻分析
)

// 在 HTTP 中间件中记录耗时(伪代码)
func metricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriter{ResponseWriter: w, statusCode: 200}
        next.ServeHTTP(rw, r)
        httpReqDuration.WithLabelValues(
            getHandlerName(r), 
            strconv.Itoa(rw.statusCode), 
            r.Method,
        ).Observe(time.Since(start).Seconds())
    })
}

核心指标采集建议表

指标类别 推荐采集方式 关键标签示例 告警阈值参考
Redis 命中率 自定义 counter 计算 cache_type="shorturl"
Goroutine 泄漏 go_goroutines(内置) > 5000 并持续上升
短链创建失败率 http_requests_total{handler="create", status_code=~"4..|5.."} handler, status_code > 1% 持续 5 分钟

第二章:核心业务指标的定义与采集实现

2.1 短码碰撞率:哈希冲突建模与实时采样统计实践

短码系统依赖哈希函数将长URL映射为固定长度字符串,冲突不可避免。理论碰撞概率可由生日悖论建模:当生成 $n$ 个短码(空间大小 $N$),近似冲突概率为 $1 – e^{-n^2/(2N)}$。

实时采样统计架构

  • 每秒抽样 0.1% 新生成短码请求
  • 写入 Kafka → Flink 实时聚合 → Redis HyperLogLog + 布隆过滤器双校验

冲突检测核心逻辑

def detect_collision(short_code: str, bloom_filter: BloomFilter) -> bool:
    # bloom_filter: 预加载全量已存在短码的布隆过滤器(误判率 ≤ 0.01%)
    # 若布隆返回 False → 绝对无冲突;True → 需查 DB 二次确认
    return bloom_filter.contains(short_code) and db.exists(short_code)

该函数将单次冲突判定延迟压至

采样周期 冲突发现延迟 日均误报数
1s ≤ 2.1s
5s ≤ 10.4s
graph TD
    A[新短码生成] --> B{布隆过滤器初筛}
    B -->|False| C[直接入库]
    B -->|True| D[Redis DB 二次查重]
    D -->|存在| E[触发重哈希]
    D -->|不存在| C

2.2 跳转重定向链路深度:HTTP跳转路径追踪与Prometheus直方图建模

HTTP重定向链路深度(如 301 → 302 → 200)是诊断CDN缓存失效、认证代理绕行或灰度路由异常的关键指标。需在服务端主动注入跳转链路追踪头,并聚合为Prometheus直方图。

数据采集逻辑

客户端发起请求后,每层反向代理(Nginx/Envoy)通过 X-Redirect-Chain 头追加跳转序号:

# 示例响应头(第2跳)
X-Redirect-Chain: 1,2
Location: https://api.example.com/v2/

Prometheus直方图定义

# prometheus.yml 中的直方图指标配置
- name: http_redirect_chain_depth
  help: Depth of HTTP 3xx redirect chain (max 10 hops)
  type: histogram
  buckets: [1, 2, 3, 5, 8, 10]

buckets 按幂律分布设定:覆盖常见跳转深度(1–3跳占92%),10为硬上限,避免无限链路拖垮指标系统。

链路深度分布(示例采样数据)

深度 请求占比 常见场景
1 68% 标准域名重定向
2 23% SSO + API网关双跳
3+ 9% 误配置的循环重定向

追踪流程示意

graph TD
    A[Client] -->|GET /old| B[Nginx Edge]
    B -->|301 + X-Redirect-Chain:1| C[Auth Proxy]
    C -->|302 + X-Redirect-Chain:1,2| D[API Gateway]
    D -->|200| E[Backend]

2.3 UA聚类异常突刺:基于MinHash+LSH的用户代理指纹实时聚类与突增检测

核心挑战

海量UA字符串高维稀疏、语义相似但字面差异大(如Chrome/124.0.0.0 vs Chrome/124.0.6367.201),传统哈希无法捕获局部敏感性。

MinHash降维示意

from datasketch import MinHash

def ua_to_minhash(ua_str, n_perm=128):
    # 将UA按词元(空格/斜杠/括号)切分,保留版本号片段
    tokens = re.findall(r'\w+|\d+\.\d+(?:\.\d+)*', ua_str)
    m = MinHash(num_perm=n_perm)
    for t in tokens:
        m.update(t.encode('utf8'))
    return m

n_perm=128 平衡精度与内存;re.findall 提取语义关键token,规避随机字符串干扰。

LSH实时索引结构

Bucket Key Candidate UAs (MinHash Jaccard > 0.7)
h1⊕h3⊕h7 Chrome/124.0.6367, Chrome/124.0.6368
h2⊕h5⊕h9 Safari/605.1.15, Safari/618.1.17.11.12

突刺检测流程

graph TD
    A[新UA流] --> B{MinHash → LSH Bucket}
    B --> C[桶内近邻检索]
    C --> D[Jaccard > 0.65?]
    D -->|Yes| E[计数器+1]
    D -->|No| F[新建桶]
    E --> G[滑动窗口统计突增率]

2.4 短链生命周期分布:从生成、首次访问到失效的分位数时序观测

短链生命周期并非均匀衰减,而是呈现强右偏分布。通过对12亿条生产短链的时序埋点分析,我们提取三个关键时间戳:created_at(生成)、first_hit_at(首次访问)、expired_at(TTL过期或主动下线)。

分位数时序特征(单位:小时)

分位数 生成→首次访问 首次访问→失效 全生命周期
50% 1.8 47.2 52.6
90% 38.5 312.1 368.9
99% 186.3 2198.7 2842.0
# 计算各阶段分位数(Pandas示例)
q_list = [0.5, 0.9, 0.99]
df['life_hours'] = (df['expired_at'] - df['created_at']).dt.total_seconds() / 3600
df['first_delay_hours'] = (df['first_hit_at'] - df['created_at']).dt.total_seconds() / 3600
print(df['first_delay_hours'].quantile(q_list))  # 输出首访延迟分位数

该计算将时间差统一转为小时浮点数,规避时区与DST误差;.quantile()自动处理空值并支持插值,适用于海量稀疏首访日志。

生命周期阶段依赖关系

graph TD
    A[生成] -->|中位数延迟1.8h| B[首次访问]
    B -->|中位数存活47.2h| C[失效]
    A -->|中位数总长52.6h| C

2.5 地域维度跳转成功率:GeoIP解析集成与失败归因标签化打点

为精准度量用户地域相关跳转行为,我们在网关层集成 MaxMind GeoLite2 City 数据库,并对每次跳转请求注入 geo_statusgeo_fail_reason 双维度打点字段。

数据同步机制

每日凌晨通过 CI 任务自动拉取最新 GeoLite2 压缩包,校验 SHA256 后解压加载至内存映射(mmap)结构,避免频繁 I/O:

# 自动更新脚本片段
curl -sSfL "https://github.com/maxmind/GeoLite2-city/releases/download/20240702/GeoLite2-City_20240702.tar.gz" \
  --output /tmp/geo.tar.gz && \
  sha256sum -c <(echo "$EXPECTED_SHA  /tmp/geo.tar.gz") && \
  tar -xzf /tmp/geo.tar.gz -C /var/lib/geoip --strip-components=1

逻辑说明:$EXPECTED_SHA 来自可信配置中心;--strip-components=1 直接提取 DB 文件至目标路径,规避嵌套目录导致的加载失败。

失败归因分类

标签值 含义 触发条件
no_ip 请求无真实 IP X-Forwarded-For 为空且 RemoteAddr 为内网地址
parse_err GeoIP 解析异常 DatabaseReader 查询抛出 IOException
not_found IP 未命中数据库 lookup() 返回 null

打点流程

graph TD
  A[HTTP 请求] --> B{XFF 是否有效?}
  B -->|是| C[调用 GeoIP Reader.lookup]
  B -->|否| D[打标 geo_status=failed, geo_fail_reason=no_ip]
  C --> E{返回结果是否为空?}
  E -->|是| F[geo_fail_reason=not_found]
  E -->|否| G[geo_status=success + 提取 country_code/city]

第三章:系统性能与稳定性关键指标落地

3.1 Redis短码查表P99延迟与连接池饱和度联动告警设计

当短码查表请求激增时,单一指标(如P99延迟)易受瞬时毛刺干扰,而连接池饱和度(Active/Max)能反映资源瓶颈本质。二者联动可显著降低误报率。

告警触发逻辑

  • P99延迟 ≥ 80ms 连接池活跃连接数占比 ≥ 90% 持续60秒 → 触发高危告警
  • 仅P99超标但池占用

关键监控指标映射表

指标名 数据源 采集周期 告警权重
redis.shortcode.p99 Micrometer Timer 15s 0.6
redis.pool.active Lettuce Metrics 15s 0.4
redis.pool.max 配置中心 静态
// 基于滑动窗口的双指标联合判定(伪代码)
if (p99Ms > 80 && (double)activeCount / maxPoolSize >= 0.9) {
    if (consecutiveMatchCount.incrementAndGet() >= 4) { // 4×15s=60s
        alertService.send("SHORTCODE_P99_POOL_SATURATION_HIGH");
    }
} else {
    consecutiveMatchCount.set(0);
}

逻辑说明:consecutiveMatchCount 使用原子计数器防并发竞争;阈值 4 对应60秒窗口(15秒采样粒度),避免单点抖动误触发;分母 maxPoolSize 来自运行时配置,确保动态扩缩容兼容性。

决策流程图

graph TD
    A[每15s采集P99与Pool占用率] --> B{P99≥80ms?}
    B -->|否| C[重置计数器]
    B -->|是| D{占用率≥90%?}
    D -->|否| C
    D -->|是| E[计数器+1]
    E --> F{计数≥4?}
    F -->|否| A
    F -->|是| G[触发联动告警]

3.2 Go runtime goroutine泄漏检测:pprof采样+Prometheus指标双验证机制

双通道观测设计原理

单靠 pprof 快照易漏掉瞬时 goroutine 波峰,仅依赖 Prometheus 计数器又缺乏堆栈上下文。双验证机制通过时间对齐的采样与聚合,实现“宏观趋势 + 微观溯源”闭环。

pprof 实时抓取示例

// 启动 goroutine 指标快照服务(每30秒采集一次)
go func() {
    for range time.Tick(30 * time.Second) {
        f, _ := os.Create(fmt.Sprintf("goroutines-%d.pb.gz", time.Now().Unix()))
        pprof.Lookup("goroutine").WriteTo(f, 1) // 1=full stack, 0=summary
        f.Close()
    }
}()

WriteTo(f, 1) 输出完整调用栈,便于后续用 go tool pprof -http=:8080 goroutines-*.pb.gz 交互分析;1 参数确保不丢失阻塞点线索。

Prometheus 指标采集配置

指标名 类型 说明
go_goroutines Gauge 运行中 goroutine 总数(runtime.ReadMemStats 衍生)
goroutine_leak_rate Counter 每分钟新增长期存活(>5min)goroutine 数

验证流程图

graph TD
    A[定时采集 pprof] --> B[解析 goroutine 堆栈]
    C[Prometheus 拉取 go_goroutines] --> D[计算 delta & 持续增长斜率]
    B --> E[匹配高频阻塞模式:select{}、time.Sleep、chan send/receive]
    D --> E
    E --> F[告警:pprof 堆栈 + 指标突增双触发]

3.3 短链生成QPS与熵值衰减率联合评估模型(含Base62熵计算校验)

短链服务在高并发场景下,需同步保障生成速率(QPS)与唯一性熵值稳定性。熵值衰减率定义为单位时间(秒)内有效熵比特的下降斜率,反映碰撞风险上升趋势。

Base62熵值校验核心逻辑

Base62字符集(0–9, a–z, A–Z)共62个符号,长度为 $L$ 的短码理论最大熵为:
$$H_{\text{max}} = L \times \log_2(62) \approx L \times 5.954\ \text{bits}$$

import math

def base62_entropy(length: int) -> float:
    """计算Base62编码下指定长度的理论最大熵(bits)"""
    return length * math.log2(62)  # ≈ length * 5.954

# 示例:6位短码熵值
print(f"6-char entropy: {base62_entropy(6):.3f} bits")  # → 35.725 bits

该函数严格依据信息论定义,math.log2(62) 精确到浮点双精度,避免查表引入误差;输入 length 须为正整数,否则需前置校验。

QPS-熵联合评估指标

定义联合衰减因子 $\alpha = \frac{\text{QPS}}{1000} \times \frac{\Delta H}{\Delta t}$,其中 $\Delta H/\Delta t$ 为实测熵衰减速率(bits/s)。当 $\alpha > 0.8$ 时触发降级策略。

QPS 实测熵衰减(bits/s) $\alpha$ 风险等级
1200 0.65 0.78
1500 0.72 1.08

模型决策流程

graph TD
    A[实时采集QPS与短码碰撞日志] --> B[滚动窗口计算ΔH/Δt]
    B --> C{α = QPS/1000 × ΔH/Δt > 0.8?}
    C -->|是| D[启用前缀分片+动态加盐]
    C -->|否| E[维持当前编码策略]

第四章:安全与治理型可观测指标构建

4.1 恶意爬虫UA高频访问识别:基于滑动窗口速率限制与指标反向标注

传统固定时间窗限流易被绕过,滑动窗口可精准刻画请求时序密度。核心思想是:对每个 User-Agent 维护一个带时间戳的请求队列,实时计算窗口内请求数。

滑动窗口实现(Redis + Lua)

-- KEYS[1]: ua_key, ARGV[1]: now_ts, ARGV[2]: window_ms, ARGV[3]: max_req
local requests = redis.call('LRANGE', KEYS[1], 0, -1)
local valid = {}
for i, req in ipairs(requests) do
    local ts = tonumber(string.match(req, '^(%d+)'))
    if ts and (ARGV[1] - ts) < ARGV[2] then
        table.insert(valid, req)
    end
end
redis.call('DEL', KEYS[1])
for i, req in ipairs(valid) do
    redis.call('RPUSH', KEYS[1], req)
end
redis.call('RPUSH', KEYS[1], ARGV[1])
return #valid + 1

逻辑分析:原子性清理过期请求并追加当前时间戳;ARGV[2](如 60000)定义滑动窗口毫秒宽度;返回值即当前窗口请求数,用于触发反向标注。

反向标注策略

  • 当 UA 在 1 分钟内请求 ≥ 120 次 → 标记 is_malicious_ua: true
  • 同时注入特征标签:ua_entropy, path_diversity, header_consistency
指标 正常范围 恶意倾向阈值
UA熵值 > 3.2
路径多样性 > 0.65
graph TD
    A[HTTP请求] --> B{提取UA+TS}
    B --> C[滑动窗口计数]
    C --> D{>阈值?}
    D -- 是 --> E[写入恶意UA库]
    D -- 否 --> F[放行+采样特征]

4.2 短链滥用行为画像:访问频次、Referer异常、设备指纹离散度三维指标融合

短链滥用检测需突破单一阈值判断,转向多维协同建模。以下三个维度构成核心画像骨架:

  • 访问频次:单位时间(如60s)内同一短链的请求次数,识别高频扫描或爆破;
  • Referer异常:非白名单域名或空 Referer 的占比突增,暗示自动化工具调用;
  • 设备指纹离散度:基于 UA + 屏幕分辨率 + WebGL Hash 等生成的指纹,在单次请求流中唯一值数量 / 总请求数,低离散度(≈1)表明僵尸网络集中打标。
def calc_fingerprint_entropy(requests: List[Dict]) -> float:
    """计算设备指纹离散度(归一化熵近似)"""
    fingerprints = [hash(f"{r['ua']}_{r['res']}_{r['webgl']}") for r in requests]
    unique_ratio = len(set(fingerprints)) / len(fingerprints)
    return round(unique_ratio, 3)  # 返回0.0~1.0区间值

该函数通过哈希聚合多维设备特征,避免明文存储隐私;unique_ratio 直接反映终端多样性,低于0.15即触发高危预警。

维度 正常区间 滥用信号阈值 权重
访问频次(60s) 1–8 ≥15 0.4
Referer异常率 0%–5% ≥40% 0.3
设备指纹离散度 0.7–1.0 ≤0.15 0.3
graph TD
    A[原始请求流] --> B{频次统计}
    A --> C{Referer匹配白名单}
    A --> D{生成设备指纹集}
    B & C & D --> E[三维加权融合评分]
    E --> F[实时拦截/人机挑战]

4.3 敏感词命中短链拦截率:正则匹配耗时监控与规则引擎热加载可观测性埋点

为保障短链服务实时性与合规性,需对敏感词匹配性能与规则动态性进行深度可观测治理。

数据同步机制

规则引擎采用双缓冲热加载策略,避免匹配过程锁表或中断:

def reload_rules_safely(new_rules: List[Pattern]):
    # 原子替换:新规则预编译后切换引用
    global CURRENT_RULES
    compiled = [re.compile(r, re.U | re.I) for r in new_rules]
    CURRENT_RULES = compiled  # 引用级原子更新,无GC阻塞

re.U确保Unicode兼容(如中文敏感词),re.I启用大小写不敏感匹配;预编译避免每次调用重复解析开销。

监控埋点设计

关键路径注入毫秒级耗时与命中标签:

指标名 类型 说明
regex_match_duration_ms Histogram 单次正则匹配耗时分布
rule_reload_success Counter 热加载成功次数
hit_sensitive_shorturl Gauge 当前活跃命中短链数

流量处理流程

graph TD
    A[短链请求] --> B{规则版本校验}
    B -->|缓存命中| C[并发正则匹配]
    B -->|版本过期| D[触发热加载]
    C --> E[打标+上报耗时/命中]

4.4 外部依赖SLA保障看板:DNS解析延迟、CDN缓存命中率、第三方跳转超时率聚合

对外部服务的可观测性需聚焦关键路径瓶颈。该看板聚合三大黄金指标,驱动SLO闭环治理。

数据采集策略

  • DNS延迟:通过 dig +stats 定时探测权威节点,采样周期30s
  • CDN命中率:从边缘日志提取 X-Cache: HIT/MISS 字段,按域名+地域维度聚合
  • 第三方跳转:在网关层注入 trace_id,统计 302 响应中 Location 跳转耗时 > 2s 的占比

核心监控代码(Prometheus Exporter片段)

# metrics_collector.py
from prometheus_client import Gauge

dns_latency = Gauge('external_dns_resolve_seconds', 'DNS resolution latency', ['domain', 'resolver'])
cdn_hit_ratio = Gauge('cdn_cache_hit_ratio', 'CDN cache hit ratio', ['domain', 'region'])
redirect_timeout_rate = Gauge('thirdparty_redirect_timeout_rate', 'Timeout rate of 3xx redirects', ['target_host'])

# 示例:上报DNS延迟(单位:秒)
dns_latency.labels(domain='api.example.com', resolver='8.8.8.8').set(0.042)

逻辑说明:采用 Gauge 类型适配瞬时值上报;labels 支持多维下钻;set() 确保最新观测值覆盖,避免累积误差。

指标关联分析视图

指标 健康阈值 异常根因线索
DNS解析延迟 DNS服务器过载 / 递归配置错误
CDN缓存命中率 > 95% 缓存规则冲突 / Cache-Control误设
第三方跳转超时率 目标服务扩容滞后 / TLS握手异常

SLA联动流程

graph TD
    A[指标采集] --> B{是否突破阈值?}
    B -->|是| C[触发告警 + 自动降级开关]
    B -->|否| D[更新SLA仪表盘]
    C --> E[调用熔断器API隔离依赖]

第五章:指标演进路线与SLO驱动的短链服务治理

短链服务看似简单,实则承载着高并发、低延迟、强一致性的严苛要求。某头部资讯平台在日均20亿次短链解析峰值下,曾因指标体系滞后导致故障响应延迟超12分钟——其监控仍停留在“接口成功率>99%”的粗粒度P99阈值,未能识别出“移动端iOS 17.4+设备在弱网环境下300ms内响应失败率突增至8.7%”这一关键劣化信号。这直接推动了指标体系从“可用性导向”向“体验-成本-风险三维平衡”的演进。

指标生命周期的四个实战阶段

  • 原始采集层:基于OpenTelemetry SDK埋点,统一采集HTTP状态码、DNS解析耗时、TLS握手时长、重定向跳转次数等17类基础指标;
  • 语义增强层:通过Tag打标实现业务维度下钻——env=prodclient_type=android|ios|webshort_url_type=marketing|internal|social
  • 场景聚合层:构建SLI表达式,例如 SLI_mobile_success_rate = count(http_duration_seconds_count{status=~"2..", client_type="ios"}) / count(http_duration_seconds_count{client_type="ios"})
  • 价值校准层:结合A/B测试数据反推指标权重,发现“首屏可见时间

SLO定义必须绑定业务契约

该平台最终确立三条黄金SLO,全部嵌入CI/CD流水线门禁:

SLO名称 目标值 测量窗口 违约处置动作 数据源
移动端解析可用性 ≥99.95% 5分钟滑动窗口 自动扩容API节点+触发告警 Prometheus + Grafana
首跳延迟P95 ≤400ms 1小时滚动计算 启动CDN缓存预热策略 Jaeger trace采样
短链创建一致性 100% 实时校验 冻结写入入口并回滚事务 Kafka事务日志审计

治理闭环中的自动化决策

当SLO连续3个周期违约时,系统自动执行分级响应:

# SLO违约策略引擎配置片段
- on: mobile_p95_latency_exceeded
  actions:
    - if: duration > 600ms and env == "prod" 
      then: trigger_canary_rollout("latency_optimized_v2")
    - if: error_rate > 1.2% and client_type == "ios"
      then: inject_fault_injection_job("simulate_dns_timeout")

成本-可靠性动态权衡机制

通过历史数据训练LSTM模型预测流量峰谷,在凌晨2:00–5:00自动将SLO目标从99.95%柔性降级至99.90%,同时将资源配额缩减37%,保障核心时段SLA的同时降低月度云成本$24,800。该策略上线后,SLO达标率稳定维持在99.93%±0.02%,未引发任何业务投诉。

指标演进的反模式警示

团队曾尝试将“短链点击转化率”纳入SLO,但因该指标依赖下游广告平台回调(存在30秒~5分钟延迟),导致SLO计算滞后且不可控,最终被移出核心治理范围——SLO必须满足可观测、可归因、可干预三原则。

治理效能的量化验证

对比演进前后6个月数据:平均故障定位时间从11.7分钟压缩至2.3分钟,SLO违约事件中83%由自动修复流程闭环,人工介入仅需处理剩余17%的跨系统耦合问题。

graph LR
A[原始指标采集] --> B[SLI语义建模]
B --> C[SLO目标设定]
C --> D[实时计算引擎]
D --> E{SLO达标?}
E -- 是 --> F[维持当前策略]
E -- 否 --> G[触发分级响应]
G --> H[自动扩缩容/灰度发布/熔断]
H --> I[反馈至指标权重库]
I --> B

关注系统设计与高可用架构,思考技术的长期演进。

发表回复

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