第一章:Go链路追踪的核心概念与演进脉络
链路追踪(Distributed Tracing)是观测分布式系统行为的关键技术,用于记录请求在跨服务、跨进程、跨线程调用中的完整生命周期。在 Go 生态中,其核心抽象围绕 Trace(全局唯一标识一次端到端请求)、Span(单次操作的逻辑单元,含起止时间、标签、事件、父级引用)及 Context(携带追踪元数据的传播载体)展开。Go 原生 context.Context 的设计天然适配追踪上下文传递,使轻量级集成成为可能。
追踪模型的标准化演进
早期 Go 项目多依赖自建或简单埋点(如日志打点 + request ID 串联),缺乏统一语义。2016 年 OpenTracing 社区兴起,定义了语言无关的 API 规范;Go SDK(opentracing-go)提供 StartSpanFromContext 等接口,推动框架层(如 Gin、gRPC-Go)快速接入。2019 年 OpenTracing 与 OpenCensus 合并为 OpenTelemetry(OTel),成为当前事实标准。Go SDK(go.opentelemetry.io/otel)通过 TracerProvider、Tracer 和 Span 接口重构抽象,支持自动与手动埋点,并原生兼容 W3C Trace Context(traceparent HTTP header)传播协议。
Go 运行时与追踪的深度协同
Go 1.21+ 引入 runtime/trace 模块增强运行时可观测性,但其聚焦于 Goroutine 调度、GC、网络阻塞等底层事件,与业务链路追踪互补而非替代。开发者可结合使用:
- 业务层:OTel SDK 手动创建 Span 并注入 Context;
- 基础设施层:利用
otelhttp中间件自动捕获 HTTP 请求; - 运行时层:通过
runtime/trace.Start采集调度器快照,辅助定位延迟根因。
典型初始化代码示例
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)
func initTracer() {
// 创建 gRPC 导出器(对接 Jaeger/Tempo 等后端)
exp, _ := otlptracegrpc.New(context.Background())
// 构建 trace provider
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(resource.MustNewSchema(
semconv.ServiceNameKey.String("my-go-service"),
)),
)
otel.SetTracerProvider(tp)
}
该初始化建立全局 TracerProvider,后续所有 otel.Tracer("...").Start() 调用均由此驱动,实现追踪数据的统一采集与导出。
第二章:OpenTelemetry Go SDK深度实践
2.1 OpenTelemetry架构模型与Go SDK核心组件解析
OpenTelemetry 采用可插拔的三层架构:API(契约层)、SDK(实现层) 和 Exporter(传输层)。Go SDK 严格遵循此分层,确保观测能力与业务逻辑解耦。
核心组件职责
otel.Tracer:生成 Span,管理上下文传播otel.Meter:创建 Instruments(Counter、Histogram 等)用于指标采集trace.SpanProcessor:同步/异步处理 Span 生命周期(如BatchSpanProcessor)exporter:将数据序列化并发送至后端(如 OTLP、Jaeger、Prometheus)
数据同步机制
BatchSpanProcessor 默认每 5s 或满 512 个 Span 触发一次导出:
bsp := sdktrace.NewBatchSpanProcessor(
otlpexporter.NewUnstarted(),
sdktrace.WithBatchTimeout(5*time.Second), // 批处理超时
sdktrace.WithMaxExportBatchSize(512), // 单批最大 Span 数
)
该配置平衡延迟与吞吐:
WithBatchTimeout防止低流量下数据滞留;WithMaxExportBatchSize控制内存占用与网络包大小。
| 组件 | 线程安全 | 可热替换 | 典型用途 |
|---|---|---|---|
| Tracer | ✅ | ❌ | 分布式链路追踪 |
| Meter | ✅ | ✅ | 应用性能指标(如 QPS) |
| SpanProcessor | ✅ | ❌ | Span 缓存与导出调度 |
graph TD
A[Instrumentation Code] --> B[OTel API]
B --> C[SDK: Tracer/Meter/Processor]
C --> D[Exporter: OTLP/Zipkin/Jaeger]
D --> E[Collector or Backend]
2.2 手动埋点:Context传递、Span生命周期与属性标注实战
手动埋点是可观测性建设的基石,需精准控制 Span 的创建、激活、结束与上下文流转。
Context 传递机制
OpenTracing/OTel 中 Context 是跨线程/异步边界的载体。关键操作:
Context.current()获取当前活跃上下文Context.root()强制脱离父链路withContext()显式绑定新 Span 到线程局部存储
Span 生命周期管理
Span span = tracer.spanBuilder("db.query")
.setParent(Context.current()) // 继承上游链路
.startSpan();
try (Scope scope = tracer.withSpan(span)) {
// 业务逻辑执行
span.setAttribute("db.statement", "SELECT * FROM users");
} finally {
span.end(); // 必须显式结束,否则内存泄漏
}
spanBuilder().startSpan()创建未激活 Span;withSpan()将其注入 Context 并激活;span.end()标记完成并触发上报。遗漏end()将导致 Span 悬挂、指标失真。
属性标注最佳实践
| 类别 | 推荐 Key | 示例值 | 说明 |
|---|---|---|---|
| 语义标签 | http.method |
"GET" |
标准化 HTTP 属性 |
| 业务标识 | user.id |
"u_8a9f3c1e" |
非敏感可追溯字段 |
| 错误诊断 | error.type |
"ConnectionTimeout" |
便于聚合分析 |
跨线程传播示意
graph TD
A[主线程 Span] -->|Context.capture()| B[子线程任务]
B --> C[新建 Scope]
C --> D[继承 Parent Span]
D --> E[上报至 Collector]
2.3 自动化插件集成:net/http、database/sql、grpc-go等主流库埋点配置
OpenTelemetry 提供标准化的自动插件(Auto-Instrumentation),无需修改业务代码即可为常见库注入可观测性能力。
支持的主流库与启用方式
net/http:通过otelhttp.NewHandler包裹http.Handlerdatabase/sql:使用otelsql.Open替代sql.Opengrpc-go:注册otelgrpc.UnaryServerInterceptor和otelgrpc.UnaryClientInterceptor
配置示例(Go)
import (
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/contrib/instrumentation/database/sql/otelsql"
)
// HTTP 埋点
http.Handle("/api", otelhttp.NewHandler(http.HandlerFunc(handler), "api"))
// SQL 埋点
db, _ := otelsql.Open("mysql", dsn) // 自动捕获 query、exec、rows 等操作
otelhttp.NewHandler将 HTTP 方法、状态码、路径模板(如/users/{id})作为 Span 属性自动注入;otelsql.Open透传context.Context并记录慢查询、错误类型及执行时长。
| 库 | 插件包路径 | 关键能力 |
|---|---|---|
| net/http | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp |
路径归一化、响应大小、延迟 |
| database/sql | go.opentelemetry.io/contrib/instrumentation/database/sql/otelsql |
语句脱敏、驱动识别、错误分类 |
| grpc-go | go.opentelemetry.io/contrib/instrumentation/grpc/otelgrpc |
方法名提取、流式 Span 生命周期 |
graph TD
A[HTTP 请求] --> B[otelhttp.Handler]
B --> C[生成 Span]
C --> D[关联 DB 查询]
D --> E[otelsql.Driver]
E --> F[注入 DB Span]
F --> G[统一导出至后端]
2.4 TraceID与SpanID生成策略、采样器(Sampler)定制与性能权衡
分布式唯一标识生成逻辑
TraceID 通常为 128 位十六进制字符串(如 4bf92f3577b34da6a3ce929d0e0e4736),SpanID 为 64 位,二者均需满足全局唯一、无序性与高吞吐。常见实现基于时间戳+随机数+机器标识的混合方案。
// Snowflake 变体:64bit ID,含毫秒时间戳(41bit)、逻辑节点ID(10bit)、序列号(13bit)
public long nextSpanId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) throw new RuntimeException("Clock moved backwards");
if (timestamp == lastTimestamp) sequence = (sequence + 1) & 0x1FFF; // 13-bit wrap
else sequence = 0;
lastTimestamp = timestamp;
return ((timestamp - EPOCH) << 23) | (nodeId << 13) | sequence;
}
逻辑分析:该 ID 保证单节点每毫秒生成最多 8192 个不重复 SpanID;
EPOCH为自定义纪元时间,nodeId避免集群冲突;高位时间戳保障全局大致有序,利于日志归并。
采样策略权衡矩阵
| 采样器类型 | 采样率可控性 | CPU 开销 | 适用场景 |
|---|---|---|---|
| AlwaysOn | ❌ 固定 100% | 高 | 故障复现、关键链路调试 |
| RateLimiting | ✅ 动态阈值 | 中 | 流量高峰降噪 |
| TraceIDHash | ✅ 哈希后取模 | 低 | 无状态横向扩展 |
自定义采样决策流程
graph TD
A[收到新Span] --> B{是否根Span?}
B -->|是| C[计算TraceID % 100 < sampleRate]
B -->|否| D[继承父Span采样标记]
C --> E[标记 sampled=true]
D --> E
E --> F[上报或丢弃]
2.5 上报管道优化:Exporter选型、批量发送、重试机制与背压控制
Exporter选型对比
| 方案 | 吞吐量 | 内存占用 | 支持背压 | 失败重试 |
|---|---|---|---|---|
| Prometheus Client SDK | 中 | 低 | ❌ | 手动实现 |
| OpenTelemetry OTLP HTTP | 高 | 中 | ✅(gRPC流控) | ✅(内置指数退避) |
| 自研批处理Exporter | 极高 | 可控 | ✅(令牌桶) | ✅(可配置策略) |
批量发送与重试逻辑
def send_batch(metrics: List[Metric], max_retries=3):
for attempt in range(max_retries + 1):
try:
resp = httpx.post(
"http://collector:4318/v1/metrics",
json={"resourceMetrics": pack_batch(metrics)},
timeout=5.0
)
if resp.status_code == 200:
return True
elif resp.status_code in (429, 503): # 服务端限流或过载
time.sleep(2 ** attempt * 0.1) # 指数退避
continue
except httpx.RequestError:
time.sleep(2 ** attempt * 0.1)
return False
该函数采用指数退避重试,max_retries=3对应最大等待约0.8秒;超时设为5秒防止长阻塞;状态码429/503触发背压感知式暂停。
背压控制流程
graph TD
A[采集端生成指标] --> B{缓冲区水位 > 80%?}
B -->|是| C[启用令牌桶限速]
B -->|否| D[直通发送]
C --> E[丢弃低优先级指标 or 阻塞采集]
D --> F[异步批量提交]
第三章:高精度上下文传播与跨服务透传工程实践
3.1 W3C Trace Context规范在Go中的完整实现与兼容性验证
W3C Trace Context(v1.3)定义了 traceparent 与 tracestate 字段的序列化格式与传播语义。Go 生态中,go.opentelemetry.io/otel/propagation 提供了标准实现。
核心传播器使用示例
import "go.opentelemetry.io/otel/propagation"
p := propagation.TraceContext{}
carrier := propagation.MapCarrier{
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"tracestate": "rojo=00f067aa0ba902b7,congo=t61rcm8r",
}
ctx := p.Extract(context.Background(), carrier)
该代码从 HTTP header 等载体中解析 trace context:traceparent 解析出版本、trace ID、span ID、flags;tracestate 按逗号分隔键值对并保留顺序,用于跨厂商上下文传递。
兼容性验证要点
- ✅ 支持大小写不敏感 header 键(如
TraceParent) - ✅ 严格校验 trace ID/span ID 的 32/16 进制长度与格式
- ❌ 拒绝非法 flags(如非
00/01/02/03)
| 验证项 | OpenTelemetry Go | Jaeger Client | Lightstep SDK |
|---|---|---|---|
traceparent 解析 |
✅ | ⚠️(忽略 flags) | ✅ |
tracestate 合并 |
✅ | ❌ | ✅ |
graph TD
A[HTTP Request] --> B[Parse traceparent]
B --> C{Valid format?}
C -->|Yes| D[Extract traceID/spanID/flags]
C -->|No| E[Drop trace context]
D --> F[Inject into span context]
3.2 HTTP/GRPC/gRPC-Web多协议头注入与提取的健壮封装
统一处理跨协议头部需抽象协议差异。核心在于识别传输层语义:HTTP 使用 Header,gRPC 使用 Metadata,gRPC-Web 则通过 X-Grpc-Web 代理头透传。
协议头映射策略
| 协议 | 原生载体 | 透传方式 |
|---|---|---|
| HTTP | http.Header |
原生支持 |
| gRPC | metadata.MD |
:authority 等伪头需显式转换 |
| gRPC-Web | http.Header |
grpc-encoding, grpc-status 等前缀标准化 |
func InjectHeaders(ctx context.Context, md metadata.MD) context.Context {
// 将业务头(如 trace-id)注入 gRPC Metadata,并兼容 gRPC-Web 的 HTTP 头格式
md.Append("x-trace-id", trace.FromContext(ctx).TraceID().String())
md.Append("x-env", "prod")
return metadata.NewOutgoingContext(ctx, md)
}
逻辑分析:InjectHeaders 接收原始上下文与 metadata.MD,追加标准化业务头;x- 前缀确保在 gRPC-Web 反向代理中被正确提升为 HTTP 头,避免被过滤。参数 ctx 携带链路追踪上下文,md 是可变元数据容器。
graph TD
A[请求入口] --> B{协议类型}
B -->|HTTP| C[直接读取 http.Header]
B -->|gRPC| D[解包 metadata.MD]
B -->|gRPC-Web| E[解析 X-Grpc-Web 头+标准 header]
C & D & E --> F[归一化为 HeaderMap]
3.3 异步场景(goroutine池、channel、time.AfterFunc)下的上下文延续方案
在异步执行中,context.Context 的天然生命周期与 goroutine 不一致,需显式传递与绑定。
数据同步机制
使用 context.WithCancel 或 context.WithTimeout 创建子上下文,并通过 channel 同步取消信号:
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()
ch := make(chan string, 1)
go func(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
ch <- "done"
case <-ctx.Done(): // 响应父上下文取消
return
}
}(ctx)
逻辑分析:子 goroutine 显式接收 ctx,在 select 中监听 ctx.Done();若父上下文超时或取消,立即退出,避免泄漏。time.After 不受 context 控制,必须配合 ctx.Done() 才能实现可取消延迟。
goroutine 池中的上下文流转
| 组件 | 是否继承 parentCtx | 关键约束 |
|---|---|---|
| worker goroutine | ✅ 显式传入 | 必须在启动时绑定 ctx |
| channel 操作 | ⚠️ 仅当读写含超时 | 使用 ctx 构建带超时的 channel 操作 |
graph TD
A[main goroutine] -->|WithTimeout| B[worker pool ctx]
B --> C[worker1: select{ctx.Done, ch}]
B --> D[worker2: select{ctx.Done, ch}]
C --> E[响应取消并清理]
D --> E
第四章:分布式追踪数据可观测性增强体系构建
4.1 结构化日志与Trace Span双向关联(Log-to-Trace)实现
核心机制:上下文透传与字段对齐
Log-to-Trace 要求日志中嵌入 trace_id、span_id 和 trace_flags,并与 OpenTelemetry SDK 生成的 Span 元数据严格一致。
数据同步机制
日志框架(如 Zap/Logrus)需通过 With 或 AddCallerSkip 注入 trace 上下文:
// 基于 context.Context 提取 span 并注入结构化字段
func LogWithContext(ctx context.Context, logger *zap.Logger) *zap.Logger {
span := trace.SpanFromContext(ctx)
sc := span.SpanContext()
return logger.With(
zap.String("trace_id", sc.TraceID().String()),
zap.String("span_id", sc.SpanID().String()),
zap.Bool("trace_sampled", sc.IsSampled()),
)
}
逻辑分析:
trace.SpanFromContext安全提取当前活跃 Span;sc.TraceID().String()返回 32 位十六进制字符串(如432a78b5c9f01234567890abcdef1234),确保与后端 Jaeger/OTLP 服务解析兼容;IsSampled()决定日志是否参与采样分析。
关联验证字段对照表
| 日志字段 | Span 字段 | 类型 | 用途 |
|---|---|---|---|
trace_id |
SpanContext.TraceID |
string | 全局唯一追踪标识 |
span_id |
SpanContext.SpanID |
string | 当前 Span 局部唯一标识 |
trace_flags |
SpanContext.TraceFlags |
uint8 | 指示采样状态(如 0x01) |
graph TD
A[应用写日志] --> B{Log Middleware}
B --> C[从 context 提取 SpanContext]
C --> D[注入 trace_id/span_id]
D --> E[输出 JSON 结构化日志]
E --> F[日志采集器按 trace_id 聚合]
4.2 自定义Span指标(Metrics)暴露与Prometheus集成实践
在 OpenTelemetry SDK 中,可通过 Meter 创建自定义指标,精准捕获 Span 生命周期关键信号。
暴露 HTTP 请求延迟直方图
from opentelemetry.metrics import get_meter
from opentelemetry.exporter.prometheus import PrometheusMetricReader
meter = get_meter("io.example.http")
http_duration = meter.create_histogram(
"http.server.duration",
unit="s",
description="HTTP request duration"
)
# 在 Span 结束时记录
http_duration.record(0.125, {"http.method": "GET", "http.status_code": "200"})
该直方图自动按 le 标签分桶,适配 Prometheus 原生 histogram 类型;record() 的第二个参数为属性标签,决定指标时间序列维度。
Prometheus 集成关键配置
| 组件 | 作用 | 推荐值 |
|---|---|---|
PrometheusMetricReader |
拉取式指标收集器 | 启用 enable_target_info=True |
/metrics 端点 |
HTTP 暴露路径 | 默认绑定 0.0.0.0:9464 |
数据同步机制
graph TD
A[OTel SDK] --> B[Meter.record]
B --> C[Aggregation Store]
C --> D[PrometheusMetricReader]
D --> E[/metrics HTTP Handler]
E --> F[Prometheus Scrapes]
4.3 错误分类、异常堆栈捕获与Error Span语义化标注
错误的三层语义分类
- 业务错误(如
InsufficientBalanceError):可预期、无需告警,应携带上下文ID; - 系统错误(如
DatabaseTimeoutException):需触发熔断与告警; - 协议错误(如
400 Bad Request):由网关拦截,不进入业务Span。
异常堆栈标准化捕获
try:
process_payment()
except Exception as e:
# 自动注入trace_id、span_id、error_type
span.record_exception(e, attributes={
"error.category": "payment", # 业务域标识
"error.severity": "critical", # 可选: info/warn/error/critical
"error.stack_hash": hash_stack(e) # 去重聚合关键指标
})
该逻辑确保所有异常在OpenTelemetry SDK中统一注入结构化属性,
stack_hash基于前5帧类名+方法名生成,避免海量重复堆栈刷屏。
Error Span语义化标注规范
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
error.type |
string | ✅ | 全限定类名(如 io.opentelemetry.api.trace.StatusCode.ERROR) |
error.message |
string | ✅ | 精简可读提示(不含敏感数据) |
error.stack_trace |
string | ❌ | 仅在采样率 > 0.1% 的Span中填充 |
graph TD
A[抛出Exception] --> B{是否已捕获?}
B -->|否| C[全局UncaughtExceptionHandler]
B -->|是| D[手动record_exception]
C & D --> E[注入error.*属性]
E --> F[标记Span为error=true]
4.4 分布式上下文调试工具链:本地开发代理、Trace ID注入测试与调试中间件
在微服务联调中,跨进程的请求链路追踪常因上下文丢失而失效。本地开发代理(如 dev-proxy)可自动为出站请求注入 X-B3-TraceId 和 X-B3-SpanId,无需修改业务代码。
调试中间件实现示例(Express.js)
// trace-inject-middleware.js
function traceInjectMiddleware(req, res, next) {
const traceId = req.headers['x-b3-traceid'] || generateTraceId();
const spanId = generateSpanId();
// 注入标准化 OpenTracing 头部
res.setHeader('X-B3-TraceId', traceId);
res.setHeader('X-B3-SpanId', spanId);
res.locals.traceContext = { traceId, spanId };
next();
}
逻辑分析:中间件优先复用上游传入的 X-B3-TraceId 实现链路延续;若无则生成新 Trace ID(16位十六进制字符串),确保本地调试链路可追溯。res.locals 供后续日志/监控模块消费。
本地代理关键能力对比
| 能力 | dev-proxy | curl -H 手动注入 | IDE 插件 |
|---|---|---|---|
| 自动继承上游 TraceID | ✅ | ❌ | ⚠️(部分支持) |
| 多协议支持(HTTP/gRPC) | ✅ | ❌(需额外封装) | ❌ |
graph TD
A[前端请求] --> B[本地开发代理]
B --> C{是否含X-B3-TraceId?}
C -->|是| D[透传并生成新SpanID]
C -->|否| E[生成全新TraceID+SpanID]
D & E --> F[转发至后端服务]
第五章:从单体到云原生——Go微服务追踪治理全景总结
追踪数据采集的Go实践痛点
在某电商中台迁移项目中,团队采用 OpenTelemetry Go SDK 替换旧版 Jaeger 客户端,发现 http.RoundTripper 装饰器在高并发下引发 goroutine 泄漏。根本原因是未正确复用 otelhttp.NewTransport 实例,导致每次请求新建 tracer。修复后 P99 延迟下降 37%,日均采集 span 数量稳定在 2.4 亿条。
链路上下文跨协程传递规范
Go 的 context.Context 是传播 traceID 的唯一可靠载体。但在实际业务代码中,常出现 go func() { /* 忘记传入 ctx */ }() 导致子 span 断链。我们通过静态扫描工具 go-critic 配置自定义规则,强制检测所有 go 关键字后是否携带 ctx 参数,并集成至 CI 流水线,拦截率 92.6%。
采样策略的动态分级配置
| 场景 | 采样率 | 触发条件 | 存储位置 |
|---|---|---|---|
| 支付核心链路 | 100% | service.name == “payment” | Elasticsearch |
| 商品搜索(非错误) | 1% | http.status_code == 200 | Loki |
| 全链路异常 | 100% | exception.type != “” | ClickHouse |
该策略使后端存储成本降低 68%,同时保障关键路径可观测性不降级。
服务网格与 SDK 协同埋点
在 Istio 环境中,Sidecar 已自动注入 HTTP header,但 Go 微服务仍需主动解析 traceparent 并创建 SpanContext。我们封装了 otelutil.ExtractFromHeaders 工具函数,兼容 W3C Trace Context 和 B3 格式,在 17 个服务中统一落地,避免因 header 解析不一致导致链路断裂。
func NewTracedHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := otelutil.ExtractFromHeaders(r.Header, r.Context())
span := trace.SpanFromContext(ctx)
// 自动注入 span ID 到响应头,供前端埋点使用
w.Header().Set("X-Trace-ID", span.SpanContext().TraceID().String())
h.ServeHTTP(w, r.WithContext(ctx))
})
}
指标驱动的根因定位流程
当订单履约延迟突增时,运维人员不再逐个查看服务日志,而是执行如下 Mermaid 查询逻辑:
flowchart TD
A[Prometheus 报警:payment-service p95 > 2s] --> B{Trace 分析}
B --> C[筛选 error=true 的 span]
C --> D[按 service.name 分组统计失败率]
D --> E[定位到 inventory-service 返回 503]
E --> F[检查其下游 redis 连接池耗尽指标]
该流程将平均故障定位时间从 22 分钟压缩至 3 分 40 秒。
多语言服务间 traceID 对齐
在混合技术栈中(Go + Python + Node.js),曾因 Python 服务未启用 tracestate 头导致跨语言链路丢失。我们推动全栈统一使用 OpenTelemetry 1.20+ 版本,并在 Go 侧强制开启 otel.Propagators = propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}),实现跨语言 trace 上下文完整透传。
