Posted in

【Go云原生实战洞察】:免费K8s + Go ≠ 免费可观测——OpenTelemetry Collector企业版收费点全拆解

第一章:Go语言本身是否收费——开源本质与商业现实的再确认

Go 语言自 2009 年由 Google 开源起,即采用 BSD 3-Clause 许可证发布。该许可证明确允许自由使用、修改、分发,无论商用或非商用,均无需支付许可费用、不设地域限制、不强制开源衍生作品。这意味着:任何个人或企业均可免费下载、编译、部署 Go 程序,包括构建闭源商业软件、嵌入式系统、SaaS 平台等。

开源许可的法律保障

BSD 3-Clause 的核心条款包括:

  • 允许无偿使用、复制、修改和再分发;
  • 要求保留原始版权声明、许可声明及免责声明;
  • 禁止使用贡献者姓名为衍生产品背书(非限制性义务)。
    该许可比 GPL 更宽松,不触发“传染性”开源要求,是云原生基础设施(如 Kubernetes、Docker、Terraform)广泛采用 Go 的关键法律基础。

商业生态中的“免费”边界

需明确区分:

  • ✅ Go 编译器、标准库、go 命令行工具完全免费;
  • ✅ 官方文档、学习资源(golang.org)、测试框架(testing 包)零成本获取;
  • ❌ 某些第三方 IDE 插件(如 JetBrains GoLand)或企业级监控平台(如 Datadog 的 Go APM 扩展)可能收费,但它们并非 Go 语言本身组成部分。

验证许可证状态的实操步骤

可通过官方仓库直接核查:

# 克隆 Go 源码仓库(仅需元数据,无需完整下载)
git clone --depth 1 https://go.googlesource.com/go go-license-check
cd go-license-check/src
# 查看根目录 LICENSE 文件(内容即为 BSD 3-Clause 全文)
cat ../../LICENSE

执行后将输出标准 BSD 3-Clause 文本,其中第 1 条明确声明:“Redistributions of source code must retain the above copyright notice…” —— 这是法律效力的直接依据。

项目 是否收费 说明
go install 官方二进制包永久免费提供
go.dev 文档 由 Google 托管,无访问门槛
Go 泛型支持 Go 1.18+ 内置特性,无需额外授权

Go 的免费性不是市场策略,而是其设计哲学的延伸:降低采用门槛,推动统一工具链与工程实践。

第二章:OpenTelemetry Collector企业版收费逻辑深度溯源

2.1 OpenTelemetry规范演进中的商业分层设计(理论)+ 从OTLP协议版本差异看厂商锁定风险(实践)

OpenTelemetry 的规范演进并非纯技术中立,而是隐含三层商业分层:

  • 基础层(W3C Trace Context、OTLP v0.9+):由CNCF托管,强制兼容;
  • 扩展层(Resource Detectors、Semantic Conventions v1.21+):厂商可插拔实现;
  • 增值层(AI异常归因、SLO热力图):闭源API与私有编码格式绑定。

OTLP v0.10 vs v1.0 关键断裂点

字段 v0.10 支持 v1.0 强制要求 风险表现
resource 编码 允许嵌套JSON 必须扁平化key-value 自定义资源标签丢失
status.code string 枚举 int32(0=Ok, 1=Error) APM厂商需重写状态映射逻辑
// OTLP v1.0 trace.proto 片段(关键变更)
message Status {
  // v0.10: optional string code = 1;
  int32 code = 1; // ← 强制数值化,破坏字符串语义兼容性
  string message = 2;
}

此变更迫使采集端(如Jaeger Agent)升级SDK并重写状态转换器,若厂商未同步v1.0 SDK,则trace status字段在后端被静默丢弃——构成典型的协议级锁定:用户无法自由切换接收端而不重构数据管道。

厂商锁定传导路径

graph TD
  A[自研Exporter] -->|硬编码v0.10 status.string| B(云厂商A Collector)
  B -->|拒绝v1.0 int32 status| C[丢弃Trace]
  D[OTel SDK v1.0] -->|默认发送int32| C

2.2 Collector核心组件License矩阵解析(理论)+ 对比Apache-2.0 vs Elastic License 2.0在日志导出器中的合规边界(实践)

License矩阵关键维度

Collector各模块许可状态需从三方面交叉校验:

  • 分发方式(SaaS/On-prem)
  • 修改范围(core/exporter/processor)
  • 衍生作品定义(static link vs plugin interface)

Apache-2.0 与 ELv2 合规差异

维度 Apache-2.0 Elastic License 2.0
商业SaaS分发 ✅ 允许 ❌ 禁止托管式日志导出服务
插件二次开发 ✅ 保留原许可 ⚠️ 导出器若调用elasticsearch_exporter内部API即触发传染性限制

日志导出器典型违规场景

// ❌ ELv2 违规:直接嵌入 elastic/go-elasticsearch/v8 客户端(非接口抽象)
import "github.com/elastic/go-elasticsearch/v8" // → 触发ELv2 Section 3(a)

// ✅ 合规替代:仅依赖标准 exporterhelper 接口
import "go.opentelemetry.io/collector/exporter/exporterhelper"

该导入强制要求所有实现必须通过exporter.CreateSettings注入依赖,隔离底层传输协议许可约束。

许可决策流程

graph TD
    A[新增Exporter] --> B{是否调用Elastic专有API?}
    B -->|是| C[必须采用ELv2兼容模式<br>禁用SaaS部署]
    B -->|否| D[可选Apache-2.0<br>支持全场景分发]

2.3 扩展插件生态的许可陷阱(理论)+ 实测Jaeger Exporter企业版插件在Go构建链中的编译失败场景(实践)

开源可观测性生态中,企业版插件常采用 SSPL 或商业双许可模式,表面兼容 MIT 的 Jaeger 客户端 SDK,实则在 go.mod 中隐式引入闭源 jaeger-exporter-enterprise/v2 模块,触发 Go 构建链的模块校验失败。

许可冲突本质

  • SSPL 要求衍生服务必须开源(含 SaaS 部署场景)
  • Go 的 go build -mod=readonly 拒绝加载未签名/非标准 checksum 的私有模块

实测失败日志片段

# go build ./cmd/app
go: github.com/jaegertracing/jaeger-exporter-enterprise/v2@v2.1.0: 
invalid version: unknown revision v2.1.0

此错误非网络问题:企业版模块未发布至 proxy.golang.org,且 replace 指令无法绕过 sumdb.sum.golang.org 的校验——Go 1.18+ 强制验证 module checksum,而私有模块无官方 sum 条目。

典型规避尝试与后果对比

方案 是否绕过校验 是否合规 构建稳定性
GOPRIVATE=*.enterprise.com ❌(违反 SSPL 第13条) 临时可用,CI 失败率 >67%
go mod edit -replace + 本地 vendor ❌(仍需校验 sum) ⚠️(需审计所有 transitive 依赖) 不可靠
graph TD
    A[go build] --> B{go.mod contains enterprise/v2}
    B -->|Yes| C[fetch from GOPROXY]
    C --> D{sum.golang.org 有记录?}
    D -->|No| E[build failure: “unknown revision”]
    D -->|Yes| F[校验失败:SSPL license mismatch]

2.4 多租户与RBAC能力缺失的技术代价(理论)+ 基于Go原生net/http中间件手写租户隔离层的性能损耗实测(实践)

当系统缺乏原生多租户与RBAC支持时,业务层被迫承担鉴权、数据隔离、上下文透传等职责,导致:

  • 数据访问逻辑与业务逻辑高度耦合
  • 租户ID硬编码引发SQL注入与越权风险
  • 权限变更需全量重启服务,无法热更新

手写租户中间件核心实现

func TenantIsolation(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tenantID := r.Header.Get("X-Tenant-ID") // 从Header提取租户标识
        if tenantID == "" {
            http.Error(w, "missing X-Tenant-ID", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

该中间件在请求生命周期起始注入tenant_id,后续Handler可通过r.Context().Value("tenant_id")安全获取。零依赖、无反射,但每次请求增加约120ns上下文拷贝开销(实测于Go 1.22/AMD EPYC)。

性能对比(10K RPS压测)

实现方式 P99延迟(ms) CPU占用率 上下文分配次数/req
无租户中间件 3.2 41% 0
context.WithValue 3.8 47% 1
graph TD
    A[HTTP Request] --> B{Has X-Tenant-ID?}
    B -->|No| C[401 Unauthorized]
    B -->|Yes| D[Inject tenant_id into Context]
    D --> E[Next Handler]

2.5 自动化可观测性治理的隐性成本(理论)+ 使用Go编写Collector配置热重载控制器替代企业版Policy Engine的工程权衡(实践)

可观测性治理的“自动化”常被误认为仅是功能叠加,实则隐含三类隐性成本:策略漂移导致的语义失真多租户RBAC与采样策略的耦合熵增、以及控制平面与数据平面间心跳同步的时序债务

热重载控制器核心逻辑

以下为基于 fsnotify 的轻量级配置监听器片段:

func NewHotReloadController(configPath string, collector *otelcol.Supervisor) *HotReloadController {
    watcher, _ := fsnotify.NewWatcher()
    watcher.Add(configPath)
    return &HotReloadController{watcher: watcher, collector: collector}
}

// 触发重载时调用 otelcol.Supervisor.Shutdown() + Restart()

fsnotify 仅监听文件变更事件(WRITE/CHMOD),避免轮询开销;Supervisor.Restart() 内部执行平滑过渡:旧 pipeline drain 完毕后才启用新配置,保障 metrics trace 无丢失。

工程权衡对比

维度 企业版 Policy Engine 自研热重载控制器
配置生效延迟 3–8s(gRPC轮询)
多租户策略隔离粒度 Namespace级 文件路径前缀绑定
运维复杂度 依赖独立控制平面 单二进制嵌入
graph TD
    A[Config File Change] --> B{inotify Event}
    B --> C[Parse YAML]
    C --> D[Validate Schema]
    D --> E[Graceful Restart]
    E --> F[New Collector Instance]

第三章:Go云原生可观测栈的成本构成三维模型

3.1 构建时成本:Go module proxy与私有registry的带宽/认证开销(理论+实践)

数据同步机制

Go module proxy(如 proxy.golang.org)默认启用缓存与按需拉取,但首次构建仍需完整下载模块源码及校验文件(@v/list, @v/vX.Y.Z.info, .mod, .zip)。私有 registry(如 JFrog Artifactory 或 Nexus)则额外引入双向 TLS 认证、Bearer Token 刷新与 scope 鉴权链路。

认证开销对比

组件 RTT 增量 首次鉴权延迟 模块复用率影响
公共 proxy ~0 ms 高(全局共享)
私有 registry(OAuth2) +80–200 ms 依赖 token TTL 中(需定期 refresh)

实践验证代码

# 启用详细网络日志,观测认证与下载阶段耗时
GODEBUG=httptrace=1 go mod download github.com/spf13/cobra@v1.8.0 2>&1 | \
  grep -E "(Connect|DNS|TLS|Auth|Got)"

该命令输出包含 DNS 解析、TCP 连接、TLS 握手、HTTP Authorization 头发送、以及首个 200 OK 响应时间戳。关键参数:httptrace 触发 Go 标准库底层网络事件回调;2>&1 合并 stderr/stdout 便于过滤;grep 精准定位认证与传输瓶颈点。

graph TD
  A[go build] --> B{Proxy configured?}
  B -->|Yes| C[GET /github.com/spf13/cobra/@v/v1.8.0.info]
  B -->|No| D[Direct fetch + checksum verification]
  C --> E[TLS handshake + Auth header]
  E --> F[Cache hit?]
  F -->|Yes| G[Stream .zip from cache]
  F -->|No| H[Fetch upstream → store → serve]

3.2 运行时成本:Go runtime metrics采集对P99延迟的影响量化分析(理论+实践)

Go 程序在高负载下,runtime.ReadMemStatsdebug.ReadGCStats 等同步指标读取会触发 STW 片段,显著抬升尾部延迟。

关键观测点

  • GOGC=100 下,每 2–5 秒一次 GC 周期,ReadMemStats 平均耗时 8–15μs,但 P99 达 42μs(实测于 32vCPU/64GB 实例)
  • 指标采集频率 >10Hz 时,goroutine 调度抖动上升 17%

推荐采集策略

// 使用 runtime/metrics(Go 1.17+)替代旧 API,零 STW
import "runtime/metrics"
func sample() {
    set := metrics.All()
    samples := make([]metrics.Sample, len(set))
    for i := range samples {
        samples[i].Name = set[i]
    }
    metrics.Read(samples) // 非阻塞、快照式、无内存分配
}

metrics.Read 底层复用 mcentral 快照机制,避免 memstats 全局锁竞争;samples 切片需复用以规避 GC 压力。

指标源 STW 影响 P99 延迟增量 分配开销
runtime.ReadMemStats +38–42μs ~12KB
runtime/metrics.Read +0.3–0.7μs 零分配

graph TD A[应用请求] –> B{是否启用 metrics.Read?} B –>|是| C[无锁快照 → P99 ≈ 基线] B –>|否| D[memstats 锁竞争 → P99 抬升]

3.3 维护时成本:Collector Go SDK升级引发的语义版本断裂案例复盘(理论+实践)

问题起源:v0.92.0 → v0.93.0 的非兼容变更

OpenTelemetry Collector Go SDK 在 v0.93.0 中将 exporterhelper.QueueSettingsQueueSize 字段从 int 改为 uint32,违反了语义化版本对 PATCH 级别的向后兼容承诺。

关键代码断裂点

// 升级前(v0.92.x)——编译通过
cfg := exporterhelper.QueueSettings{QueueSize: -1} // 允许负值表示禁用队列

// 升级后(v0.93.0)——编译失败:cannot use -1 (untyped int) as uint32 value
cfg := exporterhelper.QueueSettings{QueueSize: -1}

逻辑分析:uint32 类型无法接收负字面量,且 SDK 未提供向后兼容的构造函数或类型别名过渡层;参数 QueueSize 语义本应支持“禁用”语义(如 -1),但新类型强制数值域收缩,导致配置逻辑失效。

影响范围对比

维度 v0.92.x v0.93.0
类型安全性 弱(int 可隐式赋值) 强(uint32 显式约束)
配置表达能力 支持 -1 禁用队列 仅支持 (被解释为“最小队列”)
升级成本 零改造 全量检查 + 适配层封装

根本修复路径

  • 引入适配包装器:
    type SafeQueueSettings struct {
    QueueSize int
    }
    func (s SafeQueueSettings) ToSDK() exporterhelper.QueueSettings {
    if s.QueueSize < 0 { return exporterhelper.QueueSettings{QueueSize: 0} } // 显式降级策略
    return exporterhelper.QueueSettings{QueueSize: uint32(s.QueueSize)}
    }

第四章:规避企业版依赖的Go原生可观测方案实战

4.1 基于Go标准库net/http/httputil构建轻量级trace注入中间件(理论+实践)

HTTP反向代理是实现trace注入的理想切入点——httputil.ReverseProxy天然支持请求/响应拦截,无需侵入业务逻辑。

核心思路

利用Director函数修改原始请求头,注入X-Trace-ID;通过ModifyResponse注入响应头,透传上下文。

proxy := httputil.NewSingleHostReverseProxy(target)
proxy.Director = func(req *http.Request) {
    if req.Header.Get("X-Trace-ID") == "" {
        req.Header.Set("X-Trace-ID", uuid.New().String()) // 自动生成唯一trace ID
    }
}

此处Director在代理转发前执行:req为上游请求副本,Set操作安全无副作用;uuid.New().String()确保每跳独立trace标识,避免ID污染。

关键能力对比

能力 标准ReverseProxy 自定义trace中间件
请求头注入 ✅(via Director) ✅(增强逻辑)
响应头透传 ❌(需ModifyResponse) ✅(自动补全)
无依赖、零外部SDK
graph TD
    A[Client Request] --> B{Has X-Trace-ID?}
    B -->|No| C[Generate & Inject]
    B -->|Yes| D[Pass Through]
    C --> E[Proxy Forward]
    D --> E
    E --> F[ModifyResponse: Add Span-ID]

4.2 使用Go Generics实现跨Exporter的Metrics Schema统一适配器(理论+实践)

为什么需要泛型适配器

Prometheus Exporter(如 node_exporterpostgres_exporter)返回指标结构各异,但监控平台需统一消费。传统接口抽象导致类型擦除与运行时断言,而 Go 泛型可静态约束 Metric 的字段契约。

核心泛型接口设计

type Metric[T any] interface {
    Labels() map[string]string
    Value() T
    Timestamp() time.Time
}

T 约束数值类型(float64/int64),Labels() 强制标准化标签键(jobinstance等),避免各Exporter自定义键名(如 server vs host)。

统一转换器实现

func Adapt[T Number](raw map[string]interface{}) Metric[T] {
    return &genericMetric[T]{
        labels:   extractLabels(raw),
        value:    toNumber[T](raw["value"]),
        ts:       time.Now(),
    }
}

Number 是自定义约束(~float64 | ~int64),extractLabels 归一化 hostinstancedb_namedatabasetoNumber 安全类型转换,失败则 panic(因Exporter数据格式应受契约保障)。

适配效果对比

Exporter 原始标签键 统一后键
node_exporter instance, job ✅ 一致
postgres_exporter server, dbname instance, database
graph TD
    A[Raw JSON from Exporter] --> B{Adapt[float64]}
    B --> C[Metric[float64]]
    C --> D[Unified Metrics Pipeline]

4.3 利用Go embed + text/template动态生成Collector配置的CI/CD流水线(理论+实践)

传统硬编码 Collector 配置易引发环境漂移与维护熵增。Go 1.16+ 的 embed 包可将模板文件编译进二进制,结合 text/template 实现零外部依赖的声明式配置生成。

核心工作流

  • CI 构建阶段读取环境变量(如 ENV=prod, REGION=us-east-1
  • 加载嵌入的 collector.tmpl 模板
  • 渲染为 otel-collector-config.yaml 并注入流水线产物
// main.go:嵌入模板并渲染
import (
    "embed"
    "text/template"
    "os"
)

//go:embed collector.tmpl
var tmplFS embed.FS

func main() {
    t := template.Must(template.ParseFS(tmplFS, "collector.tmpl"))
    cfg := struct {
        Env   string
        Region string
    }{os.Getenv("ENV"), os.Getenv("REGION")}

    f, _ := os.Create("otel-collector-config.yaml")
    t.Execute(f, cfg)
}

逻辑说明:embed.FS 在编译期固化模板;template.Execute 将运行时环境变量注入结构体,安全替换 {{.Env}} 等占位符;输出文件直接供后续部署步骤使用。

模板变量对照表

模板变量 来源 示例值
{{.Env}} CI 环境变量 staging
{{.Region}} Git Tag/Job 参数 ap-southeast-1
graph TD
    A[CI Job 启动] --> B[读取 ENV/REGION]
    B --> C[加载 embed.FS 中 collector.tmpl]
    C --> D[执行 text/template 渲染]
    D --> E[生成 otel-collector-config.yaml]
    E --> F[部署至目标集群]

4.4 基于Go signal.Notify与pprof的Collector进程级健康自愈机制(理论+实践)

自愈触发双通道设计

  • 信号通道:监听 SIGUSR1(手动触发诊断)、SIGUSR2(热重载配置)
  • HTTP健康探针通道/debug/pprof/healthz 返回实时goroutine数、内存增长速率

pprof集成诊断核心逻辑

func setupHealthHandler() {
    mux := http.NewServeMux()
    mux.HandleFunc("/debug/pprof/healthz", func(w http.ResponseWriter, r *http.Request) {
        stats := runtime.MemStats{}
        runtime.ReadMemStats(&stats)
        // 检测30秒内堆增长超200MB则标记异常
        if stats.HeapAlloc > lastHeapAlloc+200*1024*1024 {
            w.WriteHeader(http.StatusServiceUnavailable)
            w.Write([]byte("HEAP_GROWTH_ANOMALY"))
            return
        }
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("OK"))
    })
}

该handler通过runtime.ReadMemStats获取实时内存快照,对比上一次采样值判断内存泄漏风险;HeapAlloc为已分配但未释放的字节数,是关键健康指标。

信号注册与优雅恢复流程

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGUSR1, syscall.SIGUSR2)
go func() {
    for sig := range sigChan {
        switch sig {
        case syscall.SIGUSR1:
            log.Println("Triggering self-healing: GC + pprof dump")
            runtime.GC() // 强制垃圾回收
            pprof.WriteHeapProfile(profileFile) // 保存堆快照
        }
    }
}()

signal.Notify将OS信号转为Go channel事件;SIGUSR1触发即时GC与堆分析,避免OOM前静默崩溃;profileFile需预设带时间戳路径,确保可追溯。

机制 触发条件 动作 恢复时效
信号自愈 手动kill -USR1 $PID GC + 堆快照 + 日志标记
HTTP健康探针 /healthz 超时或失败 上报告警,触发外部重启策略 秒级
graph TD
    A[Collector运行中] --> B{健康检查}
    B -->|SIGUSR1| C[强制GC + 生成heap.pprof]
    B -->|/healthz异常| D[上报Metrics + 触发K8s Liveness Probe]
    C --> E[继续服务]
    D --> F[容器自动重启]

第五章:免费≠零成本,Go云原生可观测的理性成本观

在某电商中台团队落地 Prometheus + Grafana + OpenTelemetry 的过程中,运维负责人曾兴奋地宣称“我们全面采用开源栈,监控零预算上线”。三个月后,SRE 团队却提交了一份《可观测性隐性成本分析报告》——仅因未预估资源开销与人力折损,月均隐性支出达 4.7 万元。

开源组件的资源吞噬效应

以 Go 编写的轻量级 exporter(如 prometheus/client_golang)虽无许可费用,但高频指标采集会显著抬升 CPU 使用率。实测显示:在单节点部署 12 个微服务、每秒采集 800+ 指标时,Go runtime GC 频次上升 3.2 倍,P99 延迟波动从 12ms 跃至 47ms。下表为压测对比数据:

场景 CPU 平均占用率 内存常驻增长 GC Pause P99 日志写入量/小时
无指标采集 18% 1.2ms 8MB
标准指标采集(默认采样) 39% +1.2GB 4.7ms 210MB
全量指标 + 自定义标签 68% +3.8GB 18.3ms 1.4GB

工程师时间才是最大沉没成本

该团队初期使用 go.opentelemetry.io/otel/exporters/otlp/otlptrace 直连 OTLP Collector,但因未配置 gRPC Keepalive 与重试策略,导致 trace 数据丢失率达 23%。排查耗时 17 人日——远超购买商业 APM(如 Honeycomb)的季度订阅费。更关键的是,开发人员被迫学习 W3C Trace Context 规范、Span 生命周期管理、Context 透传陷阱等非业务知识。

成本结构的三维拆解

可观测性真实成本由三部分构成:

  • 基础设施成本:存储(TSDB 磁盘 IOPS 与压缩率)、计算(Prometheus 查询层内存放大)、网络(trace 数据跨 AZ 传输带宽)
  • 人力成本:告警规则调优(平均每个服务需 4.2 小时/月)、仪表盘维护(每季度 15+ 版本迭代适配)、Schema 演进治理(如指标命名规范变更影响 23 个告警模板)
  • 机会成本:因低效诊断导致 MTTR 延长,一次 P1 故障平均多损失 2.1 小时业务黄金时段
// 示例:低成本优化的关键代码片段(避免全量 label 爆炸)
func NewCounterVec() *prometheus.CounterVec {
    return prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_request_total",
            Help: "Total HTTP requests.",
        },
        // 严格限制 label 维度:仅保留 method/status,剔除 user_id/path_pattern
        []string{"method", "status"},
    )
}

商业方案的理性接入时机

当团队出现以下信号时,应启动成本再评估:

  • 告警疲劳:周均有效告警
  • 数据血缘断裂:无法追溯某条慢 SQL 的完整调用链(缺失 DB 层 span)
  • 合规审计失败:GDPR 要求 trace 中敏感字段脱敏,但开源插件无动态掩码能力
flowchart LR
    A[指标采集] --> B{是否含高基数label?}
    B -->|是| C[触发标签降维策略<br>如:user_id → user_tier]
    B -->|否| D[直通TSDB]
    C --> E[写入前哈希映射]
    E --> F[存储成本↓37%<br>查询性能↑2.1x]

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

发表回复

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