协程上下文变量传递必须用co::getcontext(),全局变量、静态属性或$_server会因协程切换导致数据丢失;每个协程拥有独立上下文容器,生命周期与协程一致,需手动透传至子协程,并在http入口从请求头提取trace_id初始化,跨服务调用时须手动注入请求头。

协程上下文变量传递必须用 Co::getContext()
直接用全局变量、静态属性或 $_SERVER 存 Trace ID 或用户 ID,协程切换后必然丢失——Swoole 的协程是用户态轻量调度,不共享运行时状态。唯一可靠方式是把数据塞进当前协程专属的上下文容器,也就是 Co::getContext() 返回的数组。
它本质是一个协程隔离的 key-value 存储,每个协程启动时自动初始化一个空数组,生命周期与协程一致,协程结束自动销毁,不会泄漏。
- 入口处写入:
Co::getContext()['trace_id'] = $id; - 任意子协程中读取:
$id = Co::getContext()['trace_id'] ?? null; - 注意:不能用引用赋值(如
&$ctx = Co::getContext()),因为每次调用都返回新数组副本
子协程不会自动继承父协程上下文
go() 启动的新协程默认拥有空上下文,父协程里写的 Co::getContext() 数据不会自动带过去。这是最容易踩的坑——你以为“父子”就自动传参,其实要手动透传。
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
- 正确做法:在
go()闭包外先取出上下文数据,再作为参数传入 - 错误示例:
go(function() { echo Co::getContext()['user_id']; }); // 输出 null - 正确示例:
$uid = Co::getContext()['user_id'] ?? 0; go(function($uid) { echo $uid; }, $uid); - 如果嵌套深、参数多,建议封装成工具函数,比如
co_with_context(fn() => {...}),内部自动提取并注入
HTTP Server 入口怎么安全初始化上下文
在 $server->handle() 回调里,是协程上下文的“起点”,但别急着往 Co::getContext() 写数据——得先判断是否已有上游透传的 trace-id 或 x-request-id,否则每次请求都生成新 ID,链路就断了。
- 优先从请求头读:
$traceId = $request->header['x-trace-id'] ?? uniqid('t-', true); - 再存入上下文:
Co::getContext()['trace_id'] = $traceId; - 注意:不要在
onStart或onWorkerStart里操作上下文——那些是进程级回调,没协程上下文 - 如果用了中间件(如 EasySwoole),确保中间件执行也在协程内,否则
Co::getContext()会报错或返回空
跨服务调用时上下文怎么透传
用 Swoole\Coroutine\Http\Client 发起下游 HTTP 请求时,上下文里的 trace_id 不会自动加到请求头,必须手动塞进去,否则链路在第一个 RPC 就断裂。
- 标准做法:读取本地
Co::getContext()['trace_id'],设置为下游请求头 - 示例:
$client->setHeaders(['x-trace-id' => $traceId]); - Redis/MySQL 客户端同理:在执行前从上下文取 ID,通过注释或自定义字段写入日志或慢查询上下文(如 MySQL 的
/* trace_id=xxx */ SELECT ...) - RPC 场景(如 Swoole TCP Client)需在协议体里预留字段,由序列化层统一注入,不能靠中间件拦截——因为中间件可能不在协程上下文中运行
协程上下文不是魔法,它只解决“同一个协程内数据可访问”的问题;父子协程隔离、跨服务无感透传、中间件执行时机错位,这三处最容易漏掉手动传递,一漏整个链路追踪就失效。










