vault backup: 2026-05-17 21:40:31
This commit is contained in:
+375
-32
@@ -1,6 +1,6 @@
|
||||
---
|
||||
tags: [microservice, distributed-tracing, opentelemetry, jaeger, skywalking]
|
||||
create time: 2026-05-05
|
||||
create time: 2026-05-05 14:30
|
||||
---
|
||||
|
||||
# 分布式链路追踪
|
||||
@@ -16,11 +16,11 @@ create time: 2026-05-05
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Req["请求 trace_id=abc123"] --> Span1["Span #1<br/>API Gateway<br/>5ms"]
|
||||
Span1 --> Span2["Span #2<br/>Order Service<br/>70ms"]
|
||||
Span1 --> Span3["Span #3<br/>User Service<br/>15ms"]
|
||||
Span2 --> Span4["Span #4<br/>Inventory DB Query<br/>60ms"]
|
||||
|
||||
Req["请求 trace_id=abc123"] --> Span1["Span #1 API Gateway 5ms"]
|
||||
Span1 --> Span2["Span #2 Order Service 70ms"]
|
||||
Span1 --> Span3["Span #3 User Service 15ms"]
|
||||
Span2 --> Span4["Span #4 Inventory DB Query 60ms"]
|
||||
|
||||
style Span2 fill:#ff9999
|
||||
style Span4 fill:#ffcc99
|
||||
```
|
||||
@@ -75,62 +75,315 @@ flowchart TB
|
||||
|
||||
## 上下文传播 (Context Propagation)
|
||||
|
||||
`trace_id` 如何从上游服务传递到下游?
|
||||
`trace_id` 如何从上游服务传递到下游?核心原则:**调用者创建上下文 → 被调者提取上下文 → 在自身链路中继续使用并透传到更下游**。
|
||||
|
||||
### HTTP 场景:W3C Trace Context 标准
|
||||
### W3C Trace Context 标准
|
||||
|
||||
这是业界事实标准([W3C Recommendation](https://www.w3.org/TR/trace-context/)),被 OpenTelemetry、Jaeger、SkyWalking 全部支持。
|
||||
|
||||
```json
|
||||
// HTTP Header 中实际传递的字段
|
||||
{
|
||||
"traceparent": "00-abc123def456...-789ghi012jkl-01",
|
||||
"tracestate": "congo=t61rcWkgMzE"
|
||||
"tracestate": "congo=t61rcWkgMzE,vendor=value"
|
||||
}
|
||||
```
|
||||
|
||||
格式:`version-trace_id-span_id-flags`
|
||||
| 字段 | 格式 | 说明 |
|
||||
|------|------|------|
|
||||
| `version` | 2 字符十六进制 | 版本号,目前固定 `00` |
|
||||
| `trace_id` | 32 字符十六进制 | 128-bit 全局唯一标识,建议用 ULID / Snowflake 生成 |
|
||||
| `span_id` | 16 字符十六进制 | 64-bit 本段 span 的唯一标识 |
|
||||
| `flags` | 2 字符十六进制 | 目前仅 `01` 表示已采样 |
|
||||
|
||||
**`tracestate`**:供厂商扩展使用,例如用于在多家 tracing 系统间同步采样决策或路由信息。多个厂商以逗号分隔。
|
||||
|
||||
### HTTP 场景:中间件自动注入与提取
|
||||
|
||||
```go
|
||||
// OpenTelemetry SDK 自动处理 context 注入和提取
|
||||
func Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 从入站请求提取 trace context
|
||||
// ① 从入站请求提取 trace context(如果请求中没有 traceparent,会生成新的 trace)
|
||||
ctx := propagator.Extract(r.Context(), headerReader{r.Header})
|
||||
|
||||
// 创建新的 span
|
||||
|
||||
// ② 在当前服务内创建新 span
|
||||
ctx, span := tracer.Start(ctx, "handleOrder")
|
||||
defer span.End()
|
||||
|
||||
// 将 trace context 注入到出站请求
|
||||
|
||||
// ③ 将 trace context 注入到出站请求 —— 关键一步
|
||||
r = r.WithContext(ctx)
|
||||
propagator.Inject(ctx, headerWriter{r.Header})
|
||||
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### MQ 场景:Message Header 透传
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant A as Service A
|
||||
participant B as Service B
|
||||
|
||||
C->>A: GET /api/order\ntraceparent=00-abc...
|
||||
Note over A: Extract create Span A
|
||||
A->>B: POST /inventory\nInject traceparent
|
||||
Note over B: Extract create Span B
|
||||
B-->>A: Response
|
||||
A-->>C: Response
|
||||
```
|
||||
|
||||
### gRPC 场景:Metadata 透传
|
||||
|
||||
gRPC 基于 HTTP/2,传播方式类似,但使用的是 **gRPC Metadata** 而非 HTTP Headers(gRPC 库内部会将 metadata 转为 HTTP Header)。
|
||||
|
||||
```go
|
||||
// RocketMQ 生产者 - 自动注入 trace context
|
||||
msg := rocketmq.NewMessage("order-created", payload)
|
||||
msg.WithProperty("trace_id", currentTraceID) // 手动注入
|
||||
msg.WithProperty("span_id", currentSpanID)
|
||||
// === 调用方:自动从 context 提取 trace 信息写入 metadata ===
|
||||
ctx, span := tracer.Start(parentCtx, "callUserService")
|
||||
defer span.End()
|
||||
|
||||
// RocketMQ 消费者 - 恢复 trace context
|
||||
traceID := msg.GetProperty("trace_id")
|
||||
span, _ := tracer.Start(ctx, "processOrderCreated",
|
||||
trace.WithSpanKind(trace.SpanKindConsumer))
|
||||
// otelgrpc.Interceptor 会自动完成 Inject,无需手动处理
|
||||
conn, err := grpc.DialContext(ctx, "user-service:9090",
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()), // 自动注入
|
||||
)
|
||||
client := userpb.NewUserServiceClient(conn)
|
||||
resp, err := client.GetUser(ctx, &userpb.GetRequest{Id: "123"})
|
||||
|
||||
// === 被调方:拦截器自动完成 Extract ===
|
||||
server := pb.NewUserServiceServerImpl()
|
||||
grpc.NewServer(
|
||||
grpc.StatsHandler(otelgrpc.NewServerHandler()), // 自动提取
|
||||
).Serve(listener)
|
||||
```
|
||||
|
||||
> [!note] 为什么不直接用 HTTP Headers?
|
||||
> gRPC 对 metadata key 有严格的大小写规范——全部小写且不支持连字符。因此 `traceparent` 这样的 Header 名无法直接映射到 gRPC Metadata,各 tracing SDK 内部做了适配层,开发者只需关注 `context`,不需要手动操作 metadata。
|
||||
|
||||
### MQ 场景:消息属性透传
|
||||
|
||||
消息队列没有标准化的上下文传播协议,需要**在消息属性中手动携带 trace 信息**。OpenTelemetry 提供了标准的 attribute 命名约定。
|
||||
|
||||
```go
|
||||
// === RocketMQ 生产者:注入 trace context ===
|
||||
msg := rocketmq.NewMessage("order-created", payload)
|
||||
|
||||
// 标准 OTel 属性名
|
||||
msg.WithProperty(semconv.RPCSystemKey, "rocketmq")
|
||||
msg.WithProperty(semconv.MessagingSystemKey, "rocketmq")
|
||||
msg.WithProperty(semconv.HTTPFlavorKey, "1.1")
|
||||
|
||||
// 手动注入 trace_id 和 span_id
|
||||
if sc, ok := trace.SpanFromContext(ctx).SpanContext(); ok && sc.IsValid() {
|
||||
msg.WithProperty("traceparent", fmt.Sprintf(
|
||||
"00-%s-%s-01", sc.TraceID().String(), sc.SpanID().String(),
|
||||
))
|
||||
}
|
||||
|
||||
producer.SendSync(msg)
|
||||
|
||||
// === RocketMQ 消费者:恢复 trace context ===
|
||||
consumer.Receive(func(ctx context.Context, messages ...*rocketmq.Message) error {
|
||||
for _, msg := range messages {
|
||||
// 从消息属性中提取 traceparent
|
||||
tp := msg.GetProperty("traceparent")
|
||||
if tp != "" {
|
||||
ctx = propagator.Extract(ctx, textmap.Reader(func(k string) string {
|
||||
return tp // 模拟 header reader
|
||||
}))
|
||||
} else {
|
||||
// 补偿策略:如果上游没带 trace 信息,作为新 trace 起点
|
||||
ctx = trace.ContextWithRemoteSpanContext(ctx,
|
||||
trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: generateFallbackTraceID(),
|
||||
Remote: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
_, span := tracer.Start(ctx, "processOrderCreated",
|
||||
trace.WithSpanKind(trace.SpanKindConsumer))
|
||||
span.SetAttributes(
|
||||
semconv.MessagingSystemKey, "rocketmq",
|
||||
semconv.MessagingDestinationKey, msg.Topic,
|
||||
semconv.MessagingMessageIDKey, msg.MsgID,
|
||||
)
|
||||
|
||||
doProcess(ctx, msg.Body)
|
||||
span.End()
|
||||
}
|
||||
return nil
|
||||
}, rocketmq.ConsumeFunc())
|
||||
```
|
||||
|
||||
> [!tip] 补偿策略:异步解耦时可能出现"上游未传 trace"的情况
|
||||
>
|
||||
> 当 MQ 消息来自非追踪系统或上游漏传了 trace_id 时,消费者应作为 **新 Trace 的根 Span** 启动,而不是丢弃消息。同时通过属性标记 `messaging.message.is_remote=true` 标明这是一个远程关联点。
|
||||
|
||||
### 同步并发:Goroutine 间的 Context 传播
|
||||
|
||||
Go 的 `context.Context` 是协程安全的,子 Goroutine 直接使用父级 context 即可自动继承 trace。关键在于**不能丢失 context 引用**。
|
||||
|
||||
```go
|
||||
func handleOrder(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
_, span := tracer.Start(ctx, "handleOrder")
|
||||
defer span.End()
|
||||
|
||||
// ✅ 正确:直接传入同一 context,子 goroutine 自动继承 trace
|
||||
var wg sync.WaitGroup
|
||||
for _, item := range order.Items {
|
||||
wg.Add(1)
|
||||
go func(item OrderItem) {
|
||||
defer wg.Done()
|
||||
processItem(ctx, item) // ctx 携带 trace context
|
||||
}(item)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// ❌ 错误:创建了无父 context 的新 context,trace 断裂
|
||||
// go func() {
|
||||
// _, childSpan := tracer.Start(context.Background(), "processItem")
|
||||
// ...
|
||||
// }()
|
||||
}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Main["主 Goroutine"]
|
||||
S0["Span #1 handleOrder"]
|
||||
S0 --> S1["Span #2 proc Item A"]
|
||||
S0 --> S2["Span #3 proc Item B"]
|
||||
S0 --> S3["Span #4 proc Item C"]
|
||||
end
|
||||
|
||||
style S0 fill:#bbddff
|
||||
style S1 fill:#ccf2ff
|
||||
style S2 fill:#ccf2ff
|
||||
style S3 fill:#ccf2ff
|
||||
```
|
||||
|
||||
> [!warning] 常见陷阱
|
||||
>
|
||||
> 1. **忘记传入 context**:用 `context.Background()` 或 `context.TODO()` 启动了 Goroutine,导致 trace 断裂。
|
||||
> 2. **超时覆盖**:子 Goroutine 中设置了独立的 `context.WithTimeout` 但没有保留父级的 deadline,导致整体超时无效。
|
||||
> 3. **recover 后丢失 context**:`defer recover()` 中如果有错误上报逻辑,仍需持有原始 context。
|
||||
|
||||
### 额外负载:Baggage 机制
|
||||
|
||||
除了 trace/span 信息,有时需要在链路间传递**业务标签**(如用户 ID、租户 ID、A/B 测试分组),这通过 **Baggage** 实现。
|
||||
|
||||
```go
|
||||
// === 入口服务:设置 baggage ===
|
||||
baggage, _ := baggage.New(baggage.Pair("user.id", "u-12345"),
|
||||
baggage.Pair("tenant", "acme-corp"))
|
||||
ctx = propagator.Inject(ctx, baggageInjector{baggage})
|
||||
|
||||
// === 中间任意服务:读取 baggage ===
|
||||
baggage = baggage.FromContext(ctx)
|
||||
userID, _ := baggage.Value("user.id")
|
||||
// userID == "u-12345"
|
||||
|
||||
// === 下游服务:也能读到同样的 baggage ===
|
||||
```
|
||||
|
||||
| 特性 | Baggage | Span Attributes |
|
||||
|------|---------|----------------|
|
||||
| 传播范围 | 沿整条链路传递给所有下游 | 仅记录在本地 Span 中,不传播 |
|
||||
| 大小限制 | 总计 512 字节(防止 Header 膨胀) | 无限制 |
|
||||
| 典型用途 | 用户 ID、租户、环境标记 | 状态码、耗时、SQL 语句等遥测数据 |
|
||||
| 是否计入 Span Duration | 否 | 否 |
|
||||
|
||||
> [!caution] Baggage 安全须知
|
||||
>
|
||||
> Baggage 会被编码为 HTTP Header 随每个请求传播,**绝不能包含敏感信息**(密码、Token、PII)。默认 512 字节上限也意味着不适合传递大量数据。如需传递大对象,请改为通过 `span.RecordError()` 或独立日志存储。
|
||||
|
||||
### 边界的判断:何时创建新 Trace
|
||||
|
||||
并非所有场景都需要延续上游 trace。以下情况应考虑开启新 trace:
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title 跨系统调用:是否延续 Trace?
|
||||
x-axis Low Coupling --> High Coupling
|
||||
y-axis New Trace --> Continue Trace
|
||||
"健康检查 / Scrape": [0.2, 0.15]
|
||||
"定时任务 / Cron": [0.15, 0.1]
|
||||
"MQ 消息异步解耦": [0.4, 0.7]
|
||||
"正常 HTTP/gRPC 请求": [0.85, 0.9]
|
||||
"第三方回调": [0.5, 0.25]
|
||||
"消息积压重新消费": [0.3, 0.3]
|
||||
```
|
||||
|
||||
| 场景 | 行为 | 说明 |
|
||||
|------|------|------|
|
||||
| 正常 HTTP / gRPC / MQ 请求 | 延续上游 trace_id | 标准流程,Extract → Continue |
|
||||
| 健康检查 / Prometheus scrape | 新建 trace | 可标记 `tracestate` 为 healthcheck |
|
||||
| 定时任务 / Cron job | 新建 trace | 无上游 context,天然无父 span |
|
||||
| 消息积压重新消费(跨越数天) | 新 trace + baggage 引用原 trace_id | 保留追溯关联 |
|
||||
| 第三方回调(外部系统主动推送) | 新建 trace,通过 callback_id 间接关联 | 无法 Extract,只能新建 |
|
||||
| 伪造 traceparent | 验证格式合法性,非法则忽略并新建 | 记录告警,防止注入攻击 |
|
||||
|
||||
**决策流程:**
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Incoming["Incoming Request"] --> HasTrace{"是否有有效 traceparent"}
|
||||
HasTrace -->|"是"| Extract["Extract 并继续"]
|
||||
HasTrace -->|"否"| NewTrace["创建新 trace"]
|
||||
|
||||
Extract --> Valid{"格式合法"}
|
||||
Valid -->|"是"| Continue["延续 trace"]
|
||||
Valid -->|"否"| LogWarn["记录告警丢弃伪造 context"]
|
||||
LogWarn --> NewTrace
|
||||
|
||||
NewTrace --> MarkBaggage["可选 baggage 携带原 trace_id 作引用"]
|
||||
MarkBaggage --> StartRoot["启动根 Span"]
|
||||
```
|
||||
|
||||
## 主流方案对比
|
||||
|
||||
### 方案全景图
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title 三大追踪方案特性对比
|
||||
x-axis Low Invasiveness --> High Flexibility
|
||||
y-axis All-in-one APM --> Modular Components
|
||||
"SkyWalking": [0.25, 0.75]
|
||||
"OpenTelemetry": [0.75, 0.85]
|
||||
"Jaeger": [0.6, 0.35]
|
||||
```
|
||||
|
||||
| 方案 | 协议 | 存储后端 | 侵入程度 | 特色能力 |
|
||||
|------|------|---------|---------|---------|
|
||||
| **OpenTelemetry** | OTLP | Prometheus/Jaeger/Zipkin | SDK + Auto-Instrumentation | CNCF 标准,厂商中立,未来趋势 |
|
||||
| **Jaeger** | Jaeger native | Cassandra/Elasticsearch | Agent / SDK | Uber 开源,UI 友好,支持业务 Tags |
|
||||
| **SkyWalking** | SkyWalking | MySQL/Elasticsearch/ES | 零侵入 Java Agent | 国产,中文文档完善,APM 一体 |
|
||||
|
||||
### 演进路线:从单体到 OTel
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Phase1["Phase 1 无 Tracing"]
|
||||
P1["各服务独立日志\n排查靠翻日志和SSH"]
|
||||
end
|
||||
|
||||
subgraph Phase2["Phase 2 Jaeger/SkyWalking 自研"]
|
||||
P2["特定语言适配\n链路覆盖不全"]
|
||||
end
|
||||
|
||||
subgraph Phase3["Phase 3 OpenTelemetry 统一"]
|
||||
P3["SDK 统一\nAuto-Instrumentation\nCollector 收集"]
|
||||
end
|
||||
|
||||
P1 ==> P2 ==> P3
|
||||
|
||||
note["推荐目标 OTel Collector 灵活后端\n不锁死任何组件按需替换"]
|
||||
note -.-> P3
|
||||
```
|
||||
|
||||
> [!tip] 选型建议
|
||||
> 新项目优先选 **OpenTelemetry**——它是行业标准,未来会被所有工具支持。如果团队需要开箱即用的 APM,**SkyWalking** 的 Java Agent 零侵入方案是快速上手的最佳选择。
|
||||
|
||||
@@ -170,16 +423,79 @@ func handleOrder(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
## 采样策略
|
||||
|
||||
> [!question] 为什么需要采样?
|
||||
> 假设日活 100 万用户,每个用户产生 10 次请求 = 每天 1 千万条 Trace。如果全部存储,按每条 Span 平均 500 bytes 算,单张表一天就是 **数 GB**。采样不是偷懒,是**用可控的成本换取可观测性**。
|
||||
|
||||
### 采样算法
|
||||
|
||||
OpenTelemetry 提供了两种内置采样器:
|
||||
|
||||
```go
|
||||
// 静态采样:简单粗暴,适合开发环境
|
||||
alwaysOn := sdktrace.AlwaysSample() // 100% 采集
|
||||
alwaysOff := sdktrace.NeverSample() // 0% 采集(但仍在 SDK 内创建 span)
|
||||
|
||||
// 动态采样:根据链路状态智能决策 ⭐ 推荐生产使用
|
||||
dynamic := sampler.TraceIDRatioBased(0.1) // 10% 随机采样
|
||||
```
|
||||
|
||||
实际项目中通常组合使用 **Parent-Based 分层采样**,确保父子链路一致性:
|
||||
|
||||
```go
|
||||
provider := sdktrace.NewTracerProvider(
|
||||
// Parent-based: 父级已采样则子级必采样,反之亦然
|
||||
sdktrace.WithSampler(
|
||||
sdktrace.ParentBased(
|
||||
sdktrace.TraceIDRatioBased(0.1), // 无父 trace 时默认 10%
|
||||
),
|
||||
),
|
||||
sdktrace.WithBatcherExporter(exporter),
|
||||
)
|
||||
```
|
||||
|
||||
### 错误优先采样
|
||||
|
||||
```go
|
||||
type errorPrioritySampler struct {
|
||||
baseRate float64 // 基础采样率
|
||||
}
|
||||
|
||||
func (s *errorPrioritySampler) ShouldSample(params sampling.Parameters) sampling.Result {
|
||||
ctx := params.SpanContext.Context
|
||||
// 检查当前 span 是否已有 error status
|
||||
if sc, ok := trace.SpanFromContext(ctx).SpanContext(); ok {
|
||||
attrs := sc.Attributes()
|
||||
for _, attr := range attrs {
|
||||
if attr.Key == "error" && attr.Value.AsBool() {
|
||||
return sampling.Result{
|
||||
Decision: sampling.RecordAndSample, // 100% 采样错误请求
|
||||
Attributes: []attribute.KeyValue{},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 非错误请求走基础采样率
|
||||
rate := rand.Float64()
|
||||
decision := sampling.Drop
|
||||
if rate < s.baseRate {
|
||||
decision = sampling.RecordAndSample
|
||||
}
|
||||
return sampling.Result{Decision: decision}
|
||||
}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
AllReq["全部请求"] --> Sampler{"采样决策"}
|
||||
Sampler -->|"100%"| Core["核心链路全量采集"]
|
||||
Sampler -->|"5~10%"| Normal["普通路径随机采样"]
|
||||
Sampler -->|"0%"| Health["健康检查/内部心跳"]
|
||||
|
||||
Sampler -->|"有error"| CheckError{"错误标记"}
|
||||
CheckError -->|"是"| Core["核心链路全量采集"]
|
||||
CheckError -->|"否"| Prob["概率采样"]
|
||||
Sampler -->|"无parent"| Prob
|
||||
Prob -->|"命中"| Core
|
||||
Prob -->|"未命中"| Drop["丢弃"]
|
||||
|
||||
Core --> Storage["Trace 存储"]
|
||||
Normal --> Storage
|
||||
Health --> Drop["丢弃"]
|
||||
Drop --> Log["非采样请求仅记录计数"]
|
||||
```
|
||||
|
||||
| 环境 | 采样率 | 理由 |
|
||||
@@ -190,9 +506,36 @@ flowchart LR
|
||||
| **生产 - 错误链路** | 100% | 出错时的请求优先保留 |
|
||||
|
||||
> [!note] 基于错误的智能采样
|
||||
>
|
||||
>
|
||||
> 高级做法:**正常路径低采样,一旦检测到错误立即提升当前请求的采样率**。这样既省了存储,又能在出问题时有足够的数据回溯。
|
||||
|
||||
### 采样对应用的影响
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Sampled["被采样的请求比例十百分比"]
|
||||
S1["完整 Span 数据"]
|
||||
S2["完整日志关联"]
|
||||
S3["存储到后端"]
|
||||
end
|
||||
|
||||
subgraph Dropped["未被采样的请求比例九十百分比"]
|
||||
D1["SDK 内仍创建 Span"]
|
||||
D2["不影响业务逻辑"]
|
||||
D3["上报时直接跳过 Export"]
|
||||
end
|
||||
|
||||
Note["采样不等于不执行\n采样只影响上报不影响业务"]
|
||||
Note --> Sampled
|
||||
Note --> Dropped
|
||||
```
|
||||
|
||||
> [!warning] 常见误区
|
||||
>
|
||||
> 1. **"采样后 span 就不创建了"** → 错!SDK 内部仍然创建和记录 span,只是在 `Exporter` 阶段跳过网络上报。
|
||||
> 2. **"采样会导致链路断裂"** → 配合 `ParentBased` 可以保证:只要链路上有一条被采样,整条链路的 span 都会被保留。
|
||||
> 3. **"统计 P99 会有偏差"** → 正确。低采样率下 P99 估计值会偏低,需用统计学方法做补偿估计。
|
||||
|
||||
## 渐进式落地路线
|
||||
|
||||
> [!tip] 不要试图一开始就采集全部 Span
|
||||
|
||||
Reference in New Issue
Block a user