JS `Distributed Tracing` `Baggage Propagation`:跨服务上下文传递自定义数据

各位观众老爷,晚上好!我是你们的老朋友,今天咱们来聊聊分布式追踪里一个相当实用但又容易被忽视的家伙——Baggage Propagation(行李传递)。

开场白:追踪,不止于追踪

想象一下,你是一位侦探,负责调查一起复杂的案件。线索分散在不同的城市(服务),你需要一路追踪嫌疑人的踪迹。传统的追踪工具,比如追踪ID,只能告诉你“嫌疑人去过这里”,但不能告诉你“嫌疑人在这里做了什么”。Baggage Propagation 就像是你在嫌疑人的行李箱里放了一个秘密标签,这个标签可以携带额外的信息,帮助你更好地了解嫌疑人的行为动机和关键信息。

什么是 Baggage Propagation?

简单来说,Baggage Propagation 允许你在分布式追踪系统中跨服务传递自定义的数据。这些数据可以是用户ID、会话ID、AB测试分组、产品特征等等任何你想传递的信息。它就像一个“行李箱”,可以携带信息穿梭于各个服务之间。

为什么我们需要 Baggage Propagation?

  1. 更丰富的上下文信息: 仅仅依靠追踪ID,我们只能知道请求经过了哪些服务。但有了 Baggage,我们就可以知道用户是谁,AB测试分组是什么,从而更精确地分析问题。

  2. 更好的性能分析: 我们可以通过 Baggage 传递一些标志,比如“是否使用了缓存”,从而更好地了解性能瓶颈。

  3. 灵活的业务逻辑控制: Baggage 可以用于传递业务规则,比如“是否允许打折”,从而在不同的服务中执行不同的逻辑。

  4. 更强大的可观测性: Baggage 携带的信息可以帮助我们更好地理解系统的行为,例如追踪特定用户的请求路径,或者分析特定AB测试组的转化率。

Baggage Propagation 的工作原理

Baggage Propagation 的核心思想是:

  • 在请求的入口处,创建 Baggage。
  • 在请求的传递过程中,携带 Baggage。
  • 在请求的处理过程中,读取 Baggage。

通常,Baggage 是通过 HTTP Header 或者消息队列的 Message Header 来传递的。

代码实战:用 JavaScript 实现 Baggage Propagation

接下来,我们用 JavaScript 来演示如何实现 Baggage Propagation。我们将使用 OpenTelemetry 作为追踪框架,并且使用 HTTP Header 来传递 Baggage。

1. 安装 OpenTelemetry 相关依赖

npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-console @opentelemetry/resources @opentelemetry/semantic-conventions

2. 初始化 OpenTelemetry SDK

const { NodeSDK } = require('@opentelemetry/sdk-node');
const { ConsoleSpanExporter } = require('@opentelemetry/exporter-console');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'my-service',
  }),
  spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter()),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

3. 创建 HTTP 服务 (使用 Express.js)

npm install express
const express = require('express');
const { trace, context, propagation } = require('@opentelemetry/api');

const app = express();
const port = 3000;

app.get('/hello', (req, res) => {
  const tracer = trace.getTracer('my-tracer');

  tracer.startActiveSpan('hello-handler', (span) => {
    // 从 HTTP Header 中获取 Baggage
    const baggage = propagation.getBaggage(context.active());

    if (baggage) {
      baggage.getAllEntries().forEach((entry, key) => {
        span.setAttribute(`baggage.${key}`, entry.value);
      });
    }

    // 处理业务逻辑
    const message = `Hello, world! Baggage: ${baggage ? JSON.stringify(baggage.getAllEntries()) : 'No Baggage'}`;
    res.send(message);

    span.end();
  });
});

app.get('/world', (req, res) => {
  const tracer = trace.getTracer('my-tracer');

  tracer.startActiveSpan('world-handler', (span) => {
    // 创建 Baggage
    let baggage = propagation.createBaggage({
      userId: { value: '123' },
      abTestGroup: { value: 'A' },
    });

    // 将 Baggage 注入到 Context 中
    const ctx = propagation.setBaggage(context.active(), baggage);

    // 将 Baggage 注入到 HTTP Header 中
    const headers = {};
    propagation.inject(ctx, headers, {
      set: (carrier, key, value) => {
        carrier[key] = value;
      },
    });

    // 模拟调用下游服务 (这里只是简单地打印 headers)
    console.log('Propagated Headers:', headers);

    const message = `World! Baggage created and propagated.`;
    res.send(message);
    span.end();
  });
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

4. 发送带有 Baggage 的请求

为了更清晰地演示,我们使用 curl 命令来发送带有 Baggage 的请求。

  • 首先请求 /world 创建并传递Baggage到headers中。
  • 然后,我们手动将headers信息添加到请求 /hello 中。
curl http://localhost:3000/world

# 输出类似:World! Baggage created and propagated.
# 控制台会输出携带 Baggage 的 headers 信息

# 然后,手动添加headers进行请求
curl -H "baggage: userId=123,abTestGroup=A" http://localhost:3000/hello

# 输出类似:Hello, world! Baggage: {"userId":{"value":"123"},"abTestGroup":{"value":"A"}}
# 控制台会输出 span 信息,其中包含 Baggage 信息

代码解释

  • /hello 接口:这个接口负责从 HTTP Header 中读取 Baggage,并将其添加到 Span 的 Attributes 中。 注意 propagation.getBaggage(context.active()) 用于获取当前上下文中的 Baggage。
  • /world 接口:这个接口负责创建 Baggage,并将其注入到 HTTP Header 中。 注意propagation.createBaggage创建Baggage,propagation.inject将Baggage注入到headers。

Baggage 的注意事项

  • Baggage 的大小: Baggage 会增加请求的大小,因此应该尽量控制 Baggage 的大小,只传递必要的信息。
  • Baggage 的安全性: Baggage 可以被中间人截获,因此不应该传递敏感信息。
  • Baggage 的兼容性: 不同的追踪系统可能对 Baggage 的格式有不同的要求,因此需要注意兼容性。
  • 命名规范: 建议使用有意义的 Baggage Key,并遵循统一的命名规范。例如:user.idab_test.group

Baggage 的格式

OpenTelemetry Baggage 采用 Key-Value 对的形式存储数据。Key 是一个字符串,Value 可以是一个字符串,也可以是一个包含更多信息的对象。

{
  "userId": {
    "value": "123",
    "metadata": {
      "source": "authentication-service"
    }
  },
  "abTestGroup": {
    "value": "A"
  }
}

Baggage Propagation 的常用方式

  • HTTP Header: 这是最常用的方式,简单易用,但会增加 HTTP 请求的大小。
  • 消息队列 Message Header: 适用于异步场景,例如 Kafka、RabbitMQ。
  • gRPC Metadata: 适用于 gRPC 服务。
  • Context Propagation: 适用于在同一个进程内的服务之间传递 Baggage。

Baggage 与 Correlation ID 的区别

  • Correlation ID: 用于关联不同的 Span,形成一个完整的追踪链。它是一个全局唯一的 ID。
  • Baggage: 用于传递自定义的数据,可以包含任何你想传递的信息。

Correlation ID 就像是追踪案件的编号,Baggage 就像是嫌疑人行李箱里的秘密标签。

Baggage 的最佳实践

  1. 只传递必要的信息: 避免传递过大的 Baggage,减少网络开销。
  2. 使用有意义的 Key: 方便理解和维护。
  3. 遵循统一的命名规范: 提高可读性和可维护性。
  4. 避免传递敏感信息: 确保安全性。
  5. 考虑兼容性: 选择通用的 Baggage 格式,避免不同系统之间的兼容性问题。
  6. 使用 OpenTelemetry 提供的 API: 简化 Baggage 的创建、注入和读取过程。

高级用法:Baggage 的动态更新

在某些场景下,我们可能需要在请求的处理过程中动态更新 Baggage。例如,当用户进行身份验证后,我们可以将用户ID添加到 Baggage 中。

const express = require('express');
const { trace, context, propagation } = require('@opentelemetry/api');

const app = express();
const port = 3000;

app.get('/auth', (req, res) => {
  const tracer = trace.getTracer('my-tracer');

  tracer.startActiveSpan('auth-handler', (span) => {
    // 模拟用户身份验证
    const userId = '456';

    // 从 HTTP Header 中获取 Baggage
    let baggage = propagation.getBaggage(context.active());

    // 如果 Baggage 不存在,则创建一个新的 Baggage
    if (!baggage) {
      baggage = propagation.createBaggage();
    }

    // 更新 Baggage
    baggage = baggage.set('userId', { value: userId });

    // 将 Baggage 注入到 Context 中
    const ctx = propagation.setBaggage(context.active(), baggage);

    // 将 Baggage 注入到 HTTP Header 中
    const headers = {};
    propagation.inject(ctx, headers, {
      set: (carrier, key, value) => {
        carrier[key] = value;
      },
    });

    // 模拟调用下游服务 (这里只是简单地打印 headers)
    console.log('Propagated Headers (After Auth):', headers);

    const message = `Authenticated! User ID added to Baggage.`;
    res.send(message);
    span.end();
  });
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

在这个例子中,/auth 接口模拟了用户身份验证的过程,并将用户ID添加到 Baggage 中。

总结

Baggage Propagation 是分布式追踪中一个强大的工具,可以帮助我们更好地理解系统的行为,提高可观测性,并实现更灵活的业务逻辑控制。但是,在使用 Baggage 的时候,我们需要注意 Baggage 的大小、安全性、兼容性以及命名规范。

Q&A 环节

好了,今天的讲座就到这里。大家有什么问题吗?

(等待观众提问,并耐心解答)

感谢大家的参与!希望今天的讲座对大家有所帮助。 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注