本文目录导读:

在PHP项目中实现分布式追踪,串联微服务调用链,通常需要解决两个核心问题:链路数据生成(在每一次HTTP/RPC调用中传递上下文) 和 数据采集与分析(将数据上报到后端)。
以下是具体的实现方案,从基础设施级别到代码级别逐步说明:
核心思路:Trace ID 与 Span ID 的传递
- Trace ID: 整个请求链路唯一标识。
- Span ID: 单个服务/操作(如一个API请求、一个数据库查询)的唯一标识。
- Parent Span ID: 标识当前Span的父级,用于构建调用链的父子关系。
串联的关键在于:当服务A调用服务B时,必须将当前上下文(Trace ID, Parent Span ID, Span ID)通过HTTP Header(或RPC框架的元数据)传递给服务B。
技术选型:开源工具
在PHP生态中,最主流且与云原生兼容的方案是 OpenTelemetry (OTel),其次是 Jaeger 和 Zipkin。
| 工具/协议 | 说明 | PHP支持情况 | 推荐度 |
|---|---|---|---|
| OpenTelemetry (OTel) | CNCF孵化项目。 当前事实标准,支持多种协议(gRPC/HTTP)和多种后端。 | 有官方PHP SDK (open-telemetry/opentelemetry-php)。 | ★★★★★ |
| Jaeger | 早期流行的分布式追踪系统,支持通过Thrift或gRPC上报。 | 有第三方库(jonahgeorge/jaeger-php)。 | ★★★★☆ |
| Zipkin | 由Twitter开源,基于HTTP传输。 | 有第三方库(jcchavezs/zipkin-php)。 | ★★★☆☆ |
| Datadog APM | SaaS服务。 | 有官方扩展(dd-trace-php)。 | ★★★☆☆ |
| SkyWalking | 国产优秀APM。 | 可以使用gRPC协议通过OTel兼容方式上报。 | ★★★☆☆ |
强烈建议采用 OpenTelemetry (OTel),因为它不绑定后端,可以方便地从Jaeger/Zipkin切换到其他。
具体实现步骤(以 OpenTelemetry 为例)
假设有三个微服务:Service-A (入口), Service-B (业务), Service-C (数据库/下游)。
安装 OpenTelemetry PHP SDK
composer require open-telemetry/opentelemetry composer require open-telemetry/transport-grpc composer require open-telemetry/exporter-otlp # 或使用 HTTP 传输 # composer require open-telemetry/transport-http
初始化 Tracer 与 Exporter (每个服务启动时执行)
通常放在框架的 bootstrap 或 middleware 的最外层。
<?php
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use OpenTelemetry\Contrib\Otlp\SpanExporter;
// 1. 配置 Exporter:将 Span 数据发送到 Collector 或 Jaeger
// 生产环境通常上报到:OpenTelemetry Collector(推荐) 或 Jaeger Agent
$transport = (new OtlpHttpTransportFactory())->create(‘http://otel-collector:4318/v1/traces’, ‘application/x-protobuf’);
$exporter = new SpanExporter($transport);
// 2. 创建 Resource(描述当前服务信息)
$resource = ResourceInfo::create(Attributes::create([
‘service.name’ => ‘service-a’, // 重要!区分服务
‘service.version’ => ‘1.0.0’,
]));
// 3. 建立 TracerProvider
$tracerProvider = new TracerProvider(
new BatchSpanProcessor($exporter),
$resource
);
// 4. 获取全局 Tracer
$tracer = $tracerProvider->getTracer(‘my-php-app’, ‘1.0.0’);
// 将 $tracerProvider 保存到全局容器或单例中,后续使用
关键:上下文 (Context) 的传播
这是串联调用链的核心机制。
A. 服务A(入口)或调用方:创建 Span 并注入 Header
在发起HTTP请求前,将Trace上下文注入到请求头中。
<?php
// 假设在 Service-A 的一个控制器方法中
public function handleRequest($request) {
// 1. 开启一个 Root Span
$span = $this->tracer->spanBuilder(‘/api/user/123’)
->setSpanKind(SpanKind::KIND_SERVER)
->startSpan();
// 2. 将当前 Span 设置为活跃上下文
$scope = $span->activate();
try {
// 3. 业务逻辑:调用 Service-B
$client = new GuzzleHttp\Client();
// ★★★ 关键步骤:准备请求头,并将当前 Trace 上下文注入 ★★★
$headers = [];
$propagator = (new \OpenTelemetry\API\Propagation\TraceContextPropagator());
$context = Context::getCurrent(); // 获取包含当前 Trace ID/Span ID 的上下文
$propagator->inject($headers, $context); // 将 Traceparent 等 Header 设置到 $headers
// 4. 调用下游服务
$response = $client->get(‘http://service-b:8080/api/data’, [
‘headers’ => $headers
]);
// 5. 记录业务 Span 事件
$span->addEvent(‘user.fetched’);
return $response->getBody();
} catch (\Exception $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
// 6. 关闭 Span 和 Scope
$scope->detach();
$span->end();
}
}
发给 Service-B 的 HTTP 请求头中包含了类似这样的信息:
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
B. 服务B:提取上下文并创建子 Span
在 Service-B 的入口处(通常是中间件或框架过滤器),需要提取上游传递过来的 Trace 上下文。
<?php
// Service-B 的中间件逻辑
public function process($request, $handler) {
// 1. 从请求头中提取 Trace 上下文
$propagator = new \OpenTelemetry\API\Propagation\TraceContextPropagator();
$context = $propagator->extract($request->getHeaders());
// $context 现在包含了 Service-A 的 Trace ID 和 Parent Span ID
// 2. 在提取出的上下文基础上,创建子 Span
$span = $this->tracer->spanBuilder(‘/api/data’)
->setParent($context) // ★★★ 将当前 Span 挂载到 Context 上 ★★★
->setSpanKind(SpanKind::KIND_SERVER)
->startSpan();
$scope = $span->activate();
try {
// 3. 继续执行业务逻辑(可能继续调用 Service-C)
// ... 同样使用 inject/...
return $handler->handle($request);
} finally {
$scope->detach();
$span->end();
}
}
处理异步任务(消息队列,Job)
对于消息队列(如RabbitMQ, Kafka),上下文无法通过Header传递,需要手动编码到消息体中:
- 生产者:发送消息前,将当前Trace上下文(
$propagator->inject())序列化(如JSON)为消息的一个字段(_trace_context)。 - 消费者:消费消息时,从消息体中提取该字段,然后用
$propagator->extract()重建上下文,再创建子Span。
架构部署:数据流
- PHP 应用 (Service-A, B, C) -> 使用 OpenTelemetry SDK -> 导出 Span 数据。
- 导出目标 -> 推荐使用 OpenTelemetry Collector(部署在K8s或Docker中)。
优势:数据缓冲、重试、过滤,并且能将数据转发给多个后端(Jaeger, Prometheus, ClickHouse等)。
- Collector -> Jaeger (可视化UI) 或 SigNoz / Grafana Tempo。
- Jaeger UI:最经典的分布式追踪查询界面,可以按Trace ID搜索,查看调用瀑布图。
常见问题与最佳实践
- SDK 性能问题:
- PHP是单线程阻塞模型,频繁的Span创建/结束会消耗CPU。
- 解决方案:使用 采样(Sampling),生产环境通常使用“基于请求头的采样”(Head-based sampling)或“概率采样”(如保留10%的Trace),OpenTelemetry支持
SamplerInterface。
- 如何串联 MySQL/Redis 调用?
- 不推荐在PHP层面手动插桩MySQL查询,因为性能损耗较大。
- 推荐方案:利用数据库代理层(如
ProxySQL或Vitess)或数据库自身的慢查询日志 + eBPF(如Pixie)来捕获调用,如果必须做,可以使用OpenTelemetry的mysqli/PDO自动插桩扩展(open-telemetry/opentelemetry-auto-mysqli)。
- 日志与追踪关联:
- 在日志中打印当前
Trace ID,方便排查问题时将日志和调用链关联起来。$traceId = $span->getContext()->getTraceId(); $logger->info(‘处理用户请求’, [‘trace_id’ => $traceId]);
- 在日志中打印当前
- 自动插桩(Auto-Instrumentation):
- 手动添加Span非常繁琐,可以考虑使用 OpenTelemetry 的 PHP 扩展(C 扩展,如
ext/otel)或第三方库(如signalfx/signalfx-tracing),它们能自动为 Guzzle、Laravel、Symfony、MySQL 创建 Span。 - 目前PHP的自动插桩不如Java/Js成熟,手动插桩仍是主流。
- 手动添加Span非常繁琐,可以考虑使用 OpenTelemetry 的 PHP 扩展(C 扩展,如
帮你选一个快速上手方案
如果你的项目是中小型,想快速跑通:
- 后端:用 Docker 启动一个 All-in-One 的 Jaeger。
- 依赖:安装
jcchhavezs/zipkin-php或open-telemetry/opentelemetry-php+ 导出到 Jaeger。 - 代码:参考上面手动插桩的代码,重点处理好
inject和extract。 - 框架:如果使用 Laravel,可以找
laravel-opentelemetry包,它会帮你处理HTTP中间件的自动注入和提取。
总结一句话:分布式追踪串联微服务的本质是 “在进程间传递Trace上下文”,PHP通过HTTP Header(traceparent) 完成这个动作,并由OpenTelemetry等工具将分散的Span链接成一个完整的调用树。