OpenTelemetry 多租户分流怎么做:按服务名路由 traces 的实战方案
OpenTelemetry 多租户分流怎么做按服务名路由 traces 的实战方案一、问题背景单租户 Collector 不够用了我们平台早期只有一个业务方OpenTelemetry Collector 收到 SDK 上报的 trace 后一股脑往 Jaeger 后端写看着挺干净。但从 Q2 开始业务方从 1 个变成了 6 个3 个 toB 业务线、2 个内部中台、1 个支付子链。每个业务方都要求我的 trace 别跟别人混在一起——倒不是技术洁癖主要是合规审计要求支付子链的 trace 必须留存在我们自建的存储里不能外发到第三方 SaaS。成本分摊toB 业务线按 trace 量结算要能按 service.name 切片统计每家用了多少。排查干扰A 业务的告警如果命中 B 业务的 trace定位效率会塌方。一开始我天真地让每个业务方起一套独立的 OTel Agent Collector Jaeger结果运维同学一周内就来找我骂街6 套 collector、6 份配置、6 个 dashboard、6 个告警通道更别说每多一套就多一个证书、多一个升级窗口。这才回过味来多租户的本质是一份管道多路出口而不是多份管道每家一个。今天我把最终跑通的方案拆给你看按 service.name 路由 traces 到不同后端配合独立的采样和落库策略。一些关键的判断点写方案之前先明确两件事不要把租户和服务混为一谈service.name 是 SDK 上报时打的标签分流天然按它走如果租户边界比服务更细比如一个业务方有多个子服务但数据要按业务方隔离就需要再加一层 tenant_id 属性。路由在 Collector 做不在 SDK 做让 SDK 关心业务让 Collector 关心路由。SDK 只管打标签Collector 的 connectors routing processor 才是干这个的。二、整体架构[App SDK] --OTLP-- [Gateway Collector] --route-- [Tenant A Backend] (Jaeger) \-- [Tenant B Backend] (Tempo) \-- [Tenant C Backend] (Datadog) \-- default sink (ClickHouse)三层 Collector 的设计Gateway (Agent 模式): 收 SDK 上报做限流、auth、批处理统一入口。Router (Collector 模式): 核心路由层按 service.name / tenant_id 决定把 trace 转发到哪。Tenant Backend: 每个租户独立的后端存储可以是 Jaeger / Tempo / ClickHouse / Datadog / S3Athena 任意组合。为什么不直接让 SDK 发到不同后端因为 SDK 不该知道后端在哪。路由下沉到 Collector 后未来加租户只需要改 Collector 配置App 一行代码不动。三、关键配置Router Collector下面是生产环境跑了大半年的 Router Collector 核心配置。注解直接写在 yaml 里# /etc/otelcol-contrib/config.yamlreceivers:otlp:protocols:grpc:endpoint:0.0.0.0:4317http:endpoint:0.0.0.0:4318processors:# 1. 批处理聚合后批量转发降低下游压力batch:timeout:5ssend_batch_size:8192# 2. 内存限制保护 Collector 自身防止 OOMmemory_limiter:check_interval:1slimit_percentage:80spike_limit_percentage:25# 3. 路由核心按 service.name 决定走哪条 export 链路routing/traces:default_pipelines:[traces/default]error_mode:ignoretable:# 租户 A支付子链必须落自建 Jaeger-context:resourcestatement:route() where attributes[service.name] pay-gateway \ or attributes[service.name] pay-settlementpipelines:[traces/tenant_a]# 租户 BtoB 业务线 A-context:resourcestatement:route() where attributes[service.name] b2b-inventory \ or attributes[service.name] b2b-orderpipelines:[traces/tenant_b]# 租户 CtoB 业务线 B要求用 Datadog-context:resourcestatement:route() where attributes[service.name] b2b-reportpipelines:[traces/tenant_c]# 4. 不同租户独立采样支付链路全采报表链路 10% 采样tail_sampling/tenant_a:decision_wait:10snum_traces:50000expected_new_traces_per_sec:5000policies:-name:keep-alltype:always_sampletail_sampling/tenant_b:decision_wait:10snum_traces:30000policies:-name:errors-onlytype:status_codestatus_code:{status_codes:[ERROR]}-name:slow-tracestype:latencylatency:{threshold_ms:500}-name:probabilistictype:probabilisticprobabilistic:{sampling_percentage:10}exporters:# 租户 A自建 Jaegerotlp/tenant_a:endpoint:jaeger-tenant-a.internal:4317tls:insecure:falsecert_file:/etc/certs/jaeger-tenant-a.crtkey_file:/etc/certs/jaeger-tenant-a.keysending_queue:{enabled:true,num_consumers:10,queue_size:5000}retry_on_failure:{enabled:true,initial_interval:1s,max_interval:30s}# 租户 B自建 Tempootlp/tenant_b:endpoint:tempo-tenant-b.internal:4317sending_queue:{enabled:true,num_consumers:10}# 租户 CDatadog SaaSdatadog/tenant_c:api:key:${env:DD_API_KEY}site:datadoghq.com# 默认出口长期冷存储 成本结算otlphttp/default:endpoint:https://clickhouse-traces.internal/v1/tracestls:{insecure:false}service:telemetry:logs:{level:info}pipelines:traces:receivers:[otlp]processors:[memory_limiter,batch,routing/traces]# routing 后会自动衍生出 traces/tenant_a, traces/tenant_b 等子 pipeline几个容易踩坑的地方routingprocessor 配完之后真正的子 pipeline 是运行时衍生出来的配置里写不写都无所谓Collector 会自动生成traces/pipeline_name。default_pipelines一定要配否则没匹配上的 trace 会被直接丢弃——我们一开始漏了这条报警响了才发现有 30% 的 trace 不见了。error_mode: ignore比silent安全前者会记日志后者完全静默排查时找不到线索。四、踩过的坑坑 1service.name 写错trace 全跑到 default 出口SDK 默认把 service.name 设成unknown_service:进程名结果一启动全跑到 default。强制要求所有 SDK 在启动时显式覆盖 service.name我们用 lint 工具OpenTelemetry SDK 提供的validate_service_name检查器卡 CI没填或填了unknown_开头的直接拦在流水线前。坑 2tail_sampling 改变了 trace 的完整性在 Router Collector 上做 tail sampling 时所有租户的 trace 都先汇总到这里再决定留还是丢。这意味着 tenant_a 的采样窗口10s会卡住 tenant_b 的转发速度。生产上我们做了两件事把tail_sampling放到每个租户的 Backend Collector 而不是 Router Collector让路由零延迟。在 Router 上只做 head sampling按概率粗筛把 90% 的噪声提前干掉剩下的 trace 再交给租户后端精筛。坑 3批量 路由的顺序反了routingprocessor 必须在batch之后。否则一条 batch 里 5 条 trace 分属 3 个租户被一次性发到第一个匹配的下游路由就废了。实际写配置时把 routing 放 batch 后是 OK 的但反过来绝对不行。坑 4租户后端挂了整个路由链路卡死我们用sending_queue retry_on_failure隔离每个租户后端的故障tenant_a 的 Jaeger 短暂不可达时queue 会兜住retry 30 秒一次其他租户完全不受影响。这块的sending_queue.num_consumers不能开太大否则一个慢后端会吃满 Collector 的连接池。五、成本与可观测性收益上线半年后的几个关键数据存储成本分租户后报表类业务trace 量最大但价值最低只采 10%落 ClickHouse 冷存储月度成本降了 62%。排障效率支付链路全量采独立存储后P0 故障平均定位时间从 18 分钟降到 6 分钟——主要是不再被无关 trace 干扰。接入新租户耗时从原来的开 6 套 collector 6 套配置变成改一份 Router yaml 加 3 行最快的一次 25 分钟完成新租户接入。六、写在最后把多租户分流做好关键是认清三层角色SDK 只管打标签Router 只管分发Backend 才管存储和查询。任何一层越界比如让 SDK 知道后端地址、让 Backend 做路由后续都会被自己埋的单点故障咬回来。这套方案现在我们 6 个业务方共用一份 Router 配置扩第 7 个租户基本就是改 yaml 的活。如果你也在搞多租户可观测性希望这篇能给你一些可落地的参考。EOF