多租户 SaaS 平台的端到端入站请求链路追踪设计

2026-05-22 28 预计阅读时间:1 分钟
来源:cncf.io AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:12 分钟

一个租户的请求从网关进入,经过鉴权、编排引擎、数据服务,最终落到下游第三方 API——中间跨越了十几条微服务调用。当这位租户反馈"响应变慢",你打开监控面板,看到的却是一堆散落在各服务日志里的碎片化 trace ID,根本拼不出一条完整链路。这就是大多数云原生 SaaS 平台在可观测性上的真实困境。

为什么多租户场景让追踪变得更难

单租户系统里,一个 trace_id 就能串联整条调用链。多租户平台引入了三个额外维度:

租户隔离——不同租户的请求可能走不同路由规则、不同数据分片,甚至不同版本的服务实例。同一接口、同一时间段内,租户 A 的请求可能命中缓存而租户 B 的请求穿透到数据库,两条链路的表现截然不同。

共享基础设施——网关、消息队列、共享数据服务往往被所有租户共用。一个共享组件的延迟抖动,影响的是一批租户,但传统追踪系统只按服务维度聚合,看不到"哪些租户受影响"。

合规与数据边界——某些行业要求租户的链路数据不能混存到同一存储后端,或者不同租户的 trace 数据保留时长不同。这直接影响追踪系统的存储架构设计。

链路上下文的传播设计

端到端追踪的核心是让 trace context 从入口一直传播到出口,不丢失、不混淆。在多租户场景下,传播的上下文需要比标准 W3C traceparent 多一个字段:租户标识

一个实用的传播头设计:

x-trace-id: <trace-id>
x-tenant-id: <tenant-id>
x-span-id: <span-id>

或者,如果你已经采用 W3C Trace Context 标准,可以把租户标识放进 tracestate

traceparent: 00-<trace-id>-<span-id>-01
tracestate: tenant=acme Corp,slo=gold

tracestate 的好处是兼容现有 OpenTelemetry SDK 的传播机制,不需要自定义 HTTP 头解析逻辑。租户标识跟着标准传播路径走,每个服务自动继承。

关键设计决策点:

决策 选项 建议
租户 ID 传播方式 自定义 HTTP 头 vs tracestate 优先 tracestate,减少 SDK 改动
租户 ID 来源 网关提取(JWT / subdomain / header) 网关统一提取,下游服务不再重复解析
上下文丢失时的行为 创建新 trace vs 拒绝请求 生产环境建议记录告警后创建新 trace,不阻断业务

用 OpenTelemetry 实现租户感知的链路追踪

下面给出一个最小可运行的示例:在 Python 微服务中,通过 OpenTelemetry 自动传播 trace context,并在网关层注入租户标识到 tracestate

网关层:提取租户 ID 并注入 tracestate

假设网关基于 FastAPI,从 JWT 中提取租户 ID:

# gateway/main.py
from fastapi import FastAPI, Request
from opentelemetry import trace, context, propagate
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
import jwt

app = FastAPI()

# 初始化 OpenTelemetry
resource = Resource.create({"service.name": "saas-gateway", "service.version": "1.0"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
)
trace.set_tracer_provider(provider)
FastAPIInstrumentor.instrument_app(app)

TENANT_JWT_SECRET = "change-me-in-production"

@app.middleware("http")
async def inject_tenant_tracestate(request: Request, call_next):
    # 从 Authorization 头提取租户 ID
    auth_header = request.headers.get("authorization", "")
    tenant_id = "anonymous"
    if auth_header.startswith("Bearer "):
        try:
            payload = jwt.decode(
                auth_header[7:], TENANT_JWT_SECRET, algorithms=["HS256"]
            )
            tenant_id = payload.get("tenant_id", "anonymous")
        except jwt.InvalidTokenError:
            pass

    # 获取当前 span,注入租户标识到 tracestate
    current_span = trace.get_current_span()
    if current_span.is_recording():
        current_span.set_attribute("saas.tenant.id", tenant_id)
        # 写入 tracestate,下游自动传播
        current_span.set_attribute("saas.tenant.plan", payload.get("plan", "free"))

    response = await call_next(request)
    return response

@app.get("/api/orders")
async def get_orders(request: Request):
    tracer = trace.get_tracer("gateway")
    with tracer.start_as_current_span("gateway.get_orders") as span:
        span.set_attribute("http.route", "/api/orders")
        # 模拟调用下游编排服务
        return {"orders": ["order-1", "order-2"], "tenant_debug": "check tracestate"}

下游服务:自动继承租户上下文

下游服务不需要额外解析租户 ID——OpenTelemetry SDK 自动传播 traceparenttracestate

# order-service/main.py
from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
import httpx

app = FastAPI()

resource = Resource.create({"service.name": "order-service"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
)
trace.set_tracer_provider(provider)
FastAPIInstrumentor.instrument_app(app)
HTTPXClientInstrumentor.instrument()

@app.get("/orders")
async def list_orders():
    tracer = trace.get_tracer("order-service")
    with tracer.start_as_current_span("order-service.list_orders") as span:
        # tracestate 中的 tenant 信息已被 SDK 自动传播
        # 可以从 span attributes 读取(如果上游设置了 attribute)
        # 也可以从 propagator 提取的 context 中读取
        span.set_attribute("operation.type", "list")

        # 调用下游数据服务,trace context 自动传播
        async with httpx.AsyncClient() as client:
            resp = await client.get("http://data-service:8001/data")
            return resp.json()

运行示例的最小依赖

# 安装依赖
pip install fastapi uvicorn opentelemetry-api opentelemetry-sdk \
  opentelemetry-exporter-otlp-proto-grpc opentelemetry-instrumentation-fastapi \
  opentelemetry-instrumentation-httpx httpx PyJWT

# 启动 OTel Collector(用 Docker 最简单)
docker run -d --name otel-collector \
  -p 4317:4317 -p 4318:4318 \
  -p 8889:8889 \
  otel/opentelemetry-collector-contrib:0.96.0

# 启动网关
uvicorn gateway.main:app --port 8000 --host 0.0.0.0

# 启动下游服务
uvicorn order_service.main:app --port 8002 --host 0.0.0.0

# 发送带 JWT 的请求(tenant_id=acme)
# 先生成一个测试 token:
python -c "import jwt; print(jwt.encode({'tenant_id':'acme','plan':'gold'}, 'change-me-in-production', algorithm='HS256'))"

# 用生成的 token 发请求:
curl -H "Authorization: Bearer <上一步输出的token>" http://localhost:8000/api/orders

在 Jaeger 或 Grafana 中查看这条 trace,你会看到 saas.tenant.id=acme 作为 span attribute 出现在整条链路的每个 span 上。

租户感知的查询与聚合

有了租户标识作为 span attribute,下一步是让追踪后端支持按租户过滤和聚合。OpenTelemetry Collector 的配置可以做到这一点:

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  # 按 tenant ID 添加 resource attribute,便于后端过滤
  attributes/tenant:
    actions:
      - key: saas.tenant.id
        from_attribute: saas.tenant.id
        action: upsert
      - key: saas.tenant.plan
        from_attribute: saas.tenant.plan
        action: upsert

  # 可选:按租户路由到不同存储后端
  groupbyattrs/tenant:
    group_by:
      - saas.tenant.id

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
  # 合规场景:高敏感租户数据导出到独立后端
  otlp/highsec:
    endpoint: highsec-jaeger:4317

service:
  pipelines:
    traces/default:
      receivers: [otlp]
      processors: [attributes/tenant]
      exporters: [otlp/jaeger]
    # 高安全等级租户走独立管道
    traces/highsec:
      receivers: [otlp]
      processors: [groupbyattrs/tenant, attributes/tenant]
      exporters: [otlp/highsec]

在 Jaeger UI 中查询时,用 saas.tenant.id=acme 作为 tag filter,就能只看特定租户的链路。Grafana Tempo 同样支持通过 TraceQL 按 attribute 过滤:

{ .saas.tenant.id = "acme" && .http.route = "/api/orders" }

落地时的几个现实考量

网关是唯一可信的租户注入点。 下游服务不应该自行解析 JWT 来确定租户——这既浪费又容易出错。网关解析一次,写入 tracestate 和 span attribute,下游只读。

异步消息的传播不能断。 如果请求链路经过 Kafka 或 RabbitMQ,消息的 header 里必须携带 traceparenttracestate。OpenTelemetry 的 Kafka instrumentation 已经支持这一点,但你需要确认消息生产者侧的 SDK 版本足够新。

采样策略要租户感知。 全量采集成本太高。一个务实的做法:对 gold/silver 租户全量采集,free 租户按比例采样。在 Collector 中用 Tail Sampling Processor 实现:

processors:
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: gold-tenant-full
        type: and
        and:
          policies:
            - name: is-gold
              type: string_attribute
              string_attribute:
                key: saas.tenant.plan
                values: ["gold"]
            - name: always-sample
              type: always_sample
      - name: free-tenant-10pct
        type: and
        and:
          policies:
            - name: is-free
              type: string_attribute
              string_attribute:
                key: saas.tenant.plan
                values: ["free"]
            - name: probabilistic-10
              type: probabilistic
              probabilistic:
                sampling_percentage: 10

跨服务边界的 ID 一致性。 如果平台中有第三方 SaaS 集成(比如调用 Stripe API),你无法控制对方的 trace 传播。此时在出口处记录一个 span,把外部服务的请求 ID 作为 saas.external.request_id attribute 存下来,事后可以手动关联。

上手清单

  1. 确认网关提取租户 ID 的方式——JWT claim、子域名、自定义 header,选一种,统一入口。
  2. 在网关层注入 tracestate 和 span attribute——改动集中在一个点,下游零改动即可继承。
  3. 升级所有服务的 OpenTelemetry SDK 到支持 tracestate 传播的版本——0.40+ 基本都支持。
  4. Collector 配置 attribute processor——确保 saas.tenant.id 被提升为可查询的 tag。
  5. 配置按租户的采样策略——从全量采集开始,观察一周数据量,再逐步收紧。
  6. 异步链路补齐——检查 Kafka/RabbitMQ 的 message header 传播,确保不断链。

端到端追踪不是一次性工程,而是随着服务数量和租户规模增长持续演进的系统。先把网关注入和 Collector attribute 处理跑通,你就有了按租户排查问题的最小能力。之后再逐步加采样策略、合规隔离、跨边界关联,每一步都有明确的可观测性收益。


相关推荐