Posted in

Golang小程序CI/CD流水线搭建:从GitLab到K8s滚动发布,5步实现秒级上线

第一章:Golang小程序CI/CD流水线搭建:从GitLab到K8s滚动发布,5步实现秒级上线

构建高效、可靠的CI/CD流水线是现代Golang微服务交付的核心能力。本方案以GitLab CI为调度中枢,结合Docker镜像构建、Helm Chart版本化管理与Kubernetes原生滚动更新机制,实现从代码提交到生产环境服务升级的全链路自动化,平均上线耗时压缩至12秒以内(含镜像推送与Pod就绪检测)。

准备基础环境

确保集群已部署GitLab Runner(privileged模式)、Docker Registry(如GitLab Container Registry)、Helm 3及kubectl配置完成。在项目根目录创建.gitlab-ci.yml,启用docker:dind服务并挂载/var/run/docker.sock。

编写Go构建脚本

在Makefile中定义标准化构建目标,规避go build缓存污染:

# 使用Go模块构建,生成静态链接二进制
build: 
    GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/app ./cmd/app

定义GitLab CI流水线

关键阶段示例(含语义化标签与缓存加速):

stages:
  - test
  - build
  - deploy

test:
  stage: test
  script: go test -v ./...

build-image:
  stage: build
  script:
    - docker build --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

deploy-prod:
  stage: deploy
  script:
    - helm upgrade --install app ./helm/app \
        --set image.tag=$CI_COMMIT_SHA \
        --set replicaCount=3 \
        --namespace prod
  only:
    - main

Helm Chart结构优化

values.yaml中启用滚动更新策略与健康检查:

replicaCount: 3
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
readinessProbe:
  httpGet:
    path: /readyz
    port: 8080

验证发布效果

执行kubectl rollout status deployment/app -n prod实时监听滚动状态;配合Prometheus+Alertmanager监控deployment_status_phase{phase="Available"}指标,自动触发发布成功通知。流水线各阶段失败时,GitLab自动标记MR为“pipeline failed”,阻断合并流程。

第二章:Go小程序工程化构建与镜像标准化

2.1 Go模块依赖管理与多环境编译实践

Go Modules 自 v1.11 起成为官方依赖管理标准,取代 GOPATH 模式,支持语义化版本控制与可重现构建。

依赖声明与版本锁定

go.mod 文件定义模块路径与依赖约束:

module github.com/example/app

go 1.22

require (
    github.com/spf13/cobra v1.8.0
    golang.org/x/net v0.25.0 // indirect
)

require 声明直接依赖及版本;indirect 标注间接依赖;go 指定最小兼容语言版本。

多环境编译策略

利用构建标签(build tags)区分环境配置:

# 开发环境
go build -tags dev -o app-dev .

# 生产环境(禁用调试)
go build -tags prod -ldflags="-s -w" -o app-prod .
环境 构建标签 关键参数 用途
dev dev -gcflags="-l" 禁用内联,便于调试
prod prod -ldflags="-s -w" 剥离符号表与调试信息

构建流程可视化

graph TD
    A[源码] --> B{build tag}
    B -->|dev| C[启用pprof/日志详情]
    B -->|prod| D[静态链接+符号剥离]
    C --> E[可调试二进制]
    D --> F[轻量安全二进制]

2.2 静态链接与UPX压缩:极致轻量化二进制构建

静态链接剥离动态依赖,使二进制完全自包含;UPX在此基础上进一步熵减压缩,实现体积锐减。

静态编译关键标志

gcc -static -O3 -s -fPIE -pie hello.c -o hello-static

-static 强制静态链接所有 libc 和系统库;-s 移除符号表;-fPIE -pie 仍保留地址空间布局随机化(ASLR)兼容性,兼顾安全与体积。

UPX 压缩效果对比

二进制类型 原始大小 UPX –best 后 压缩率
动态链接版 16 KB 14 KB 12.5%
静态链接版 940 KB 312 KB 66.8%

压缩流程示意

graph TD
    A[源码] --> B[gcc -static]
    B --> C[未压缩静态二进制]
    C --> D[UPX --ultra-brute]
    D --> E[最终轻量二进制]

2.3 Docker多阶段构建最佳实践:从go:alpine到distroless安全镜像

为何需要多阶段构建?

传统单阶段构建将编译工具链与运行时环境一并打包,导致镜像臃肿、攻击面大。go:alpine虽轻量,仍含包管理器、shell等非必要组件。

从 alpine 到 distroless 的演进路径

# 第一阶段:构建(含完整 Go 工具链)
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o /usr/local/bin/app .

# 第二阶段:极简运行时(无 shell、无包管理器)
FROM gcr.io/distroless/static-debian12
COPY --from=builder /usr/local/bin/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
  • CGO_ENABLED=0 确保纯静态链接,避免 libc 依赖;
  • GOOS=linux 显式指定目标平台;
  • distroless/static-debian12 仅含 runtime 所需基础库,体积

安全对比(典型 Go 应用镜像)

基础镜像 大小 CVE 数量(Trivy) 可交互 shell
golang:1.22 ~980MB >200 ✅
golang:1.22-alpine ~380MB ~45 ✅ (sh)
distroless/static ~4.2MB 0 ❌
graph TD
    A[源码] --> B[builder 阶段:编译]
    B --> C[静态二进制]
    C --> D[distroless 运行阶段]
    D --> E[最小攻击面]

2.4 构建时注入版本号、Git SHA与构建时间的自动化方案

核心注入时机

在构建流水线的 pre-build 阶段,通过环境变量或构建参数将元数据注入源码,避免运行时依赖外部服务。

实现方式对比

方案 优点 缺点
编译期模板替换(如 sed/envsubst) 轻量、无额外依赖 不支持嵌套结构,易出错
构建工具原生支持(Gradle/Maven/webpack) 类型安全、可验证 需适配不同工具链
Go ldflags / Rust --cfg 零运行时开销、静态链接 仅限特定语言

Gradle 示例(Kotlin DSL)

// build.gradle.kts
tasks.withType<ProcessResources> {
    inputs.property("gitSha", providers.exec { commandLine("git", "rev-parse", "--short", "HEAD") }.standardOutput.asText.get().trim())
    inputs.property("buildTime", java.time.Instant.now().toString())
    inputs.property("version", project.version.toString())

    from(sourceSets.main.get().resources.srcDirs) {
        expand(
            "GIT_SHA" to providers.exec { commandLine("git", "rev-parse", "--short", "HEAD") }.standardOutput.asText.get().trim(),
            "BUILD_TIME" to java.time.Instant.now().toString(),
            "VERSION" to project.version.toString()
        )
    }
}

该配置在资源处理阶段动态展开占位符(如 ${GIT_SHA}),确保每次构建生成唯一标识;providers.exec 延迟执行 Git 命令,避免缓存污染;Instant.now() 保证构建时间精确到纳秒级。

流程示意

graph TD
    A[触发 CI 构建] --> B[执行 git rev-parse]
    B --> C[获取短 SHA 和当前时间]
    C --> D[注入 resources/application.properties]
    D --> E[编译打包]

2.5 Go小程序健康检查端点设计与liveness/readiness探针对齐

Go 小程序需严格对齐 Kubernetes 的探针语义,避免误判驱逐。liveness 表示进程是否存活(崩溃即重启),readiness 表示服务是否可接收流量(如依赖未就绪则下线)。

健康检查端点路由设计

// /healthz: liveness — 仅检查自身 goroutine 和内存压力
// /readyz: readiness — 额外校验 Redis 连接、DB ping、配置热加载状态
r.GET("/healthz", healthHandler(livenessProbe))
r.GET("/readyz", healthHandler(readinessProbe))

逻辑分析:/healthz 不触发外部依赖调用,避免级联失败;/readyz 使用带超时(3s)的 redis.Ping(ctx),失败返回 503 并记录 reason: "redis-unavailable"。

探针配置建议(K8s YAML 片段)

探针类型 initialDelaySeconds periodSeconds timeoutSeconds failureThreshold
liveness 15 10 2 3
readiness 5 5 3 2

状态响应结构统一

{
  "status": "ok",
  "timestamp": "2024-06-12T08:30:45Z",
  "checks": [
    { "name": "goroutine-count", "status": "ok", "value": 42 },
    { "name": "redis-ping", "status": "ok" }
  ]
}

该结构支持 Prometheus 指标自动提取,且兼容 kubectl get pods -o wide 的探针状态聚合。

第三章:GitLab CI深度集成与流水线编排

3.1 .gitlab-ci.yml语义化分层设计:stages、rules与dynamic includes

GitLab CI/CD 配置的可维护性,始于清晰的语义分层。stages 定义执行时序骨架,rules 实现条件驱动的作业调度,而 dynamic includes 支持配置即代码(IaC)式的模块化复用。

分层职责解耦

  • stages: 声明逻辑阶段(如 build, test, deploy),不绑定具体作业
  • rules: 替代过时的 only/except,支持布尔表达式、变量匹配与变更文件路径判断
  • dynamic includes: 通过 !reference 或 include: remote 按需加载 YAML 片段

动态包含示例

include:
  - template: Security/SAST.gitlab-ci.yml  # 官方模板
  - remote: https://gitlab.example.com/team/shared/-/raw/main/ci/lint.yml  # 远程片段
  - local: '/ci/deploy/base.yml'  # 项目内模块

该配置实现“一次定义、多处复用”:远程 URL 支持版本锚定(如 ?ref=v2.3),local 路径支持通配符(/ci/steps/*.yml)。GitLab 在 pipeline 创建时解析并合并 YAML,确保最终配置为单体结构。

rules 行为对比表

条件类型 示例 触发时机
$CI_PIPELINE_SOURCE == "merge_request_event" MR 创建/更新时运行 pipeline 初始化阶段
changes: ["src/**/*"] 仅当 src 目录有变更才执行 Git diff 静态分析后
graph TD
  A[Pipeline Trigger] --> B{rules 评估}
  B -->|匹配成功| C[加入stage队列]
  B -->|全部失败| D[跳过作业]
  C --> E[按stages顺序执行]

3.2 并行化单元测试与覆盖率收集:go test -race + gocov + codecov.io对接

Go 原生支持高并发测试执行,-p 参数可控制并行度,而 -race 能动态检测竞态条件:

go test -p 4 -race -covermode=atomic -coverprofile=coverage.out ./...
  • -p 4:限制最多 4 个测试包并行构建/运行
  • -race:启用竞态检测器(仅支持 go test,不可与 -covermode=count 共用)
  • -covermode=atomic:线程安全的覆盖率统计模式,适配并行测试

覆盖率生成后,需转换为 Codecov 兼容格式:

工具 作用
gocov 解析 coverage.out
gocov-html 生成本地 HTML 报告
codecov CLI 上传至 codecov.io 服务
gocov convert coverage.out | codecov

该命令将 Go 原生覆盖率转换为 JSON 并推送至云端,自动关联 PR 与分支状态。

3.3 构建缓存策略与Go module proxy加速:自建proxy与CI cache双优化

自建 Go Proxy 服务

使用 athens 快速部署私有 module proxy:

# docker-compose.yml 片段
version: '3.8'
services:
  athens:
    image: gomods/athens:v0.22.0
    environment:
      - ATHENS_DISK_STORAGE_ROOT=/var/lib/athens
      - ATHENS_GO_BINARY_ENV=go1.22.3
    volumes:
      - ./athens-storage:/var/lib/athens
    ports:
      - "3000:3000"

该配置启用磁盘持久化存储与指定 Go 版本环境,确保模块校验一致性;ATHENS_DISK_STORAGE_ROOT 决定缓存路径,避免容器重启后丢失依赖。

CI 构建层缓存协同

GitHub Actions 中复用 actions/cache 缓存 GOPATH/pkg/mod:

缓存键模板 说明
go-mod-${{ hashFiles('**/go.sum') }} 基于依赖指纹精准命中
go-build-${{ runner.os }}-${{ hashFiles('**/*.go') }} 源码变更时自动失效

双链路加速效果

graph TD
  A[go build] --> B{Go Proxy?}
  B -->|Yes| C[从私有athens拉取]
  B -->|No| D[直连proxy.golang.org]
  C --> E[命中CI缓存]
  D --> F[全量下载+解压]
  E --> G[秒级构建]

二者叠加后,典型项目 CI 构建时间下降 65%+,模块重复拉取归零。

第四章:Kubernetes滚动发布与可观测性闭环

4.1 Helm Chart结构化封装:values.yaml参数化与templates动态渲染

Helm Chart 的核心在于解耦配置与模板,实现可复用的声明式部署。

values.yaml:配置的单一可信源

定义环境无关的默认值与覆盖入口:

# values.yaml
replicaCount: 3
image:
  repository: nginx
  tag: "1.25-alpine"
ingress:
  enabled: true
  hosts:
    - host: app.example.com
      paths: ["/"]

该文件为 helm install 提供默认参数;--set 或 -f 可覆盖其中任意键,如 replicaCount 控制扩缩容粒度,ingress.hosts 驱动模板中路由生成逻辑。

templates/下的动态渲染机制

使用 Go 模板语法注入 values 数据:

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      containers:
      - name: {{ include "chart.fullname" . }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"

{{ .Values.* }} 引用层级路径,include 调用 _helpers.tpl 中定义的命名模板,确保命名一致性。

参数化与渲染协同流程

graph TD
  A[values.yaml] --> B[helm install]
  C[templates/*.yaml] --> B
  B --> D[Go template engine]
  D --> E[渲染后 Kubernetes YAML]
组件 作用 可变性
values.yaml 提供参数输入 高(可覆盖)
templates/ 定义资源结构与逻辑分支 低(静态)
_helpers.tpl 封装通用命名/标签逻辑 中

4.2 基于K8s Deployment+Service+Ingress的灰度发布基线配置

灰度发布需在不中断服务的前提下,将新版本流量逐步导流。核心依赖三类原生资源协同:Deployment 控制副本与版本标签,Service 提供稳定 ClusterIP,Ingress 实现基于 Host/Path 的七层路由分流。

标签与选择器对齐策略

  • 所有资源必须统一使用 app: api-server 和 version: v1.0/v1.1 双标签
  • Service 的 selector 仅匹配 app,Ingress 的 Canary 注解则依赖 version 区分后端

示例:带灰度注解的 Ingress 配置

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"  # 10% 流量导向 v1.1
    nginx.ingress.kubernetes.io/canary-by-header: "x-canary"
spec:
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: api-service  # 指向同一 Service,由 Endpoint 自动负载
            port:
              number: 80

逻辑分析:该 Ingress 通过 canary-weight 实现基于权重的灰度,canary-by-header 支持人工 Header 强制路由。关键在于两个 Deployment(v1.0/v1.1)均关联同一 Service,其 Endpoints 由标签自动聚合,Ingress 控制器依据注解动态调整 upstream 权重。

灰度生效链路

graph TD
  A[Client] --> B[Ingress Controller]
  B -->|10% 流量| C[Deployment v1.1 Pod]
  B -->|90% 流量| D[Deployment v1.0 Pod]
  C & D --> E[Shared Service]
组件 关键作用 必填字段示例
Deployment 版本隔离、滚动更新 spec.template.metadata.labels.version
Service 解耦路由与实例,提供稳定入口 spec.selector.app == Deployment label
Ingress 实现 HTTP 层灰度策略 annotations 中 canary-* 系列注解

4.3 Prometheus指标埋点与Gin/Echo中间件集成:QPS、延迟、错误率实时采集

核心指标定义与Prometheus注册

Prometheus需暴露三类关键业务指标:

  • http_requests_total(Counter):按method、status、path维度统计请求总量
  • http_request_duration_seconds(Histogram):记录请求延迟分布(buckets: 0.01,0.05,0.1,0.25,0.5,1,2,5)
  • http_requests_failed_total(Counter):仅捕获5xx/4xx中需告警的错误(如500、503、429)

Gin中间件实现(带注释)

func PrometheusMetrics() gin.HandlerFunc {
    reg := prometheus.DefaultRegisterer
    // 注册直方图,自动绑定le标签
    duration := promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            Buckets: []float64{0.01, 0.05, 0.1, 0.25, 0.5, 1, 2, 5},
        },
        []string{"method", "path", "status"},
    )
    reg.MustRegister(duration)

    return func(c *gin.Context) {
        start := time.Now()
        c.Next()

        status := strconv.Itoa(c.Writer.Status())
        duration.WithLabelValues(c.Request.Method, c.FullPath(), status).Observe(time.Since(start).Seconds())
    }
}

逻辑分析:该中间件在c.Next()前后捕获请求生命周期,Observe()自动将延迟映射至对应bucket;FullPath()保留路由模板(如/api/v1/users/:id),避免标签爆炸。promauto确保指标在首次使用时自动注册,规避重复注册panic。

指标采集效果对比

框架 中间件注册方式 延迟精度 路由标签稳定性
Gin Use(middleware) ✅ sub-ms ✅ 支持路径参数
Echo MiddlewareFunc ✅ sub-ms ⚠️ 需手动提取echo.Route.Path

数据流拓扑

graph TD
    A[HTTP Request] --> B[Gin/Echo Router]
    B --> C[Prometheus Middleware]
    C --> D[Observe Duration & Count]
    D --> E[Scrape Endpoint /metrics]
    E --> F[Prometheus Server]
    F --> G[Alertmanager/Grafana]

4.4 Argo Rollouts或原生RollingUpdate对比实践:渐进式流量切换与自动回滚触发

核心能力差异概览

  • 原生 RollingUpdate:仅支持副本数线性增减,无流量控制、无指标感知回滚;
  • Argo Rollouts:支持蓝绿、金丝雀、分阶段发布,可基于 Prometheus 指标自动中止/回滚。

渐进式切换对比示例

# Argo Rollouts 金丝雀策略(片段)
strategy:
  canary:
    steps:
    - setWeight: 10      # 首批10%流量
    - pause: {duration: 60s}  # 观察60秒
    - setWeight: 30      # 扩至30%
    - analysis:          # 启动指标分析
        templates: [{templateName: success-rate}]

setWeight 控制服务网格(如Istio)路由权重;analysis 引用预定义的Prometheus查询模板,失败时自动触发回滚。

自动回滚触发条件对比

能力维度 RollingUpdate Argo Rollouts
健康检查 ✅ Liveness/Readiness ✅ + 自定义指标
回滚响应时机 手动或CI重发 ✅ 实时指标异常 → 秒级回滚
回滚粒度 全量滚动 ✅ 按阶段回退(如从30%→10%→0%)

发布状态流转(Mermaid)

graph TD
  A[Start Canary] --> B[Route 10% Traffic]
  B --> C{Metrics OK?}
  C -->|Yes| D[Advance to 30%]
  C -->|No| E[Abort & Rollback]
  D --> F[Full Promotion]

第五章:总结与展望

技术演进的现实映射

在2023年某省级政务云平台升级项目中,团队将Kubernetes集群从1.22升级至1.28,同步迁移37个核心微服务。升级后API Server平均响应延迟下降42%,但发现CustomResourceDefinition(CRD)v1beta1版本在1.25+中被完全弃用,导致两个旧版审计插件失效——这直接触发了灰度发布中断。最终通过自动化脚本批量重写CRD定义,并结合Open Policy Agent(OPA)注入RBAC校验逻辑,实现零停机平滑过渡。该案例印证了版本兼容性必须嵌入CI/CD流水线的准入检查环节,而非仅依赖文档对照。

工具链协同效能量化

下表展示了三个典型DevOps团队在引入GitOps实践前后的关键指标对比(数据来源:CNCF 2024年度报告抽样):

团队 平均部署频率 配置漂移事件/月 故障恢复MTTR
A(传统CI/CD) 12次/周 8.3 47分钟
B(Argo CD + Helm) 86次/周 0.9 9分钟
C(Flux v2 + Kustomize) 132次/周 0.2 3.5分钟

值得注意的是,团队C在采用Flux时强制要求所有环境配置通过Kustomize overlays分层管理,其base/目录禁止直接修改,所有变更必须经PR评审并触发自动化diff验证——这种约束机制将配置错误率降至0.2次/月。

生产环境故障根因分析

# 某电商大促期间Prometheus告警溯源命令
kubectl get pods -n payment --field-selector status.phase=Failed -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[0].state.terminated.reason}{"\n"}{end}'
# 输出显示17个Pod因OOMKilled终止,进一步排查发现Java应用未设置-XX:MaxRAMPercentage参数,导致容器内存超限被cgroup kill

该问题在压测阶段未暴露,因测试环境资源限制宽松。上线后通过eBPF工具bcc/biosnoop捕获到JVM频繁触发major GC,最终通过JVM参数调优+容器memory.limit_in_bytes硬限制双保险解决。

架构韧性验证实践

graph LR
A[用户请求] --> B[Service Mesh入口网关]
B --> C{是否启用熔断?}
C -->|是| D[Envoy统计错误率>50%]
C -->|否| E[转发至下游服务]
D --> F[返回降级响应页]
F --> G[异步写入Redis缓存]
G --> H[定时任务补偿库存]

在2024年春节抢购活动中,订单服务因数据库连接池耗尽触发熔断,系统自动切换至Redis缓存兜底,同时后台补偿任务在3分钟内完成12.7万笔订单状态同步,用户侧感知延迟

开源生态协作新范式

Linux基金会主导的Sigstore项目已在GitHub Actions中集成cosign签名验证,某金融客户将其嵌入镜像构建流程:每次push镜像前自动执行cosign sign --key $KEY_PATH $IMAGE,CI流水线强制校验cosign verify --key $PUB_KEY $IMAGE。2024年Q1该客户拦截3起恶意镜像篡改事件,其中2起源于第三方基础镜像仓库漏洞。

未来技术落地路径

边缘AI推理框架TensorRT-LLM已支持动态批处理(Dynamic Batching),某智能工厂视觉质检系统实测显示:在Jetson AGX Orin设备上,单卡并发处理23路1080p视频流时,端到端延迟稳定在112ms±8ms,较传统TensorRT方案降低37%显存占用。下一步计划将模型切片与KubeEdge的EdgeMesh网络结合,实现跨厂区模型热更新。

安全合规闭环建设

GDPR第32条要求“适当的技术与组织措施”,某跨境医疗平台通过Terraform模块化声明式定义AWS安全组规则,所有规则变更必须关联Jira合规工单ID,并由HashiCorp Sentinel策略引擎实时校验:禁止开放0.0.0.0/0的SSH端口、强制启用S3桶加密、要求RDS实例开启审计日志。2024年审计中该策略拦截17次高危配置提交。

成本优化真实账单

某SaaS企业通过Kubecost监控发现:开发环境命名空间中32个长期空闲Pod每月产生$1,842闲置费用。实施基于标签的自动伸缩策略后,非工作时间自动缩减至1副本,周末完全休眠,季度节省$5,217——这笔资金被重新投入Prometheus远程存储的Thanos对象存储压缩优化。

工程文化落地抓手

在团队推行“可观测性即代码”实践中,要求每个微服务必须提供标准OpenTelemetry Collector配置模板,且所有trace采样率配置需通过ConfigMap注入而非硬编码。当某支付服务因采样率从1%误设为100%导致Jaeger后端过载时,运维团队3分钟内通过kubectl patch configmap otel-config -p '{"data":{"sampling_rate":"0.01"}}'完成热修复,避免了服务重启。

深入 goroutine 与 channel 的世界,探索并发的无限可能。

发表回复

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