4. 租户模型设计:基于 Tenant ID 的元数据注入策略

在多租户可观测性管道中,租户模型是服务边界划分的基石。其核心目标是在数据产生、采集、处理、存储的全生命周期中,将每个数据点与唯一的租户身份绑定。本章聚焦于 Tenant ID 的元数据注入策略,阐述如何在 OpenTelemetry 生态中设计一套高效、一致且安全的租户标识方案。

4.1 租户模型的核心要素

一个健壮的租户模型需要明确三个关键维度:标识符、注入点和传播机制。下表对比了不同租户标识方案的适用场景与优劣:

方案 标识符示例 注入点 优势 劣势
Header 透传 X-Tenant-ID: acme-corp 网关 / 入口代理 无侵入,与语言无关 依赖上游服务正确传递
Resource 属性 tenant.id (OTel Resource) SDK 初始化 / 采集器 原生 OTel 语义,自动关联 需 SDK 版本支持
Span 属性 tenant.id (Span Attribute) 业务代码 / 拦截器 粒度细,可覆盖单次请求 代码侵入,易遗漏
Context 传播 W3C Baggage tenant-id SDK Context 传播器 跨进程透明传递 Baggage 大小限制,需清理
最佳实践:推荐采用 Resource 属性 + W3C Baggage 的组合策略。Resource 属性用于数据持久化存储时的租户归属,Baggage 用于跨服务调用时的实时上下文传递。

4.2 基于 Tenant ID 的元数据注入流程

下图展示了从请求入口到数据导出的完整元数据注入链路。该流程确保每个 Span 和 Metric 在生成时即携带租户身份,无需事后回溯。

Tenant ID 元数据注入流程 Tenant ID 元数据注入流程 入口网关 提取 Tenant ID 注入 W3C Baggage 服务 A (SDK) 读取 Baggage 设置 Resource 属性 导出器 (Exporter) 说明:Baggage 跨进程传递 Tenant ID,SDK 在创建 Span 时将其写入 Resource 属性,最终导出器按 Tenant ID 路由数据。

4.3 元数据注入的三种策略

根据租户模型的复杂度和运维能力,可以选择以下三种注入策略之一:

  • 策略一:网关级注入(推荐)
    • 在 API 网关或反向代理层(如 Envoy、Nginx)解析请求头或 JWT,提取 Tenant ID。
    • 通过 X-Tenant-ID 请求头或 W3C Baggage 注入到下游服务。
    • 优点:对业务代码零侵入,统一管控。
  • 策略二:SDK 级注入
    • 在 OpenTelemetry SDK 初始化时,通过环境变量或配置文件设置 tenant.id Resource 属性。
    • 适用于每个服务实例只服务一个租户的场景(如 Sidecar 模式)。
    • 优点:配置简单,无需网络传递。
  • 策略三:业务代码级注入
    • 在业务逻辑中通过拦截器或 AOP 手动设置 Span Attribute。
    • 适用于需要根据请求内容动态决定租户的场景(如 SaaS 平台)。
    • 优点:灵活性最高;缺点:代码侵入,需严格规范。
关键决策点:如果租户信息在请求入口即可确定,优先选择 网关级注入;如果租户信息需要在服务内部动态计算,则必须使用 业务代码级注入,并配合 Baggage 传播。

4.4 Tenant ID 的传播与隔离边界

Tenant ID 的传播必须遵循 “一次注入,全程携带” 的原则。下图展示了在微服务调用链中,Tenant ID 如何通过 Baggage 跨越服务边界,并在每个服务内映射为 Resource 属性:

Tenant ID 跨服务传播 Tenant ID 跨服务传播与隔离边界 服务 A Resource: tenant.id=acme Baggage: tenant-id=acme 服务 B Resource: tenant.id=acme Baggage: tenant-id=acme 服务 C Resource: tenant.id=acme Baggage: tenant-id=acme 隔离边界:每个服务独立维护自己的 Resource 属性,但通过 Baggage 保证 Tenant ID 在调用链中一致。

4.5 代码示例:在 OTel SDK 中注入 Tenant ID

以下示例展示了如何在 Java 应用中通过 OpenTelemetry SDK 读取 Baggage 并设置 Resource 属性:

// 1. 从 Baggage 中读取 Tenant ID
String tenantId = Baggage.current().getEntryValue("tenant-id");

// 2. 如果存在,则创建包含 tenant.id 的 Resource
Resource resource = Resource.getDefault();
if (tenantId != null && !tenantId.isEmpty()) {
    resource = Resource.create(
        Attributes.of(AttributeKey.stringKey("tenant.id"), tenantId)
    );
}

// 3. 使用该 Resource 初始化 OpenTelemetry SDK
OpenTelemetrySdk sdk = OpenTelemetrySdk.builder()
    .setResource(resource)
    .setTracerProvider(
        SdkTracerProvider.builder()
            .setResource(resource)
            .addSpanProcessor(BatchSpanProcessor.builder(
                OtlpGrpcSpanExporter.builder().build()
            ).build())
            .build()
    )
    .build();
注意:如果 Baggage 中不存在 tenant-id,应使用默认值(如 unknown)或拒绝该请求,避免数据丢失租户归属。

4.6 租户模型设计检查清单

在实施租户模型前,请逐项确认以下要点:

  1. 唯一性:Tenant ID 是否全局唯一?是否区分大小写?
  2. 一致性:所有服务是否使用相同的属性键名(如 tenant.id)?
  3. 传播完整性:是否所有跨服务调用都携带了 Baggage?是否遗漏了异步消息队列?
  4. 清理机制:是否在数据导出前清理了 Baggage 中的敏感信息?
  5. 降级策略:当 Tenant ID 缺失时,是拒绝数据还是标记为默认租户?
推荐工具:使用 OpenTelemetry Collector 的 attributesprocessor 或 resourceprocessor 在采集侧统一注入或修正 Tenant ID,作为最后一道防线。