PHP 怎么用OpenTelemetry

wen PHP项目 2

本文目录导读:

PHP 怎么用OpenTelemetry

  1. 为什么 PHP 需要 OpenTelemetry?——可观测性的痛点与救星
  2. OpenTelemetry 核心概念速览
  3. PHP 环境安装与扩展配置
  4. 手写第一个 Instrumentation:追踪一次 MySQL 查询
  5. 上下文传播:跨服务传递 Trace ID
  6. 导出数据到 Jaeger / Zipkin / 云端
  7. 实战问答:项目中的常见坑与性能优化
  8. 总结:从“能用”到“好用”的路线图


《PHP 可观测性实战:从零到一接入 OpenTelemetry 的完整指南》**


目录导读

  1. 为什么 PHP 需要 OpenTelemetry?——可观测性的痛点与救星
  2. OpenTelemetry 核心概念速览(Trace / Metric / Log)
  3. PHP 环境安装与扩展配置(Otlp + 自动注入)
  4. 手写第一个 Instrumentation:追踪一次 MySQL 查询
  5. 上下文传播:跨服务传递 Trace ID(HTTP Header)
  6. 导出数据到 Jaeger / Zipkin / 云端(gRPC vs HTTP)
  7. 实战问答:项目中的常见坑与性能优化
  8. 从“能用”到“好用”的路线图

开始**

为什么 PHP 需要 OpenTelemetry?——可观测性的痛点与救星

在现代微服务架构中,PHP 往往被贴上“胶水语言”或“传统 Web 脚本”的标签,但 Laravel、Symfony 等框架早已支撑起千万级 PV 的系统,当系统出现“某个接口慢 2 秒”时,传统日志只能告诉你“哪一行报错”,却无法告诉你“这一次请求在哪个服务、哪条 SQL、哪个 Redis 调用上耗费了时间”,这就是可观测性(Observability)要解决的问题。

OpenTelemetry(简称 OTel)是 CNCF 孵化项目,它统一了 Trace(链路追踪)、Metrics(指标)、Logs(日志)三大信号。对于 PHP 而言,OTel 解决了一个关键矛盾:语言生态的碎片化,以前你想接入 Zipkin 要用 Jaeger 客户端,想上报 Prometheus 又要写一个单独的 exporter,你只需要一套 SDK,通过 OTLP(OpenTelemetry Protocol)协议导出到任意后端。

OpenTelemetry 核心概念速览

在写代码之前,我们必须理解三个术语:

  • Trace:一次用户请求从入口到出口的全过程,由多个 Span 组成,Span 是带有开始/结束时间、名称、属性(Attributes)的最小工作单元,一个 GET /user 请求的 Trace 包含 controller Span、mysql_query Span、curl_external_api Span。
  • Context(上下文):Trace 的传播载体,包含 TraceIDSpanID,使用 W3C Trace Context 规范,通过 HTTP Header 传递。
  • SpanProcessor:负责在 Span 结束时处理它(如批处理、导出),PHP 是请求即生命周期的语言,注意进程内 Span 不能跨请求共享。

PHP 环境安装与扩展配置

你的 PHP 版本需要 4 以上(推荐 8.1+),我们使用官方推荐的 open-telemetry/opentelemetry-php 库,它依赖 gRPC 或 protobuf 扩展。

安装扩展(以 Ubuntu 为例)

# 安装 gRPC 和 protobuf 扩展
pecl install grpc protobuf
echo "extension=grpc.so" >> /etc/php/8.1/cli/conf.d/grpc.ini
echo "extension=protobuf.so" >> /etc/php/8.1/cli/conf.d/protobuf.ini
# 验证
php -m | grep grpc

Composer 安装 SDK

composer require open-telemetry/sdk open-telemetry/opentelemetry-auto-php

初始化 SDK(建议放在公共入口文件)

<?php
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
use OpenTelemetry\SDK\Trace\Exporter\Otlp\OtlpHttpExporter;
$exporter = new OtlpHttpExporter('http://collector:4318/v1/traces');
$spanProcessor = new SimpleSpanProcessor($exporter);
$tracerProvider = new TracerProvider($spanProcessor);
$tracer = $tracerProvider->getTracer('my-php-app');

手写第一个 Instrumentation:追踪一次 MySQL 查询

我们不用侵入业务代码,直接在数据库连接层包装,以下是使用 PDO 的示例:

class TracingPDO extends PDO
{
    private $tracer;
    private $span;
    public function __construct($dsn, $user, $pass, $tracer)
    {
        parent::__construct($dsn, $user, $pass);
        $this->tracer = $tracer;
    }
    public function query($query, ...$args)
    {
        $span = $this->tracer->spanBuilder('mysql_query')
            ->setSpanKind(SpanKind::KIND_CLIENT)
            ->setAttribute('db.system', 'mysql')
            ->setAttribute('db.statement', $query)
            ->startSpan();
        try {
            $result = parent::query($query, ...$args);
            $span->end();
            return $result;
        } catch (\Throwable $e) {
            $span->recordException($e);
            $span->setStatus(StatusCode::STATUS_ERROR);
            $span->end();
            throw $e;
        }
    }
}

关键点SpanKind::KIND_CLIENT 用于数据库/外部调用,可以标记为 “in-process” 或 “client” 类型,别忘了在 Try/Catch 中记录异常,否则 Trace 会“丢失”错误段。


上下文传播:跨服务传递 Trace ID

PHP 作为后端服务,接收前端或网关的请求时,必须从 HTTP Header 中提取 Trace 上下文,使用中间件更优雅:

// Laravel 中间件示例
use OpenTelemetry\Context\Context;
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
public function handle($request, Closure $next)
{
    $carrier = $request->headers->all();
    $context = TraceContextPropagator::getInstance()->extract($carrier);
    Context::storage()->attach($context); // 将上下文附加到当前执行流
    $response = $next($request);
    // 响应头也带上 traceparent,方便前端/网关对齐
    TraceContextPropagator::getInstance()->inject(
        $response->headers->all(),
        null,
        $response->header('traceparent')
    );
    Context::storage()->detach();
    return $response;
}

注意陷阱:PHP 进程模型下,若没有 Context::storage()->detach(),可能造成内存泄漏或上下文串号,特别是使用 Swoole 或 Workerman 常驻内存时,必须清理。


导出数据到 Jaeger / Zipkin / 云端

Otel 的牛逼之处在于“协议统一”,你不需要改业务代码,只需改变 Exporter 实例。

后端 协议/端点 推荐场景
Jaeger http://jaeger:4318/v1/traces 私有化部署,UI 强大
Zipkin http://zipkin:9411/api/v2/spans 传统 Java 团队共存
云端 https://api.honeycomb.io/v1/traces SaaS 免运维,支持高基数

性能调优:不要用 SimpleSpanProcessor(同步阻塞),改用 BatchSpanProcessor,它将 Span 缓存在内存,每 5 秒或 5000 条批量推送,在 PHP-FPM 场景下,务必在请求结束前调用 TracerProvider::forceFlush() 确保 Span 完整导出。

$spanProcessor = new BatchSpanProcessor($exporter, new Clock(), 5, 2048, 5000);

实战问答:项目中的常见坑与性能优化

Q1:开启 OTel 后,接口响应时间变慢 30%,怎么回事?
答:最常见的是同步导出阻塞,检查 Exporter 是否使用 HTTP/2 gRPC(比 HTTP/1.1 快),并改用异步 BatchSpanProcessor,检查你的 SQL 中是否大量使用 setAttribute 记录大对象(如长 SQL),这会序列化增加开销,建议只保留必要属性,大数据放到 eventslinks 中。

Q2:为什么在 Laravel 中,跨控制器的 Span 会丢失?
答:这是因为你在构造函数中创建了 Tracer,但 Span 生命周期未正确作用域化,确保每个请求都从 Context::storage() 获取当前 Span,而不是用全局变量,推荐使用 Tracer::spanBuilder() 在真正需要时创建新 Span,并设置父 Span 为 Context::storage()->current()

Q3:能不能只追踪错误请求,节省性能?
答:能,使用 Sampler 决策,ParentBased + TraceIdRatioBased(如只采样 10%),注意:这会导致低频故障可能被漏掉,建议在错误日志中显式增加 forceFlush 一个独立 Span 来补偿。

Q4:PHP-FPM 下的 Span 在请求结束后才 push,为什么 Jaeger 看不到?
答:因为 FPM 为每个请求创建新进程,进程退出时内存中的 Span 还没导出,解决方法有两个:一是注册 register_shutdown_function 调用 forceFlush;二是使用 swoole 常驻内存跑服务。


从“能用”到“好用”的路线图

第一步(本周):接入 opentelemetry-auto-php 自动探测库,它能自动为 Laravel/Symfony 生成框架级的 Trace(包括路由、控制器、数据库、HTTP 客户端),无需写一行业务代码,你就能在 Jaeger 看到完整的请求瀑布流。

第二步(本月):手动注入关键业务 Span(如“结算服务”“推送队列”),并使用 setAttribute 添加用户 ID、订单号等上下文信息。

第三步(长期):将 Metrics(如请求速率、DB 连接数)通过同一 Exporter 上报,实现在 Grafana 中统一看 Trace 和 Metrics。

最后提醒:可观测性不是“装了就完”,建议为每个 Trace 持续集成测试,在 CI 中跑一次请求并断言 TraceID 能正确生成且导出成功,避免升级依赖时静默失效。


本文所有域名均已替换为本地端口或通用占位符,实操时请替换为你的 Collector 真实地址。

抱歉,评论功能暂时关闭!