Posted in

【Golang云原生开发终极手册】:孔令飞领衔打造的K8s+Go高可用系统构建框架

第一章:孔令飞与云原生Go开发方法论

孔令飞作为云原生领域资深实践者,其Go开发方法论强调“可观察性先行、声明式设计、渐进式演进”三大原则。该方法论并非抽象理论,而是深度植根于Kubernetes Operator、eBPF可观测工具链及高并发微服务场景的真实工程反馈。

核心开发范式

  • 接口契约驱动:在项目初期即定义清晰的interface{}边界,例如type ResourceManager interface { Apply(*v1alpha1.Config) error; Status() v1alpha1.Status },强制实现层解耦;
  • 错误处理标准化:统一使用errors.Join()聚合多错误,并通过xerrors.WithStack()保留调用栈,避免if err != nil { return err }的简单透传;
  • 配置即代码(CoC):将环境差异封装为结构体标签,如type Config struct { Port intenv:”PORT” default:”8080″},配合github.com/mitchellh/mapstructure动态绑定。

可观测性嵌入实践

在HTTP服务启动时自动注入OpenTelemetry SDK,关键代码片段如下:

// 初始化全局TracerProvider(仅执行一次)
func initTracer() {
    exporter, _ := otlptracegrpc.New(context.Background(),
        otlptracegrpc.WithEndpoint("otel-collector:4317"),
        otlptracegrpc.WithInsecure(),
    )
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(resource.MustNewSchemaVersion(resource.SchemaURL)),
    )
    otel.SetTracerProvider(tp)
}

// 在HTTP handler中创建span
func handleRequest(w http.ResponseWriter, r *http.Request) {
    ctx, span := otel.Tracer("api").Start(r.Context(), "handle-request")
    defer span.End()
    // 业务逻辑...
}

工程效能保障机制

实践项 工具链 触发时机
接口兼容性检查 gofumpt -l + go vet Git pre-commit
CRD变更验证 controller-gen + kubebuilder CI阶段生成YAML并diff
性能基线监控 pprof + Prometheus Exporter 每次发布前压测

该方法论要求开发者在main.go中显式声明依赖注入容器,禁用全局变量隐式状态,所有组件初始化必须通过func NewXxx(...Option) (*Xxx, error)构造函数完成。

第二章:Go语言高可用系统核心设计原则

2.1 并发模型与goroutine生命周期管理实践

Go 的并发模型以 goroutine + channel 为核心,轻量级协程由 runtime 自动调度,但生命周期需开发者显式管控。

goroutine 启动与隐式泄漏风险

func loadData() {
    go func() { // 无超时、无取消、无错误处理 → 潜在泄漏
        time.Sleep(10 * time.Second)
        fmt.Println("loaded")
    }()
}

该匿名 goroutine 启动后脱离控制流,若外部无上下文约束,将长期驻留直至程序退出。

基于 context 的生命周期收敛

func loadDataWithContext(ctx context.Context) {
    go func() {
        select {
        case <-time.After(10 * time.Second):
            fmt.Println("loaded")
        case <-ctx.Done(): // 可被 cancel/timeout 主动终止
            return
        }
    }()
}

ctx.Done() 提供统一退出信号,配合 context.WithTimeout 或 WithCancel 实现可预测的生命周期收口。

关键生命周期状态对照表

状态 触发条件 是否可恢复
启动(New) go f() 执行 否
运行(Running) 被 M 抢占并执行 是(暂停)
阻塞(Blocked) 等待 channel、锁、系统调用 是(唤醒)
终止(Dead) 函数返回或 panic 且无 recover 否
graph TD
    A[New] --> B[Running]
    B --> C{I/O or Channel?}
    C -->|Yes| D[Blocked]
    C -->|No| E[Dead]
    D -->|Ready| B
    D -->|Cancel| E

2.2 错误处理与可观测性嵌入式设计

在资源受限的嵌入式系统中,错误处理不能依赖通用框架,而需轻量、确定性、可追溯。

错误分类与响应策略

  • 瞬态错误(如SPI超时):重试 + 指数退避
  • 持久错误(如Flash校验失败):降级运行 + 上报
  • 致命错误(如堆栈溢出):安全停机 + 环形日志快照

嵌入式可观测性三要素

维度 实现方式 资源开销示例
日志 二进制编码 + UART环形缓冲
指标 固定周期采样计数器 32-bit寄存器
追踪 事件ID + 时间戳压缩编码 8字节/事件
// 错误上报轻量协议(含上下文压缩)
typedef struct __attribute__((packed)) {
  uint16_t err_code;   // 预定义枚举,非字符串
  uint8_t  module_id;  // 模块索引(0~31)
  uint8_t  severity;   // 0=debug, 3=fatal
  uint32_t timestamp;  // 低16位为tick差分编码
} err_report_t;

该结构体通过 __attribute__((packed)) 消除填充字节;err_code 使用紧凑枚举替代字符串节省ROM;timestamp 采用差分编码(相对于上次上报),避免32位绝对时间带来的带宽压力。

graph TD
  A[传感器中断] --> B{校验通过?}
  B -->|否| C[记录err_code=0x0A]
  B -->|是| D[更新指标计数器]
  C --> E[写入环形日志缓冲区]
  E --> F[UART异步发送压缩包]

2.3 接口抽象与依赖注入在K8s控制器中的落地

Kubernetes控制器通过接口抽象解耦核心逻辑与具体实现,client.Client 和 cache.Cache 接口统一了数据访问方式,使测试与替换(如 mock client)成为可能。

依赖注入的典型模式

控制器构造函数接收预配置的依赖项,而非自行创建:

type Reconciler struct {
    client client.Client // 抽象接口,非具体 rest.Client
    scheme *runtime.Scheme
    logger logr.Logger
}

→ client.Client 封装了 CRUD + List/Watch,屏蔽底层 HTTP 细节;scheme 负责对象序列化;logger 支持结构化日志注入。

核心依赖关系

依赖项 作用 可替换性
client.Client 集群资源操作统一入口 ✅ 高(可注入 fake.NewClientBuilder)
cache.Cache 本地索引缓存层 ✅ 中(需兼容 Indexer 接口)
eventRecorder 事件上报 ✅ 高
graph TD
    A[Reconciler] --> B[client.Client]
    A --> C[cache.Cache]
    A --> D[eventRecorder]
    B --> E[REST client / Fake client]
    C --> F[Informers + SharedIndexInformer]

2.4 零信任安全模型下的Go服务认证与授权实现

零信任要求“永不信任,始终验证”,在Go微服务中需将认证(Who are you?)与授权(What may you do?)深度耦合于每次请求。

JWT解析与上下文注入

func AuthMiddleware(jwtKey []byte) gin.HandlerFunc {
    return func(c *gin.Context) {
        tokenStr := c.GetHeader("Authorization")
        if tokenStr == "" {
            c.AbortWithStatusJSON(401, gin.H{"error": "missing token"})
            return
        }
        token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
            return jwtKey, nil // 使用对称密钥验证签名
        })
        if err != nil || !token.Valid {
            c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
            return
        }
        c.Set("claims", token.Claims) // 注入至上下文供后续中间件使用
        c.Next()
    }
}

该中间件解析并校验JWT签名,仅当令牌有效时才放行;c.Set("claims")为下游授权逻辑提供结构化身份凭证,避免重复解析。

基于策略的RBAC授权

资源 动作 角色
/api/v1/users GET viewer, admin
/api/v1/users POST admin
/api/v1/config PUT admin

请求决策流程

graph TD
    A[HTTP Request] --> B{Has valid JWT?}
    B -->|No| C[401 Unauthorized]
    B -->|Yes| D[Extract Claims]
    D --> E{Policy Engine Match?}
    E -->|No| F[403 Forbidden]
    E -->|Yes| G[Forward to Handler]

2.5 内存安全与GC调优在长周期微服务中的实证分析

长周期微服务(运行>30天)面临对象泄漏累积与G1 GC退化风险。实证表明:未清理的ThreadLocal<ByteBuffer>引用导致老年代每72小时增长1.2GB。

关键泄漏模式识别

// ❌ 危险:ThreadLocal未remove,线程复用下持续持有堆外内存引用
private static final ThreadLocal<ByteBuffer> BUFFER_HOLDER = 
    ThreadLocal.withInitial(() -> ByteBuffer.allocateDirect(1024 * 1024));
// ✅ 修复:在请求结束钩子中显式清理
filterChain.doFilter(request, response);
BUFFER_HOLDER.remove(); // 防止弱引用被GC绕过

ThreadLocal.remove()是唯一可靠清除方式;仅依赖WeakReference无法阻止DirectBuffer元数据泄漏。

G1调优核心参数对照

参数 推荐值 作用
-XX:MaxGCPauseMillis 200 控制停顿上限,避免长周期服务抖动
-XX:G1HeapRegionSize 4M 匹配大对象分配频率,减少Humongous区碎片

GC行为演化路径

graph TD
    A[启动期:年轻代GC频繁] --> B[稳定期:混合GC主导]
    B --> C[老化期:并发标记延迟上升]
    C --> D[退化:Full GC触发→服务卡顿]

第三章:Kubernetes原生Go开发深度实践

3.1 Operator模式开发:从CRD定义到Reconcile循环优化

Operator的核心在于将运维逻辑编码为 Kubernetes 原生控制循环。首先通过 CRD 定义领域资源:

# crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: databases.example.com
spec:
  group: example.com
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              replicas: {type: integer, minimum: 1, maximum: 5}
  names:
    plural: databases
    singular: database
    kind: Database
    listKind: DatabaseList

该 CRD 声明了 Database 资源的结构约束,其中 replicas 字段被严格限制在 1–5 之间,确保语义合法性。

Reconcile 循环优化策略

  • ✅ 使用缓存索引加速关联资源查找(如按 ownerReference 索引)
  • ✅ 实现条件性 Requeue:仅当 status.observedGeneration != metadata.generation 时跳过同步
  • ❌ 避免在 Reconcile 中直接调用阻塞式 HTTP 客户端
优化手段 QPS 提升 内存开销
Informer 缓存 +320% +12%
指数退避重试 +85% +3%
并发队列分片 +210% +9%
func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  var db examplev1.Database
  if err := r.Get(ctx, req.NamespacedName, &db); err != nil {
    return ctrl.Result{}, client.IgnoreNotFound(err) // 忽略删除事件错误
  }
  // 核心逻辑:比对 spec 与实际状态,生成最小差异操作
  return ctrl.Result{RequeueAfter: time.Second * 30}, nil
}

此 Reconcile 函数采用幂等设计,返回 RequeueAfter 实现节流,避免高频轮询;client.IgnoreNotFound 确保资源已删时不报错中断循环。

graph TD
  A[Watch Event] --> B{CR in Cluster?}
  B -->|Yes| C[Fetch from Cache]
  B -->|No| D[Skip]
  C --> E[Run Reconcile]
  E --> F[Update Status]
  F --> G[Conditional Requeue]

3.2 Client-go高级用法:动态资源操作与缓存一致性保障

动态资源操作:DynamicClient 实战

使用 dynamic.Interface 可泛化操作任意 CRD 或内置资源,无需预生成类型定义:

dynClient := dynamic.NewForConfigOrDie(config)
unstr, err := dynClient.Resource(schema.GroupVersionResource{
    Group:    "apps",
    Version:  "v1",
    Resource: "deployments",
}).Namespace("default").Get(context.TODO(), "nginx", metav1.GetOptions{})
// 参数说明:GroupVersionResource 定义资源坐标;GetOptions 控制服务端行为(如 resourceVersion)
// 逻辑分析:DynamicClient 底层通过 RESTClient 发起 HTTP 请求,返回 *unstructured.Unstructured,规避类型强绑定

缓存一致性保障机制

Informer 通过 Reflector + DeltaFIFO + Indexer 构建事件驱动缓存:

组件 职责
Reflector 监听 API Server 变更流
DeltaFIFO 存储增删改事件队列
Indexer 提供内存索引加速查询

数据同步机制

graph TD
    A[API Server] -->|Watch Stream| B(Reflector)
    B --> C[DeltaFIFO]
    C --> D[Controller Loop]
    D --> E[Indexer Cache]
    E --> F[SharedInformer Handler]

3.3 K8s API Server扩展机制与Webhook定制开发

Kubernetes API Server 通过 Aggregated API 和 Dynamic Admission Control 提供两类核心扩展能力:前者用于注册自定义资源端点(如 metrics.k8s.io),后者通过 ValidatingAdmissionWebhook 与 MutatingAdmissionWebhook 实现策略注入。

Webhook 请求生命周期

# admissionregistration.k8s.io/v1 MutatingWebhookConfiguration 示例
webhooks:
- name: tenant-mutator.example.com
  rules:
  - operations: ["CREATE"]
    apiGroups: ["*"]
    apiVersions: ["*"]
    resources: ["pods"]
  clientConfig:
    service:
      namespace: webhook-system
      name: tenant-mutator-svc

▶️ 此配置声明:所有 Pod 创建请求将同步转发至 tenant-mutator-svc。clientConfig.service 指定集群内服务地址,rules 定义匹配粒度;API Server 会等待其返回 AdmissionReview 响应并应用变更。

扩展能力对比

机制 时序 可修改对象 典型用途
Mutating Webhook 请求入栈后、持久化前 ✅(字段注入、标签添加) 自动注入 sidecar、设置默认容忍度
Validating Webhook Mutating 后、写入 etcd 前 ❌(仅校验) 阻断违反命名规范或配额的资源
graph TD
    A[API Request] --> B{Aggregated API?}
    B -->|Yes| C[Route to APIService]
    B -->|No| D[Admission Phase]
    D --> E[Mutating Webhooks]
    E --> F[Validating Webhooks]
    F --> G[Storage]

第四章:云原生Go系统高可用工程体系构建

4.1 多集群联邦调度框架与Go控制平面实现

多集群联邦调度需统一纳管异构K8s集群,核心在于解耦调度决策与执行,并保障跨集群资源视图一致性。

架构设计原则

  • 控制平面无状态化,便于水平扩展
  • 各集群Agent仅上报摘要指标(CPU/内存/自定义CRD状态)
  • 调度器基于声明式策略(如topology-aware、cost-optimized)生成Placement决策

数据同步机制

采用Delta Watch机制降低带宽消耗:

// DeltaStore维护增量变更队列,避免全量List开销
type DeltaStore struct {
    queue   workqueue.RateLimitingInterface // 带限速的变更队列
    indexer cache.Indexer                   // 本地索引缓存
}
// 每次Sync仅处理diff,非全量Reconcile

该结构使千节点级联邦下同步延迟稳定在

调度策略路由表

策略名 触发条件 目标集群筛选逻辑
zone-preferred label: topology.kubernetes.io/zone=us-east-1a 优先匹配zone标签一致集群
cost-aware annotation: cluster-cost-tier=low 排序按预设成本权重
graph TD
    A[联邦API Server] --> B[Policy Engine]
    B --> C{策略匹配}
    C -->|zone-preferred| D[Topology Resolver]
    C -->|cost-aware| E[Price Estimator]
    D & E --> F[Placement Decision]

4.2 自愈型服务网格Sidecar的Go轻量级替代方案

传统Envoy Sidecar内存占用高、启动慢,难以适配边缘与Serverless场景。轻量级替代方案聚焦于最小化依赖与事件驱动自愈。

核心设计原则

  • 零CGO依赖,纯Go实现
  • 基于net/http/httputil构建反向代理内核
  • 利用k8s.io/client-go监听Pod/Endpoint变更事件

自愈机制流程

graph TD
    A[Watch API Server] --> B{Pod Ready?}
    B -- 否 --> C[触发健康检查]
    C --> D[调用 /healthz 接口]
    D -- 失败 --> E[自动重启代理实例]
    D -- 成功 --> F[更新路由表]

路由热更新示例

// 动态加载路由配置,支持原子替换
func (p *Proxy) UpdateRoutes(cfg *RouteConfig) error {
    p.mu.Lock()
    defer p.mu.Unlock()
    p.routes = cfg.Rules // 非阻塞赋值,零停机切换
    return nil
}

cfg.Rules为[]*RouteRule结构,含Host, PathPrefix, Upstream字段;锁保护仅作用于指针交换,避免路由请求阻塞。

方案 内存占用 启动耗时 自愈延迟
Envoy ~120MB ~800ms ~3s
Go-Proxy ~12MB ~12ms ~300ms

4.3 基于eBPF+Go的内核级流量治理与故障注入实践

现代云原生系统需在不重启、不侵入业务的前提下实现细粒度网络控制。eBPF 提供安全、可编程的内核钩子,而 Go 以其高生产力和 cgo 集成能力成为理想的用户态协作者。

核心架构分层

  • eBPF 层:在 TC(Traffic Control)或 XDP 端挂载程序,拦截并标记数据包
  • Go 控制面:通过 libbpf-go 加载 BPF 对象,动态更新 map 中的策略与故障规则
  • 策略驱动:基于五元组 + 自定义标签(如 service=auth, env=staging)匹配流量

故障注入示例(BPF 代码片段)

// bpf_program.c:在 tc clsact egress 处注入延迟
SEC("classifier")
int inject_fault(struct __sk_buff *skb) {
    __u32 key = get_flow_key(skb); // 提取 src/dst ip/port + proto
    struct fault_rule *rule = bpf_map_lookup_elem(&fault_rules, &key);
    if (rule && rule->enabled && bpf_ktime_get_ns() % 100 == 0) {
        bpf_skb_event_output(skb, &events, BPF_F_CURRENT_CPU, &rule, sizeof(*rule));
        return TC_ACT_SHOT; // 主动丢包模拟故障
    }
    return TC_ACT_OK;
}

逻辑分析:该程序在 egress 路径对匹配 fault_rules map 的流执行概率性丢包(% 100 实现 ~1% 注入率)。bpf_skb_event_output 将事件推至 perf ring buffer,供 Go 程序实时消费与审计。TC_ACT_SHOT 表示终止处理并丢弃包,实现零延迟故障注入。

支持的故障类型对比

类型 延迟范围 可控维度 是否需重编译
随机丢包 — 流量标签、端口 否
固定延迟 10ms–5s 源服务名、路径 否
TCP RST 注入 即时 连接状态、方向 是(需 map 更新)
graph TD
    A[Go 应用启动] --> B[加载 eBPF 字节码]
    B --> C[初始化 fault_rules map]
    C --> D[监听 Prometheus /inject API]
    D --> E[写入故障规则到 BPF map]
    E --> F[eBPF 程序实时生效]

4.4 混沌工程平台Go SDK设计与K8s原生混沌实验编排

为实现Kubernetes原生混沌实验的声明式编排与可编程控制,平台提供轻量、类型安全的Go SDK,封装CRD操作、实验生命周期管理及事件回调机制。

核心能力抽象

  • 支持ChaosExperiment资源的创建、状态轮询与终止
  • 内置重试策略与上下文超时控制
  • 提供OnStart/OnComplete钩子函数注册接口

SDK初始化示例

// 初始化客户端,自动发现集群内ChaosOperator服务
client, err := chaos.NewClient(chaos.WithKubeConfig("/etc/kube/config"))
if err != nil {
    log.Fatal("failed to init chaos client: ", err)
}

WithKubeConfig指定K8s认证配置路径;若运行于集群内,SDK默认使用ServiceAccount令牌自动加载。

实验编排流程(Mermaid)

graph TD
    A[定义ChaosSpec] --> B[调用CreateExperiment]
    B --> C{CRD已注册?}
    C -->|是| D[Operator监听并调度]
    C -->|否| E[返回ValidationError]
    D --> F[Pod注入故障/网络延迟等]

常见实验类型支持表

类型 动作粒度 超时默认值 是否支持回滚
PodKill 单Pod 30s 是
NetworkDelay Service级别 60s 是
CPUStress Node级别 120s 否

第五章:走向生产级云原生Go生态

高可用服务网格集成实践

在某金融级支付平台升级中,团队将自研Go微服务(基于Gin+gRPC)接入Istio 1.21,通过Envoy Sidecar实现零代码改造的mTLS双向认证与细粒度流量镜像。关键配置片段如下:

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: payment-service
spec:
  hosts:
  - "payment.internal"
  http:
  - route:
    - destination:
        host: payment-service.default.svc.cluster.local
        subset: v2
      weight: 90
    - destination:
        host: payment-service-canary.default.svc.cluster.local
        subset: v1
      weight: 10

生产级可观测性堆栈构建

采用OpenTelemetry Go SDK统一采集指标、日志与链路追踪数据,经OTLP exporter直连Prometheus+Loki+Tempo三件套。定制化仪表盘监控核心SLI: 指标类型 采集方式 告警阈值 数据源
gRPC成功率 otelgrpc.Interceptor Prometheus
日志错误率 Loki日志解析 >0.1%/min Loki
P99延迟 Jaeger span duration >300ms Tempo

安全合规加固方案

针对GDPR与等保2.0要求,在Kubernetes集群中部署以下策略:

  • 使用Kyverno策略引擎强制所有Go容器镜像签名验证(cosign verify)
  • 通过OPA Gatekeeper限制Pod必须启用seccompProfile: runtime/default
  • Go应用内嵌入go.opentelemetry.io/otel/sdk/trace并启用采样率动态调整(基于请求头X-Trace-Mode)

CI/CD流水线深度优化

GitHub Actions流水线集成Snyk扫描与Trivy镜像扫描,关键阶段耗时对比:

flowchart LR
  A[Go代码编译] --> B[静态分析-gosec]
  B --> C[单元测试覆盖率≥85%]
  C --> D[多架构镜像构建-arm64/amd64]
  D --> E[安全扫描-阻断CVE≥7.0]
  E --> F[金丝雀发布-自动回滚]

混沌工程常态化实施

在预发环境每日执行Chaos Mesh故障注入:

  • 网络延迟:对etcd客户端注入200ms±50ms抖动
  • 内存泄漏:使用pumba工具对Go进程施加15%内存压力
  • 模拟GC停顿:通过GODEBUG=gctrace=1日志分析STW时间分布

多集群联邦治理

基于Karmada 1.7实现跨AZ集群调度,Go服务通过karmada.io/v1alpha1 API声明:

// service-placement.go
placement := &workv1alpha2.ClusterPropagationPolicy{
  Spec: workv1alpha2.PropagationPolicySpec{
    ResourceSelectors: []workv1alpha2.ResourceSelector{{
      ResourceType: "Deployment",
      LabelSelector: &metav1.LabelSelector{
        MatchLabels: map[string]string{"app": "payment-go"},
      },
    }},
    Placement: workv1alpha2.Placement{
      ClusterAffinity: []string{"az-shanghai", "az-beijing"},
      SpreadConstraints: []workv1alpha2.SpreadConstraint{{
        MaxSkew: 1,
        TopologyKey: "topology.kubernetes.io/zone",
      }},
    },
  },
}

实时配置热更新机制

放弃传统ConfigMap挂载,采用Nacos Go SDK实现配置变更事件驱动:

  • 监听/payment/rate-limit路径变更
  • 触发goroutine重建限流器(基于golang.org/x/time/rate)
  • 全链路生效延迟控制在200ms内(实测P99=187ms)

资源效率深度调优

通过pprof火焰图定位到JSON序列化瓶颈,将默认encoding/json替换为github.com/json-iterator/go后:

  • 内存分配减少63%(从12.4MB→4.6MB per req)
  • GC pause时间下降至1.2ms(原8.7ms)
  • CPU使用率降低22%(AWS m5.2xlarge实例)

服务韧性设计模式

在订单服务中实现熔断-降级-重试三级防护:

  • 使用sony/gobreaker配置熔断器(连续5次超时触发)
  • 降级逻辑返回缓存中的最近成功订单状态(Redis TTL=30s)
  • 重试策略采用指数退避(base=100ms, max=1.6s, jitter=0.2)

多租户隔离架构

基于Open Policy Agent定义租户策略:

package kubernetes.admission
import data.kubernetes.namespaces

default allow = false
allow {
  input.request.kind.kind == "Pod"
  input.request.object.spec.containers[_].image == "payment-go:v3.2"
  namespaces[input.request.namespace].labels["tenant-id"] == input.request.object.metadata.labels["tenant-id"]
}

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

发表回复

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