在异步编程场景中,一个业务请求往往会拆分成多个异步任务执行,这些任务可能运行在不同线程甚至不同进程上,要完整追踪整个请求的执行链路,就需要实现带上下文的异步操作追踪链,核心是通过Span和Trace的上下文传递来串联所有相关操作。

核心概念说明
首先需要明确几个基础概念:
- Trace:代表一个完整的端到端请求链路,整个链路共享同一个Trace ID。
- Span:代表链路中的一个独立操作单元,每个Span属于一个Trace,同时可以有父Span,形成层级关系。
- 上下文:包含当前Trace ID、父Span ID、当前Span ID等信息,需要在异步任务切换时跟随传递。
上下文结构设计
我们需要先定义一个上下文结构体,用来存储追踪所需的核心信息:
#include <string>
#include <memory>
#include <unordered_map>
// 追踪上下文结构体
struct TraceContext {
std::string trace_id; // 全局唯一的Trace标识
std::string span_id; // 当前Span标识
std::string parent_span_id; // 父Span标识,根Span为空
std::unordered_map<std::string, std::string> baggage; // 附加透传数据
// 生成新的子Span上下文
TraceContext create_child_span(const std::string& new_span_id) const {
TraceContext child;
child.trace_id = trace_id;
child.span_id = new_span_id;
child.parent_span_id = span_id;
child.baggage = baggage;
return child;
}
};
上下文的存储与获取
在同步场景中,我们可以通过线程局部变量存储当前上下文,异步任务切换时手动传递上下文即可。首先实现线程局部上下文的管理:
#include <thread>
// 线程局部变量存储当前追踪上下文
thread_local std::shared_ptr<TraceContext> current_trace_context = nullptr;
// 设置当前线程的追踪上下文
void set_trace_context(std::shared_ptr<TraceContext> ctx) {
current_trace_context = ctx;
}
// 获取当前线程的追踪上下文
std::shared_ptr<TraceContext> get_trace_context() {
return current_trace_context;
}
异步场景下的上下文传递实现
异步操作的核心问题是任务切换时上下文会丢失,因此需要在提交异步任务时,把当前上下文绑定到任务中,任务执行前恢复上下文。以下是一个简单的异步任务调度器示例:
#include <functional>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>
// 异步任务包装器,绑定追踪上下文
struct AsyncTask {
std::function<void()> task;
std::shared_ptr<TraceContext> context;
void run() {
// 执行前恢复上下文
set_trace_context(context);
// 执行任务
task();
// 任务执行后清空上下文(可选,避免上下文泄漏)
set_trace_context(nullptr);
}
};
// 简单线程池实现异步调度
class ThreadPool {
public:
ThreadPool(size_t thread_num) {
for (size_t i = 0; i < thread_num; ++i) {
workers_.emplace_back([this]() {
while (true) {
AsyncTask task;
{
std::unique_lock<std::mutex> lock(queue_mutex_);
condition_.wait(lock, [this]() { return !tasks_.empty() || stop_; });
if (stop_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task.run();
}
});
}
}
// 提交异步任务,自动绑定当前上下文
void submit(std::function<void()> task) {
auto ctx = get_trace_context();
{
std::unique_lock<std::mutex> lock(queue_mutex_);
tasks_.emplace(AsyncTask{std::move(task), ctx});
}
condition_.notify_one();
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex_);
stop_ = true;
}
condition_.notify_all();
for (auto& worker : workers_) {
if (worker.joinable()) worker.join();
}
}
private:
std::vector<std::thread> workers_;
std::queue<AsyncTask> tasks_;
std::mutex queue_mutex_;
std::condition_variable condition_;
bool stop_ = false;
};
完整使用示例
以下是一个完整的调用示例,展示如何创建Trace、生成Span、在异步任务中传递上下文:
#include <uuid/uuid.h> // 用于生成唯一ID,实际项目可替换为自己的ID生成逻辑
// 生成唯一ID的辅助函数
std::string generate_id() {
uuid_t uuid;
uuid_generate(uuid);
char buf[37];
uuid_unparse(uuid, buf);
return std::string(buf);
}
// 模拟一个异步操作,内部会创建子Span
void async_operation(const std::string& op_name) {
auto ctx = get_trace_context();
if (!ctx) return;
// 创建当前操作的子Span
auto child_ctx = ctx->create_child_span(generate_id());
std::cout << "操作: " << op_name << ", TraceID: " << child_ctx.trace_id
<< ", SpanID: " << child_ctx.span_id
<< ", 父SpanID: " << child_ctx.parent_span_id << std::endl;
// 设置子上下文为当前上下文
set_trace_context(std::make_shared<TraceContext>(child_ctx));
// 模拟操作执行
// 操作完成后上下文会自动恢复为父上下文(由AsyncTask的run逻辑保证)
}
int main() {
ThreadPool pool(2);
// 1. 创建根Trace和根Span
auto root_ctx = std::make_shared<TraceContext>();
root_ctx->trace_id = generate_id();
root_ctx->span_id = generate_id();
root_ctx->parent_span_id = "";
// 2. 设置根上下文为当前上下文
set_trace_context(root_ctx);
std::cout << "根操作, TraceID: " << root_ctx->trace_id << ", SpanID: " << root_ctx->span_id << std::endl;
// 3. 提交第一个异步任务
pool.submit([&]() {
async_operation("异步任务1");
// 异步任务内部再提交子任务
pool.submit([&]() {
async_operation("异步任务1的子任务");
});
});
// 4. 提交第二个异步任务
pool.submit([&]() {
async_operation("异步任务2");
});
// 等待所有任务执行完成
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
注意事项
- 如果项目需要兼容OpenTelemetry标准,上下文结构可以参考其官方定义的格式,扩展对应的字段。
- 如果使用第三方异步框架(如Boost.Asio、libuv),需要在框架的任务提交、回调触发点插入上下文的保存和恢复逻辑,原理和上述示例一致。
- 上下文中的baggage字段可以用来透传业务自定义数据,比如用户ID、请求来源等,这些数据会跟随整个Trace链路传递。
- 多线程场景下要注意上下文的生命周期,避免悬空指针,建议使用智能指针管理上下文对象。