Posted in

【绝密架构图】飞桨+Golang微服务集群拓扑(含gRPC流式推理、负载感知调度、模型版本灰度)

第一章:飞桨+Golang微服务集群的架构演进与设计哲学

传统AI服务部署常面临模型推理延迟高、资源隔离弱、业务耦合紧等痛点。飞桨(PaddlePaddle)作为国产全栈深度学习平台,其轻量级推理引擎 Paddle Inference 与 Golang 高并发、低内存占用的特性天然互补——前者专注模型计算效率,后者承担服务治理、流量调度与协议适配,共同构成“计算-编排”分层解耦的现代AI微服务范式。

核心设计理念

  • 职责分离:Golang 服务仅暴露 REST/gRPC 接口、处理认证/限流/熔断,不加载模型;飞桨推理以独立进程(paddle_inference_server)或容器化子服务形式运行,通过 Unix Domain Socket 或本地 gRPC 通信
  • 弹性伸缩:模型实例按需启停,Golang 网关基于 Prometheus 指标(如 inference_latency_seconds)触发 Horizontal Pod Autoscaler
  • 版本共存:不同模型版本通过命名空间隔离,Golang 路由层依据请求 Header X-Model-Version: v2.1 动态转发至对应飞桨服务端点

关键集成实践

启动飞桨推理服务(启用 TensorRT 加速):

# 编译含 TensorRT 的 Paddle Inference 库后执行
paddle_inference_server \
  --model_dir=./models/resnet50_v2/ \
  --use_trt=1 \
  --trt_precision=fp16 \
  --server_port=8989 \
  --use_gpu=1  # 启用 GPU 推理

Golang 服务通过 grpc-go 客户端调用该服务,连接复用并设置超时:

conn, _ := grpc.Dial("127.0.0.1:8989", 
  grpc.WithTransportCredentials(insecure.NewCredentials()),
  grpc.WithBlock(),
  grpc.WithTimeout(3*time.Second)) // 严格控制推理链路耗时

技术选型对比

维度 纯 Python Flask + Paddle Serving 飞桨 + Golang 微服务集群
并发吞吐 ~300 QPS(GIL 限制) >3000 QPS(goroutine 调度)
内存驻留 模型与服务共享进程,易 OOM 模型进程独立,OOM 不影响网关
灰度发布 需重启服务 Golang 路由动态更新后端列表

该架构将 AI 工程能力沉淀为可编排、可观测、可治理的基础设施能力,而非嵌入业务逻辑的黑盒组件。

第二章:飞桨模型服务化核心能力构建

2.1 飞桨PaddleInference与Go binding的零拷贝内存桥接实践

零拷贝桥接的核心在于共享底层 float32 内存页,避免 CPU 多次搬运。飞桨 PaddleInference 的 PaddleTensor 支持 SetExternalData 接口注入外部内存地址,而 Go 侧通过 unsafe.Sliceruntime.KeepAlive 确保生命周期可控。

数据同步机制

需严格保证:

  • Go 分配的 []float32 底层指针在推理全程有效;
  • Paddle 调用 Predictor.Run() 前不触发 GC 回收;
  • 输入/输出 Tensor 的 shape 与 dtype 由 Go 侧预校验。

关键代码示例

// 创建与模型输入形状匹配的切片(假设为 [1,3,224,224])
data := make([]float32, 1*3*224*224)
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&data))
tensor.SetExternalData("input_0", paddle.Float32, 
    []int64{1,3,224,224}, 
    unsafe.Pointer(hdr.Data))

hdr.Data 提供原始内存地址;SetExternalData 不复制数据,仅绑定指针;[]int64 形状必须与模型输入完全一致,否则触发未定义行为。

维度 Go 侧职责 Paddle 侧职责
内存管理 runtime.KeepAlive(data) 延续生命周期 仅读写,不释放或 realloc
类型对齐 unsafe.Alignof(float32(0)) == 4 强制 float32 解析,拒绝 float64
graph TD
    A[Go: make([]float32, N)] --> B[Go: unsafe.Pointer(hdr.Data)]
    B --> C[Paddle: SetExternalData]
    C --> D[Predictor.Run()]
    D --> E[Go: data 可直接读取输出]

2.2 gRPC流式推理协议设计:StreamingPredictService接口定义与双向流压控实现

核心接口定义(IDL片段)

service StreamingPredictService {
  // 双向流:客户端持续发送分块输入,服务端实时返回增量预测结果
  rpc StreamPredict(stream PredictRequest) returns (stream PredictResponse);
}

message PredictRequest {
  bytes input_chunk = 1;           // 原始数据分块(如音频帧、文本token流)
  bool eos = 2;                    // end-of-stream 标志,指示输入终止
  uint32 sequence_id = 3;         // 客户端维护的会话序列号,用于乱序恢复
}

message PredictResponse {
  bytes output_chunk = 1;          // 模型输出分块(如TTS声学特征、LLM token)
  float confidence = 2;           // 当前chunk置信度(可选)
  uint32 sequence_id = 3;         // 与请求对齐,保障流内顺序一致性
}

此定义支持低延迟语音识别与长上下文生成场景。sequence_id 实现跨网络抖动下的逻辑有序交付;eos 显式解耦流生命周期管理,避免超时误判。

双向流压控机制关键策略

  • 客户端侧:基于服务端PredictResponse中隐含的ACK节拍(通过sequence_id回传确认)动态调节发送速率
  • 服务端侧:采用令牌桶限流 + 内存水位反馈(grpc-status-details-bin携带memory_usage_pct
  • 协议层协同:利用gRPC WriteOptions{ NoCompress: true } 减少小包压缩开销,提升吞吐稳定性

流控参数映射表

参数名 作用域 典型值 影响维度
initial_window_size 连接级 4MB 初始接收缓冲上限
max_message_length 通道级 8MB 单消息硬上限
flow_control_window 流级 动态调整(64KB~2MB) 实时窗口自适应反馈
graph TD
  A[Client Send PredictRequest] -->|含sequence_id & eos| B[gRPC Transport]
  B --> C[Server Flow Controller]
  C -->|令牌桶检查+内存水位| D[Model Executor]
  D --> E[Build PredictResponse]
  E -->|回传sequence_id & confidence| B
  B --> F[Client Rate Limiter]
  F -->|调整chunk size / interval| A

2.3 模型加载热插拔机制:基于Watcher的ONNX/PDModel动态注册与生命周期管理

模型服务需响应实时业务需求,支持无停机更新。核心在于监听模型目录变更,并自动完成解析、校验、注册与旧版本卸载。

监听与触发流程

from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class ModelWatcher(FileSystemEventHandler):
    def on_created(self, event):
        if event.src_path.endswith((".onnx", ".pdmodel")):
            load_and_register(event.src_path)  # 触发动态加载

on_created 仅捕获新增文件事件;endswith 过滤模型格式;load_and_register() 封装解析、SHA256校验、元数据注入及路由映射更新。

生命周期状态表

状态 触发条件 行为
PENDING 文件写入完成 启动异步校验与IR解析
ACTIVE 校验通过且无冲突 注册至ModelRegistry
DEPRECATED 新同名模型上线 原实例标记为只读,延迟卸载

状态流转(mermaid)

graph TD
    A[File Created] --> B{Valid Format?}
    B -->|Yes| C[SHA256 + Opset Check]
    C -->|Pass| D[Register & Route Update]
    D --> E[ACTIVE]
    B -->|No| F[Skip]
    E --> G[New Version Arrives]
    G --> H[DEPRECATED → GC]

2.4 多精度推理支持:INT8量化模型在Go服务中的Tensor内存对齐与校验策略

内存对齐约束

INT8张量需满足 16-byte 边界对齐,否则 AVX512 VNNI 指令触发 #GP 异常。Go runtime 不保证 []int8 底层分配对齐,须显式调用 alignedalloc

// 分配 16-byte 对齐的 INT8 tensor buffer
buf := alignedalloc(int64(size), 16) // size: total bytes; 16: alignment boundary
defer alignedfree(buf)

alignedalloc 封装 mmap(MAP_ANONYMOUS|MAP_PRIVATE) 并手动调整起始地址,确保 uintptr(buf) % 16 == 0

校验策略

  • 运行时校验:加载后立即验证首地址对齐性与数据范围([-128, 127])
  • 静态校验:ONNX 导出阶段注入 quantization_param 元数据校验钩子
校验项 方法 触发时机
地址对齐 uintptr(buf) & 0xF 初始化后
值域合规 for _, v := range buf { if v < -128 || v > 127 {…} } 推理前

数据流校验流程

graph TD
    A[Load INT8 Model] --> B{Aligned?}
    B -->|No| C[panic: misaligned tensor]
    B -->|Yes| D[Range Check]
    D -->|Out of bounds| E[reject input tensor]
    D -->|Valid| F[Dispatch to VNNI kernel]

2.5 推理上下文隔离:goroutine-local ExecutionScope与PaddlePlace资源绑定实践

在高并发推理服务中,避免跨 goroutine 的状态污染是关键。Paddle Serving 通过 ExecutionScope 实现 goroutine 局部执行上下文,配合 PaddlePlace(CPU/GPU 设备标识)完成资源精准绑定。

核心绑定机制

  • 每个 goroutine 初始化时独占一个 ExecutionScope
  • PaddlePlace 在 scope 创建时静态指定,不可跨协程共享
  • 内存池、Tensor 缓存、流句柄均按 place 分区隔离

示例:scope 与 place 绑定代码

// 创建 GPU0 上下文(仅该 goroutine 可访问)
scope := NewExecutionScope(
    WithPaddlePlace(NewPaddlePlace("gpu", 0)), // 指定GPU设备索引
    WithMemoryPool(true),                       // 启用本地内存池
)

NewPaddlePlace("gpu", 0) 显式声明物理设备单元;WithMemoryPool(true) 触发 per-scope CUDA memory pool 分配,规避 cudaMalloc 全局锁争用。

资源隔离效果对比

维度 全局共享模式 goroutine-local scope
Tensor 生命周期 需显式同步 自动随 goroutine 退出回收
GPU stream 复用 易发生跨协程阻塞 每 scope 独占 default stream
graph TD
    A[goroutine A] -->|绑定| B[ExecutionScope A<br>→ PaddlePlace GPU:0]
    C[goroutine B] -->|绑定| D[ExecutionScope B<br>→ PaddlePlace CPU:0]
    B --> E[独立Tensor缓存池]
    D --> F[独立CPU线程绑定]

第三章:Golang微服务治理层深度集成

3.1 负载感知调度器:基于eBPF采集的GPU显存/CPU负载指标驱动的Pod级路由决策

传统Kubernetes调度器缺乏实时硬件级负载可见性,导致GPU密集型任务常因显存争抢或CPU热区引发SLO违规。本方案通过eBPF程序在内核态无侵入采集nvidia-smi等不可达指标,并注入调度上下文。

数据采集层

// bpf_gpu_stats.c:挂载到nvidia_driver的ioctl入口点
SEC("kprobe/nv_ioctl")
int BPF_KPROBE(track_gpu_usage) {
    u64 mem_used = bpf_get_current_pid_tgid(); // 实际通过nv_device读取显存映射页
    bpf_map_update_elem(&gpu_metrics, &pid, &mem_used, BPF_ANY);
    return 0;
}

该eBPF程序绕过用户态轮询开销,以

调度决策流程

graph TD
    A[eBPF采集GPU/CPU实时负载] --> B[指标聚合至metrics-server]
    B --> C[调度器插件Filter阶段校验阈值]
    C --> D{显存>85%? CPU>90%?}
    D -->|是| E[排除该Node]
    D -->|否| F[Score阶段加权排序]

关键指标映射表

指标类型 采集方式 更新频率 用途
GPU显存 eBPF读取GPU BAR 100ms Pod亲和性过滤
CPU缓存命中率 perf_event_open 500ms 避免LLC争用节点

3.2 模型版本灰度发布体系:Consul KV + Webhook的VersionedModelRouter动态权重分发

核心架构设计

VersionedModelRouter 作为流量调度中枢,监听 Consul KV 中 /models/<name>/config 路径变更,通过 Webhook 触发实时权重重载。所有模型实例注册为 Consul 服务,携带 versionweight 标签。

数据同步机制

Consul KV 存储结构示例:

{
  "name": "recommend-v2",
  "versions": [
    {"id": "v2.1", "weight": 30, "endpoint": "http://m21:8080"},
    {"id": "v2.2", "weight": 70, "endpoint": "http://m22:8080"}
  ]
}

逻辑分析:Router 解析 JSON 后构建加权轮询池;weight 为整数百分比(总和需为100),变更后触发平滑 reload,无请求中断。endpoint 必须可被集群内 DNS 解析。

流量分发流程

graph TD
  A[Client Request] --> B{VersionedModelRouter}
  B -->|查Consul KV| C[获取当前权重配置]
  C --> D[按weight比例路由至对应模型实例]
  D --> E[v2.1:30% / v2.2:70%]

关键保障能力

  • ✅ 权重热更新延迟
  • ✅ 支持单版本灰度、AB测试、金丝雀发布
  • ❌ 不依赖 Kubernetes Ingress 或 Service Mesh

3.3 分布式追踪增强:OpenTelemetry Span注入Paddle执行图节点与gRPC流事件关联分析

为实现端到端可观测性,需将 OpenTelemetry 的 Span 生命周期深度嵌入 PaddlePaddle 动态图执行流程,并与 gRPC 流式调用事件对齐。

Span 注入执行图节点

paddle.jit.to_static 编译后图中,通过 paddle.fluid.core.Tracertrace_op 钩子注入 Span

from opentelemetry import trace
tracer = trace.get_tracer("paddle.tracer")

def traced_op_hook(op_type, inputs, attrs):
    with tracer.start_as_current_span(f"paddle.{op_type}", 
                                      context=propagator.extract(carrier)) as span:
        span.set_attribute("paddle.op.attrs", str(attrs))
        span.set_attribute("paddle.op.inputs.shape", [x.shape for x in inputs])

此钩子在每个算子执行前启动 Span,携带上游 carrier(如 HTTP headers 或 gRPC metadata)实现跨进程上下文传播;attrs 和输入 shape 被设为 Span 属性,用于后续性能归因分析。

gRPC 流事件时间对齐

gRPC ServerInterceptor 中提取 request_id 并注入 Span,与 Paddle 图中同 ID 的 Span 关联:

字段 来源 用途
trace_id OTel 全局生成 跨服务唯一标识
request_id gRPC metadata 关联流式请求与模型推理阶段
span_id OTel 自动分配 定位具体算子或 RPC 方法

关联分析流程

graph TD
    A[gRPC Unary/Streaming Call] --> B[Extract metadata → carrier]
    B --> C[Paddle Graph Execution]
    C --> D[Span per op with request_id tag]
    D --> E[Export to Jaeger/OTLP]
    E --> F[Trace-level filtering by request_id]

该机制使单次推理请求的 gRPC 网络耗时、序列化开销、算子执行延迟可在同一 Trace 中分层下钻分析。

第四章:高可用生产级部署工程实践

4.1 K8s Operator化模型服务编排:CustomResourceDefinition定义ModelService与InferenceJob

在Kubernetes中,Operator模式通过自定义资源扩展原生API能力,实现模型服务生命周期的声明式管理。

ModelService CRD核心字段设计

定义模型服务抽象,支持版本灰度、自动扩缩与健康探针:

# modelservice.crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: modelservices.inference.example.com
spec:
  group: inference.example.com
  versions:
  - name: v1alpha1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              modelUri: {type: string, description: "OSS/S3/HTTP模型路径"}
              runtime: {type: string, enum: ["torchserve", "triton", "vllm"]}
              minReplicas: {type: integer, default: 1}

该CRD将模型服务解耦为可版本化、可观测、可策略驱动的K8s原生资源。modelUri支持热更新触发滚动部署;runtime枚举约束确保运行时兼容性校验。

InferenceJob CRD:面向批推理的作业抽象

字段 类型 说明
modelRef.name string 关联ModelService名称
inputSource string S3/MinIO路径或PVC挂载点
outputSink string 结果写入目标URI

控制器协同流程

graph TD
  A[ModelService创建] --> B{验证模型可达性}
  B -->|成功| C[启动预测服务Pod]
  B -->|失败| D[设置status.conditions]
  E[InferenceJob提交] --> F[绑定ModelService Service]
  F --> G[注入input/output卷并调度Job]

4.2 流式推理熔断与降级:基于Hystrix-go扩展的StreamFallbackHandler与空帧兜底策略

在大模型流式响应场景中,下游服务抖动易导致连接中断或帧丢失。我们基于 hystrix-go 扩展实现 StreamFallbackHandler,在熔断触发时无缝切换至降级流。

空帧兜底策略设计

  • 每 500ms 注入一个空 JSON 帧 {"delta":{}},维持客户端心跳与解析状态
  • 降级流保持 text/event-stream MIME 类型与 data: 前缀格式一致性

核心处理逻辑

func NewStreamFallbackHandler() hystrix.GoFunc {
    return func(ctx context.Context) (interface{}, error) {
        select {
        case <-time.After(100 * time.Millisecond):
            return &sse.Event{Data: []byte(`{"delta":{}}`)}, nil // 空帧数据
        case <-ctx.Done():
            return nil, ctx.Err()
        }
    }
}

该函数在超时或熔断时返回标准 SSE 事件对象,Data 字段为合法空 delta 帧;100ms 是最小响应间隔,避免频发空帧冲击客户端解析器。

降级维度 触发条件 输出行为
连接失败 hystrix.ErrTimeout 返回首帧空事件
持续超时 连续3次失败 启动周期性空帧注入
恢复探测 下游健康检查通过 自动切回原始流
graph TD
    A[流式请求] --> B{Hystrix 熔断器}
    B -->|正常| C[原始推理流]
    B -->|熔断/超时| D[StreamFallbackHandler]
    D --> E[生成空帧事件]
    E --> F[按SSE协议写入ResponseWriter]

4.3 模型版本一致性保障:etcd强一致存储的ModelManifest + SHA256签名链校验机制

模型服务上线后,多节点间版本漂移是高频故障源。本机制通过 etcd 的线性一致性读写能力,将 ModelManifest(含模型元数据、依赖清单及前一版本签名)作为唯一事实源持久化。

数据同步机制

etcd 集群采用 Raft 协议确保 ModelManifest 写入全局可见前已获得多数节点确认:

# /models/v2/resnet50/manifest.yaml
version: "v2.3.1"
base: "v2.3.0"  # 父版本引用,构成签名链
sha256: "a1b2c3...f8e9"  # 当前完整 manifest + model.bin 的 SHA256
model_path: "/storage/resnet50-v2.3.1.bin"

此 YAML 被序列化为字节流后,与对应模型二进制文件拼接再哈希,形成不可篡改的绑定指纹;base 字段强制构建签名链,断链即校验失败。

校验流程

graph TD
    A[加载模型时] --> B[从etcd读取当前Manifest]
    B --> C[验证base字段存在且可追溯至根]
    C --> D[下载model.bin + manifest.yaml拼接计算SHA256]
    D --> E{匹配manifest.sha256?}
    E -->|是| F[加载成功]
    E -->|否| G[拒绝加载并告警]

关键参数说明

字段 作用 安全约束
base 构建签名链锚点 必须存在于 etcd 历史版本中
sha256 全量内容摘要 由 manifest + model.bin 联合生成,防篡改

4.4 多集群联邦推理网关:Envoy WASM Filter实现跨Region模型路由与QoS分级调度

核心架构设计

基于 Envoy 的 WASM 扩展能力,构建轻量级、可热更新的推理路由层。Filter 在 HTTP_REQUEST_HEADERS 阶段解析 x-model-idx-qos-level,动态选择目标集群与模型实例。

WASM 路由逻辑(Rust 片段)

// 解析请求头并构造路由决策上下文
let model_id = get_http_header("x-model-id")?;
let qos = get_http_header("x-qos-level").unwrap_or("best-effort".into());

let route = match qos.as_str() {
    "gold" => find_region_endpoint(model_id, "us-west-2", "latency < 50ms"),
    "silver" => find_region_endpoint(model_id, "us-east-1", "p95 < 120ms"),
    _ => find_fallback_endpoint(model_id), // 默认跨Region负载均衡
};
set_route_entry(route);

逻辑分析find_region_endpoint() 基于实时指标服务(Prometheus + Thanos)查询各Region模型服务SLA数据;latencyp95 参数来自 Envoy Stats 接入的全局可观测性管道,保障路由决策具备QoS语义。

QoS分级策略对照表

QoS Level Latency SLA Region Preference Failover Scope
gold us-west-2 Same AZ only
silver us-east-1 Same Region
bronze Any Global

流量调度流程

graph TD
    A[Ingress Request] --> B{Parse x-model-id & x-qos-level}
    B --> C[Query Global Model Registry]
    C --> D[Fetch Real-time SLI from Metrics DB]
    D --> E[Select Endpoint per QoS Policy]
    E --> F[Inject x-upstream-cluster header]

第五章:未来演进方向与生态协同展望

多模态AI驱动的运维闭环实践

某头部云服务商已将LLM与时序预测模型、日志解析引擎深度集成,构建“检测—归因—修复—验证”自动化闭环。当Prometheus告警触发后,系统自动调用微调后的运维专用大模型(基于Qwen2-7B+LoRA),结合Kubernetes事件日志、Jaeger链路追踪快照及历史SOP知识库,生成可执行的kubectl修复指令序列,并经Policy-as-Code引擎(OPA策略校验)安全过滤后提交至GitOps流水线。该方案使P1级故障平均恢复时间(MTTR)从23分钟压缩至4.7分钟,误操作率下降92%。

开源工具链的标准化互操作协议

为解决Prometheus、OpenTelemetry、eBPF探针等组件间指标语义割裂问题,CNCF正在推进OpenMetrics v2规范落地。其核心改进包括:

  • 引入@unit元标签统一计量单位(如http_request_duration_seconds{job="api", @unit="s"}
  • 定义# HELP注释的结构化Schema(支持JSON Schema嵌入)
  • 强制要求__name__字段与OpenAPI 3.1 x-metrics-name映射规则

下表对比了v1与v2在关键场景的兼容性表现:

场景 OpenMetrics v1 OpenMetrics v2 兼容方案
自定义单位声明 不支持 @unit="ms" v2解析器自动转换为v1格式输出
指标生命周期管理 无标准机制 # LIFECYCLE: transient 通过OTel Collector Processor插件注入

边缘智能与中心云的协同推理架构

某工业物联网平台部署了分层推理架构:边缘节点(NVIDIA Jetson Orin)运行轻量化YOLOv8n模型进行实时缺陷检测,每30秒向中心云同步特征向量(非原始图像);中心云大模型(Llama-3-8B)基于联邦学习框架FedML聚合各产线特征,动态更新边缘模型的注意力权重参数。实际产线数据显示,模型漂移导致的漏检率从11.3%降至2.1%,边缘带宽占用减少76%。

graph LR
    A[边缘设备] -->|特征向量/梯度更新| B[MQTT Broker]
    B --> C[FedML Server]
    C -->|全局模型权重| D[模型仓库]
    D -->|OTA推送| A
    C --> E[云侧大模型分析台]
    E -->|根因建议| F[工单系统]

可观测性即代码的工程化落地

某金融科技团队将SLO定义、告警阈值、仪表盘布局全部纳入Git仓库管理,采用Terraform Provider for Grafana + Prometheus Operator CRD实现IaC。例如,支付成功率SLO的SLI表达式被声明为:

resource "grafana_slo" "payment_success" {
  name        = "payment-success-rate"
  description = "99.95% of payments must complete within 2s"
  indicator   = "rate(http_request_duration_seconds_count{job=\"payment\", code=~\"2..\"}[5m]) / rate(http_request_duration_seconds_count{job=\"payment\"}[5m])"
  objectives  = [{ target = 0.9995, window = "30d" }]
}

该配置经CI流水线自动部署至多集群环境,变更审计日志完整记录每次SLO调整的commit hash与审批人。

跨云服务网格的零信任流量治理

在混合云环境中,Istio 1.22与SPIFFE/SPIRE深度集成,实现跨AWS EKS、Azure AKS、自建K8s集群的mTLS双向认证。所有服务间通信强制携带SPIFFE ID(如spiffe://example.org/ns/default/sa/payment),Envoy代理依据授权策略(通过OPA Rego编写)动态决策是否放行gRPC流控请求。某跨国电商在双活架构中已稳定运行该方案超18个月,未发生一次跨云横向渗透事件。

在并发的世界里漫游,理解锁、原子操作与无锁编程。

发表回复

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