第一章:Go后台SRE实践的核心理念与演进路径
SRE(Site Reliability Engineering)在Go语言后台系统中并非简单套用Google方法论,而是深度融合Go的并发模型、轻量级运行时与云原生生态所形成的实践范式。其核心理念始终围绕“可靠性即代码”——将SLI/SLO定义、错误预算消耗、自动化修复等关键能力,以Go原生方式嵌入服务生命周期,而非依赖外部平台兜底。
可观测性驱动的故障收敛
Go服务天然适合构建高保真可观测链路:通过expvar暴露运行时指标,结合OpenTelemetry SDK统一采集trace、metrics、logs。例如,在HTTP handler中注入自动追踪:
import "go.opentelemetry.io/otel/trace"
func apiHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
span := trace.SpanFromContext(ctx) // 从上下文提取当前span
span.AddEvent("start_processing") // 记录事件
defer span.End() // 确保span结束
// 业务逻辑...
}
该模式使延迟毛刺、goroutine泄漏等问题可被精确下钻至函数级,避免传统日志grep的模糊定位。
自愈机制的Go化落地
SRE强调“故障自愈优先于告警”,Go的sync.Once、time.Ticker与context.WithTimeout构成轻量级自愈基座。典型场景如数据库连接池健康检查:
var healthCheckOnce sync.Once
func initDBHealthCheck(db *sql.DB) {
healthCheckOnce.Do(func() {
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
if err := db.Ping(); err != nil {
log.Warn("DB ping failed, triggering reconnect")
// 触发连接池重建逻辑
rebuildConnectionPool(db)
}
}
}()
})
}
SLO演进的渐进式实践路径
| 阶段 | 关键动作 | Go技术支撑 |
|---|---|---|
| 基线期 | 定义P99延迟、错误率SLI | prometheus.ClientGolang埋点 |
| 控制期 | 错误预算仪表盘+自动降级开关 | gofeatureflag + gin.HandlerFunc中间件 |
| 优化期 | 基于pprof火焰图的CPU/内存热点治理 | net/http/pprof + go tool pprof |
演进本质是让SRE能力从“人肉巡检”走向“代码契约”——每个SLO都对应一段可测试、可版本化的Go验证逻辑。
第二章:SLI/SLO定义的理论基础与Go实现范式
2.1 SLI可观测性建模:从HTTP延迟到Go runtime指标的映射逻辑
SLI建模需穿透应用层直达运行时语义。HTTP P95延迟(SLI)不能孤立看待,而应与runtime.NumGoroutine()、runtime.ReadMemStats().GCNext等指标建立因果链。
数据同步机制
Go HTTP server每秒采样延迟直方图,并触发runtime.ReadMemStats()同步读取内存状态:
// 每次HTTP请求结束时采集
func recordLatencyAndRuntime(w http.ResponseWriter, r *http.Request) {
start := time.Now()
defer func() {
latency := time.Since(start)
// 映射:延迟突增 → goroutine堆积 → GC压力上升
metrics.HTTPLatency.Observe(latency.Seconds())
if latency > 200*time.Millisecond {
var ms runtime.MemStats
runtime.ReadMemStats(&ms) // 关键同步点
metrics.Goroutines.Set(float64(runtime.NumGoroutine()))
metrics.NextGC.Set(float64(ms.NextGC))
}
}()
}
该逻辑确保延迟异常时自动捕获运行时快照,避免异步采样导致的时序错位。
映射关系表
| HTTP SLI维度 | 对应Go Runtime指标 | 业务含义 |
|---|---|---|
| P95延迟 > 200ms | NumGoroutine() > 500 |
协程泄漏风险 |
| P99延迟毛刺 | MemStats.PauseTotalNs 突增 |
GC STW干扰 |
graph TD
A[HTTP Request] --> B{latency > 200ms?}
B -->|Yes| C[ReadMemStats]
B -->|No| D[仅上报延迟]
C --> E[Extract Goroutines/GCNext]
E --> F[关联告警标签]
2.2 SLO边界设计:基于Go服务拓扑的分层承诺(API层/业务层/基础设施层)
SLO边界需与服务调用链深度对齐,Go微服务天然具备清晰的分层拓扑:HTTP handler → 业务逻辑包 → 数据/中间件客户端。
分层承诺语义
- API层:
99.5%请求 P95 - 业务层:
99.9%核心流程 P95 - 基础设施层:
99.99%Redis连接成功率,P99 RTT
Go服务拓扑映射示例
// service/http/handler.go
func OrderCreateHandler(w http.ResponseWriter, r *http.Request) {
// API层SLO入口:计时+标签注入
ctx := slos.WithLayer(ctx, "api") // 注入SLO上下文层级标识
defer slos.RecordLatency(ctx, "order_create") // 自动上报至对应SLO桶
// ...
}
该代码通过WithLayer显式标记调用层级,RecordLatency依据上下文自动路由指标到对应SLO监控视图,避免手动分桶错误。
各层SLO指标归属表
| 层级 | 关键指标 | 数据来源 | 责任团队 |
|---|---|---|---|
| API层 | HTTP 5xx率、P95延迟 | Gin middleware metrics | 平台网关组 |
| 业务层 | OrderService.Process() error rate |
OpenTelemetry custom span | 订单域团队 |
| 基础设施层 | redis.Client.Do() timeout count |
go-redis hook | 基础设施平台 |
graph TD
A[HTTP Handler] -->|api-layer SLO| B[Business UseCase]
B -->|biz-layer SLO| C[DB/Redis Client]
C -->|infra-layer SLO| D[(Redis Cluster)]
2.3 错误预算的数学本质:Go中P99/P95误差容限与泊松过程建模实践
错误预算并非经验阈值,而是服务可靠性在时间域上的概率约束。其核心是将SLO(如“99.9%请求延迟 ≤ 200ms”)映射为单位时间内可容忍的异常事件数。
泊松过程建模基础
当请求到达服从齐次泊松过程(速率 λ),且单次超时事件独立,则单位时间超时次数近似服从 Poisson(λ·p),其中 p 是单请求超时概率(由 P99 延迟分布决定)。
Go 中误差容限计算示例
// 基于P99=180ms、SLO=99.9%、QPS=1000,推导每小时允许超时数
const (
slo = 0.999 // 服务等级目标
qps = 1000.0 // 每秒请求数
windowSec = 3600.0 // 1小时窗口(秒)
)
allowedFailures := (1 - slo) * qps * windowSec // ≈ 3.6 → 向下取整为3次/小时
逻辑说明:
1-slo是最大允许失败率;乘以总请求数(qps × windowSec)即得该窗口内最大允许失败次数。该值即错误预算的离散计数形式,是告警与熔断策略的数学锚点。
关键参数对照表
| 参数 | 含义 | 典型值 |
|---|---|---|
| P99 延迟 | 99% 请求响应时间上限 | 180 ms |
| SLO 失败率 | 允许的超时比例 | 0.001 |
| 错误预算/小时 | 可容忍超时事件总数 | 3 次 |
graph TD
A[P99延迟测量] --> B[推导单请求超时概率p]
B --> C[泊松建模:N~Poisson\\(λ·p\\)]
C --> D[错误预算=⌊E[N]⌋]
2.4 多维度SLI融合:Go中间件链路中延迟、错误率、饱和度的协同采样策略
在高并发微服务链路中,单一SLI易掩盖系统瓶颈。需对延迟(p95)、错误率(HTTP 5xx占比)、饱和度(goroutine数/阈值)进行时间对齐的协同采样。
采样时机协同设计
- 基于
runtime.ReadMemStats()与http.Hijacker钩子统一触发 - 每10秒窗口内聚合三类指标,避免时钟漂移
Go中间件实现示例
func SLIMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
// 三元组同步记录
recordSLI(
time.Since(start).Microseconds(),
float64(rw.status >= 500 && rw.status < 600),
float64(runtime.NumGoroutine()) / 1000,
)
})
}
recordSLI将延迟(μs)、错误标识(0/1)、归一化饱和度(0~1)写入环形缓冲区,支持滑动窗口聚合。参数1000为预设goroutine安全阈值,可动态配置。
协同采样关键约束
| 维度 | 采样频率 | 精度要求 | 关联性机制 |
|---|---|---|---|
| 延迟 | 每请求 | ±10μs | 与响应状态强绑定 |
| 错误率 | 每响应 | 布尔粒度 | 依赖rw.status捕获 |
| 饱和度 | 每100ms | ±5 goros | runtime快照对齐 |
graph TD
A[HTTP请求进入] --> B[启动计时+goroutine快照]
B --> C[执行业务Handler]
C --> D[捕获状态码+结束计时]
D --> E[三指标时间戳对齐写入]
E --> F[滑动窗口聚合计算SLI]
2.5 动态SLO调整:基于Go pprof+expvar实时反馈的自适应阈值计算框架
核心设计思想
将运行时性能指标(CPU、内存、延迟分布)与业务SLI(如P99响应时间)联合建模,实现阈值的闭环自适应。
数据采集层
通过 net/http/pprof 和 expvar 双通道暴露指标:
// 启用标准pprof与自定义expvar指标
import _ "net/http/pprof"
import "expvar"
var sloThreshold = expvar.NewFloat("slo_p99_threshold_ms")
sloThreshold.Set(200.0) // 初始值
逻辑分析:
expvar提供可写浮点变量,供调控器实时更新;pprof的/debug/pprof/trace与/debug/pprof/profile支持按需采样高开销路径,避免常驻开销。
自适应调控流程
graph TD
A[每5s采集] --> B[pprof CPU profile + expvar latency histogram]
B --> C[计算实际P99 & 资源负载率]
C --> D{偏差 >15%?}
D -->|是| E[用指数加权移动平均更新阈值]
D -->|否| F[保持当前阈值]
E --> G[写入expvar.sloThreshold]
阈值更新策略对比
| 策略 | 收敛速度 | 抗抖动性 | 实现复杂度 |
|---|---|---|---|
| 固定阈值 | — | 高 | 低 |
| 简单滑动窗口 | 中 | 低 | 中 |
| EWMA + 负载加权 | 快 | 高 | 高 |
第三章:Go后台服务的SLO保障工程体系
3.1 Go HTTP/GRPC服务的SLI埋点标准化:middleware+context+telemetry三元组实践
SLI(Service Level Indicator)埋点需贯穿请求生命周期,而非零散打点。核心在于统一注入 context.Context,由中间件(middleware)拦截请求并注入可观测性上下文,再由业务逻辑通过 telemetry 工具包消费。
三元组协同机制
- Middleware:注册全局拦截器,注入 trace ID、service name、endpoint 等基础维度
- Context:携带
telemetry.Span和 SLI 标签(如slipath,sliregion),确保跨 goroutine 传递 - Telemetry:提供
RecordLatency(),IncError()等语义化方法,绑定 OpenTelemetry SDK
func TelemetryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := telemetry.StartSpan(r.Context(), "http.server",
oteltrace.WithAttributes(
semconv.HTTPMethodKey.String(r.Method),
semconv.HTTPRouteKey.String(r.URL.Path),
))
r = r.WithContext(ctx) // 注入上下文
next.ServeHTTP(w, r)
})
}
此 middleware 在请求入口创建 span,并将 SLI 关键属性(method/path)作为 span attribute 写入,为后续 latency/error 统计提供结构化标签。
r.WithContext()确保下游 handler 可安全获取 telemetry 上下文。
| 维度 | 示例值 | 用途 |
|---|---|---|
http.status_code |
200 |
计算可用性 SLI(成功响应率) |
http.route |
/api/v1/users |
路由级 SLO 划分依据 |
slipath |
user.get_by_id |
业务语义化 SLI 标识 |
graph TD
A[HTTP/GRPC Request] --> B[Middleware: Inject Context]
B --> C[Handler: Use telemetry.FromContext]
C --> D[Record Latency/Error]
D --> E[Export to Prometheus/OTLP]
3.2 错误预算消耗的实时核算:基于Go atomic+ring buffer的毫秒级预算扣减引擎
错误预算需在毫秒级响应中精确扣减,避免锁竞争与GC干扰。核心采用 atomic.AddInt64 实现无锁计数器,配合固定容量 ring buffer(如 1024 槽)缓存最近窗口内的错误事件时间戳。
数据同步机制
ring buffer 使用原子索引(head, tail)双指针管理,写入时仅更新 head,读取时仅更新 tail,全程无互斥锁:
type BudgetRing struct {
data [1024]int64
head int64
tail int64
}
func (r *BudgetRing) Push(ts int64) {
idx := atomic.LoadInt64(&r.head) % 1024
r.data[idx] = ts
atomic.AddInt64(&r.head, 1)
}
head和tail均为int64,利用atomic保证跨 goroutine 安全;模运算% 1024实现环形覆盖,零分配、零 GC 压力。
扣减逻辑与时效性保障
- 每次请求失败调用
Push(time.Now().UnixMilli()) - 预算校验时滑动窗口扫描
tail→head区间内 ≤ 当前窗口起始时间(如now - 5min)的条目 - 统计数量即为当前错误预算消耗量
| 组件 | 延迟上限 | 内存占用 | 线程安全 |
|---|---|---|---|
| atomic 计数 | 8B | ✅ | |
| ring buffer | ~50ns | 8KB | ✅(无锁) |
graph TD
A[HTTP 请求失败] --> B[Push timestamp]
B --> C{ring buffer 写入}
C --> D[atomic head++]
D --> E[Budget Check]
E --> F[滑动窗口扫描]
F --> G[返回实时消耗值]
3.3 SLO违规熔断机制:Go中基于circuit breaker与rate limiter的协同响应模型
当SLO(如99% P95延迟 ≤ 200ms)持续不达标时,需触发协同防御响应:熔断器阻断故障传播,限流器保护下游容量。
协同决策逻辑
- 熔断器监听SLO指标(如每分钟错误率 > 5% → OPEN)
- 限流器在熔断OPEN或HALF-OPEN时自动降级为
5 QPS(原50 QPS) - 恢复期启用指数退避探测请求
// 基于go-contrib/circuitbreaker + golang.org/x/time/rate
cb := circuit.NewCircuitBreaker(circuit.Settings{
FailureThreshold: 5, // 连续5次失败触发OPEN
Timeout: 60 * time.Second,
})
limiter := rate.NewLimiter(rate.Every(200*time.Millisecond), 5) // 协同限流窗口
该配置实现“失败感知→熔断→限流收缩→探测恢复”闭环。
FailureThreshold需与SLO观测窗口(如1min)对齐;rate.Limiter的burst=5确保半开状态可安全探活。
| 状态 | 熔断器行为 | 限流器QPS | 触发条件 |
|---|---|---|---|
| CLOSED | 全量放行 | 50 | SLO达标 |
| OPEN | 拒绝所有请求 | 5 | 错误率 > 5% × 60s |
| HALF-OPEN | 允许1个探测 | 1 | OPEN超时后首次尝试 |
graph TD
A[SLO监控] -->|连续违规| B(CB: OPEN)
B --> C[限流器切换至5QPS]
C --> D[60s后进入HALF-OPEN]
D -->|成功1次| E[CB: CLOSED + 恢复50QPS]
D -->|失败| B
第四章:典型故障场景下的SLO修复与验证闭环
4.1 连接池耗尽类故障:Go net/http Transport与database/sql连接池的SLI归因分析
连接池耗尽常表现为高延迟、5xx错误或上下文超时,但根源需区分 net/http.Transport 与 database/sql 两类池化资源。
Transport 连接复用瓶颈
http.DefaultTransport.(*http.Transport).MaxIdleConns = 100
http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 100
http.DefaultTransport.(*http.Transport).IdleConnTimeout = 30 * time.Second
MaxIdleConnsPerHost 控制每主机空闲连接上限;若并发请求突增且响应慢,空闲连接被快速占满,新请求将阻塞在 getConn 阶段——此阶段不计入 HTTP SLI(如 P95 响应时间),却直接拖垮可用性。
database/sql 连接泄漏特征
| 指标 | 正常值 | 耗尽征兆 |
|---|---|---|
sql.DB.Stats().Idle |
> 80% | |
sql.DB.Stats().InUse |
波动平稳 | 持续 ≥ MaxOpenConns |
sql.DB.Stats().WaitCount |
接近零 | 持续增长 |
归因决策树
graph TD
A[HTTP 请求超时] --> B{是否复用连接?}
B -->|否| C[检查 Transport.MaxIdleConnsPerHost]
B -->|是| D[检查后端 DB 响应延迟]
D --> E{DB 查询 P99 > 2s?}
E -->|是| F[排查 sql.DB.ConnMaxLifetime]
E -->|否| G[检查 context.WithTimeout 传递完整性]
4.2 GC停顿引发的P99毛刺:Go 1.22 runtime/metrics在SLO偏差定位中的实证应用
实时观测GC停顿毛刺
Go 1.22 引入 runtime/metrics 的标准化指标,/gc/heap/allocs:bytes 和 /gc/pause:seconds 可直接反映瞬时分配压力与STW时长:
import "runtime/metrics"
// 获取最近5次GC暂停时长(秒)
var samples []metrics.Sample
samples = []metrics.Sample{{Name: "/gc/pause:seconds"}}
metrics.Read(samples)
fmt.Printf("Latest GC pause: %.3f ms", samples[0].Value.(float64)*1e3)
逻辑分析:
/gc/pause:seconds返回的是滑动窗口内最大单次暂停时长(非平均),单位为秒;乘以1e3转为毫秒,精准匹配P99毛刺阈值(如 >10ms)。该指标低开销、无侵入,替代了旧版debug.ReadGCStats的采样延迟缺陷。
SLO偏差归因流程
graph TD
A[Prometheus抓取 /gc/pause:seconds] --> B{P99响应 >200ms?}
B -->|是| C[关联时间戳对齐]
C --> D[定位对应GC事件]
D --> E[检查堆增长速率 /gc/heap/allocs:bytes]
关键指标对照表
| 指标名 | 含义 | 单位 | P99毛刺敏感度 |
|---|---|---|---|
/gc/pause:seconds |
最近一次GC STW时长 | 秒 | ⭐⭐⭐⭐⭐ |
/gc/heap/allocs:bytes |
当前堆分配总量 | 字节 | ⭐⭐⭐⭐ |
4.3 Context超时传播失配:Go服务间调用链中SLO承诺断裂的修复模式(WithTimeout/WithDeadline重构)
当上游服务以 context.WithTimeout(ctx, 500ms) 发起调用,下游却误用 context.WithDeadline(ctx, time.Now().Add(2s)),将本应级联衰减的超时演变为固定宽限期,导致SLO违规放大。
超时语义错配的典型表现
- 上游500ms SLO → 下游预留2s → 中间件重试耗尽后仍阻塞
WithDeadline忽略父上下文剩余时间,破坏调用链时效一致性
正确重构模式
// ✅ 正确:继承并压缩父context剩余超时
childCtx, cancel := context.WithTimeout(parentCtx, 300*time.Millisecond)
// ❌ 错误:与父ctx生命周期脱钩
deadline := time.Now().Add(2 * time.Second)
childCtx, cancel := context.WithDeadline(parentCtx, deadline)
WithTimeout 基于父ctx当前剩余时间动态计算截止点;WithDeadline 则强制锚定绝对时间点,无视上游已流逝耗时。
修复效果对比
| 指标 | WithTimeout | WithDeadline |
|---|---|---|
| 调用链超时收敛性 | 强(逐跳衰减) | 弱(时间漂移) |
| SLO违约率(实测) | ↓ 62% | ↑ 210% |
graph TD
A[Client: SLO=500ms] --> B[Service A: WithTimeout 400ms]
B --> C[Service B: WithTimeout 300ms]
C --> D[DB: WithTimeout 200ms]
4.4 并发控制失效导致的饱和度超标:Go sync.Pool与semaphore在SLO容量保障中的量化验证
当高并发请求击穿限流阈值,sync.Pool 的无界复用与信号量(semaphore)的粗粒度配额常引发隐性资源争用,导致 CPU 饱和度突破 SLO 约束(如 >85% 持续 5 分钟)。
数据同步机制
sync.Pool 在 GC 周期清空本地池,若对象构造开销大且请求脉冲密集,将触发高频 New 分配,加剧 GC 压力与内存抖动:
var bufPool = sync.Pool{
New: func() interface{} {
return make([]byte, 0, 1024) // 固定初始容量,避免扩容抖动
},
}
New函数仅在池空时调用;未设置MaxSize(Go 标准库不支持),实际复用率依赖 Goroutine 亲和性与 GC 频率,需结合GOGC=20与 pprof heap profile 交叉验证复用衰减曲线。
信号量精度缺陷
基于 golang.org/x/sync/semaphore 的计数器无法区分请求权重,统一按 1 扣减:
| 请求类型 | 实际资源消耗(ms/CPU) | 语义权重 | 当前扣减量 |
|---|---|---|---|
| 图片缩放 | 120 | 3 | 1 ✗ |
| 日志写入 | 8 | 1 | 1 ✓ |
容量验证路径
graph TD
A[QPS 上升] --> B{semaphore.Acquire?}
B -->|Yes| C[分配 bufPool.Get]
B -->|No| D[拒绝并打点 metrics_slo_violation_total]
C --> E[执行业务逻辑]
E --> F[bufPool.Put 回收]
F --> G[GC 触发时批量清理]
关键结论:需将 semaphore.Weighted 与 sync.Pool 容量绑定,并通过 runtime.ReadMemStats + expvar 实时校准每请求平均内存/时间开销。
第五章:面向未来的Go SRE能力演进方向
智能化故障预测与根因定位
某头部云厂商将Go编写的SRE平台与eBPF采集器深度集成,构建实时指标-调用链-内核事件三维关联图谱。当服务P95延迟突增时,系统自动触发因果推理引擎(基于Go实现的轻量级贝叶斯网络),在17秒内定位到特定CPU频点降频引发的goroutine调度抖动,并推送修复建议至值班工程师企业微信。该能力已覆盖83%的P0级告警,平均MTTR缩短至4.2分钟。
多运行时协同编排能力
现代Go SRE工具链需同时管理Kubernetes Pod、WASM边缘函数及裸金属Go守护进程。以开源项目go-srekit为例,其采用统一Operator模式抽象不同运行时生命周期——通过CRD定义SREPolicy资源,声明式指定熔断阈值、自动扩缩策略与混沌实验范围,实际执行层由Go Runtime动态加载对应适配器(如k8s_adapter.so或wasmtime_adapter.wasm)。
面向韧性架构的Go原生实践
某金融级支付网关重构中,SRE团队强制要求所有Go服务启用GODEBUG=madvdontneed=1并配合runtime.SetMemoryLimit()实现内存软限。结合自研的go-resilience库(含熔断器、重试退避、负载感知路由),在模拟AZ级故障时,核心交易链路RTO控制在8秒内,且无goroutine泄漏现象。关键指标如下:
| 能力维度 | 传统方案 | Go原生韧性方案 |
|---|---|---|
| 内存超限响应时间 | 32s | 1.8s |
| 熔断状态同步延迟 | 200ms | |
| 混沌注入精度 | Pod粒度 | Goroutine粒度 |
安全左移的SRE流水线
某政务云平台将SRE质量门禁嵌入CI/CD:Go代码提交后自动触发三项检查——go vet增强规则集(检测unsafe.Pointer误用)、gosec扫描结果与OpenSSF Scorecard评分联动、以及基于go mod graph生成的依赖拓扑图进行SBOM合规校验。2024年Q1拦截高危漏洞27例,其中12例为Go标准库net/http未正确处理HTTP/2帧导致的DoS风险。
// 示例:SRE策略执行器中的弹性上下文
type ResilientContext struct {
Deadline time.Time
Budget *CircuitBreakerBudget // 基于Go atomic实现的实时熔断计数器
TraceID string
}
func (rc *ResilientContext) Execute(fn func() error) error {
if !rc.Budget.Allow() {
return errors.New("circuit breaker open")
}
defer rc.Budget.Report()
return fn()
}
可观测性数据平面重构
传统Prometheus+Grafana架构面临标签爆炸瓶颈。某电商SRE团队采用Go编写otel-collector扩展组件,将TraceID与Metrics标签在采集端完成哈希映射压缩,内存占用降低64%;同时利用Go泛型实现多租户指标路由引擎,支持单实例处理12万TPS写入,延迟P99
graph LR
A[Go Agent] -->|OTLP| B[Collector Extension]
B --> C{Hash Router}
C --> D[TSDB-A: tenant-a]
C --> E[TSDB-B: tenant-b]
D --> F[Grafana Query]
E --> F 