PHP `Tracing` (`OpenTelemetry`/`Jaeger`):分布式调用链追踪与可视化

各位观众老爷,晚上好!今天咱们聊聊PHP里那些“隐形的翅膀”——Tracing,也就是分布式调用链追踪,配合OpenTelemetry和Jaeger,让你的代码像开了天眼一样,哪里慢、哪里出错,一览无遗!

啥是Tracing?为啥要Tracing?

想象一下,你开发了一个电商网站,用户下单流程涉及用户服务、商品服务、订单服务、支付服务等等,每个服务都可能部署在不同的服务器上。一旦用户下单失败,你面对的是一堆日志,想要找到问题根源,简直像大海捞针!

Tracing就是来解决这个问题的。它可以记录一次请求在各个服务之间的调用路径、耗时,让你清晰地看到整个调用链,快速定位性能瓶颈和错误。

简单来说,Tracing就是给你一张调用流程图,告诉你请求“从哪里来,到哪里去,中间经历了什么”。

OpenTelemetry:追踪界的“瑞士军刀”

OpenTelemetry (简称OTel) 是一个开源的可观测性框架,提供了一套标准的API、SDK和工具,用于生成、收集、处理和导出遥测数据,包括 Traces (追踪)、Metrics (指标) 和 Logs (日志)。

你可以把它理解为追踪界的“瑞士军刀”,它定义了一套通用的标准,让你可以轻松地接入各种追踪系统,比如 Jaeger、Zipkin、SkyWalking等等。

Jaeger:追踪数据的“可视化大师”

Jaeger 是一个开源的分布式追踪系统,由 Uber 开源。它可以收集、存储和可视化追踪数据,让你通过Web界面查看调用链、分析性能瓶颈。

你可以把它理解为追踪数据的“可视化大师”,它能把OpenTelemetry收集到的数据,变成漂亮的调用链图表,让你一目了然。

PHP + OpenTelemetry + Jaeger:完美搭档

接下来,咱们就来看看如何在PHP项目中,使用OpenTelemetry和Jaeger来实现分布式调用链追踪。

1. 安装扩展和依赖

首先,你需要安装OpenTelemetry PHP扩展和Jaeger客户端。

pecl install opentelemetry
pecl install opentelemetry_exporter_jaeger

如果你的PHP版本比较新,可能还需要安装一些其他的依赖,具体可以参考OpenTelemetry PHP扩展的官方文档。

然后,使用Composer安装OpenTelemetry SDK:

composer require open-telemetry/sdk
composer require open-telemetry/exporter-jaeger

2. 初始化OpenTelemetry SDK

在你的PHP项目中,你需要初始化OpenTelemetry SDK,配置Jaeger exporter。

<?php

use OpenTelemetrySDKTraceTracerProviderFactory;
use OpenTelemetrySDKResourceResourceInfoFactory;
use OpenTelemetrySDKTraceSamplerAlwaysOnSampler;
use OpenTelemetrySDKTraceSpanProcessorSimpleSpanProcessor;
use OpenTelemetryContribOtlpGrpcExporter as OtlpExporter;
use OpenTelemetryContribJaegerExporter as JaegerExporter;
use OpenTelemetrySDKTraceTracerProvider;
use OpenTelemetryAPIGlobals;
use OpenTelemetryAPICommonAttributes;
use OpenTelemetrySDKResourceResourceInfo;
use OpenTelemetrySemConvResourceAttributes;
use OpenTelemetryAPITraceTracerInterface;

require 'vendor/autoload.php';

// 配置Jaeger exporter
$exporter = new JaegerExporter(
    'MyService', // 服务名称
    'localhost:6831' // Jaeger Agent 地址
);

// 配置采样器 (AlwaysOnSampler 意味着所有请求都会被采样)
$sampler = new AlwaysOnSampler();

// 创建Span处理器
$spanProcessor = new SimpleSpanProcessor($exporter);

// 创建资源信息
$resource = ResourceInfoFactory::create(
    ResourceInfo::create(new Attributes([
        ResourceAttributes::SERVICE_NAME => 'MyService',
        ResourceAttributes::SERVICE_VERSION => '1.0.0',
    ]))
);

// 创建TracerProvider
$tracerProvider = new TracerProviderFactory(
    [
        $spanProcessor
    ],
    $sampler,
    $resource
)->createTracerProvider();

// 获取Tracer
$tracer = $tracerProvider->getTracer('MyService', '1.0.0');

// 将Tracer设置为全局变量
Globals::registerTracerProvider($tracerProvider);

代码解释:

  • JaegerExporter:配置Jaeger exporter,指定服务名称和Jaeger Agent的地址。Jaeger Agent负责接收追踪数据,并将其发送到Jaeger Collector。
  • AlwaysOnSampler:配置采样器。AlwaysOnSampler意味着所有请求都会被采样,用于开发和调试环境。在生产环境中,可以考虑使用TraceIdRatioBasedSampler,根据一定的比例进行采样,减少追踪数据的量。
  • SimpleSpanProcessor:配置Span处理器。SimpleSpanProcessor会将Span立即发送到exporter。
  • TracerProvider:创建TracerProvider,它是追踪系统的核心组件,负责创建和管理Tracer。
  • Globals::registerTracerProvider():将TracerProvider设置为全局变量,方便在代码中使用。

3. 创建Span

在你的代码中,你需要手动创建Span,来记录请求的开始和结束。

<?php

use OpenTelemetryAPIGlobals;
use OpenTelemetryContextContext;

// 获取Tracer
$tracer = Globals::tracerProvider()->getTracer('MyService', '1.0.0');

// 创建根Span
$span = $tracer->spanBuilder('MyController::index')->startSpan();
$scope = $span->activate(); // 激活 Span

try {
    // 模拟一些业务逻辑
    echo "Hello, Tracing!n";
    sleep(1); // 模拟耗时操作

    // 创建子Span
    $childSpan = $tracer->spanBuilder('MyDatabase::query')->startSpan();
    $childScope = $childSpan->activate();

    try {
        // 模拟数据库查询
        echo "Querying database...n";
        sleep(2); // 模拟耗时操作
        $childSpan->setAttribute('db.statement', 'SELECT * FROM users WHERE id = 1'); // 添加属性
    } finally {
        $childSpan->end(); // 结束子Span
        $childScope->detach(); // Detach 子 Span
    }

    // 记录事件
    $span->addEvent('User logged in', ['user.id' => 123]);
} catch (Exception $e) {
    $span->setStatus(OpenTelemetryAPITraceStatusCode::STATUS_ERROR, $e->getMessage()); // 设置状态码为错误
    $span->recordException($e); // 记录异常
} finally {
    $span->end(); // 结束根Span
    $scope->detach(); // Detach 根 Span
}

代码解释:

  • $tracer->spanBuilder('MyController::index')->startSpan():创建一个Span,并设置Span的名称为MyController::index
  • $span->activate():激活Span,将其设置为当前上下文的活动Span。
  • $span->end():结束Span。
  • $span->setAttribute('db.statement', 'SELECT * FROM users WHERE id = 1'):添加属性,用于记录Span的额外信息,比如数据库查询语句。
  • $span->addEvent('User logged in', ['user.id' => 123]):记录事件,用于记录Span的关键事件,比如用户登录。
  • $span->setStatus(OpenTelemetryAPITraceStatusCode::STATUS_ERROR, $e->getMessage()):设置Span的状态码为错误,并记录错误信息。
  • $span->recordException($e):记录异常。

4. 运行代码,查看Jaeger UI

运行你的PHP代码,然后访问Jaeger UI (通常是 http://localhost:16686),你就可以看到调用链了!

5. 进阶技巧

  • Context Propagation:如果你的请求跨越多个服务,你需要使用Context Propagation来传递追踪信息。OpenTelemetry提供了一套标准的Context Propagation机制,可以将追踪信息注入到HTTP Header中,并在下游服务中提取出来。

    <?php
    
    use OpenTelemetryAPIGlobals;
    use OpenTelemetryContextContext;
    use OpenTelemetryAPIPropagationPropagationInterface;
    use OpenTelemetryAPIPropagationContextContextPropagatorInterface;
    use OpenTelemetryContextPropagationArrayAccessGetterSetter;
    
    // 获取Tracer
    $tracer = Globals::tracerProvider()->getTracer('MyService', '1.0.0');
    
    // 获取 TextMapPropagator (例如 W3C Trace Context)
    $propagator = Globals::propagator();
    
    // 创建根Span
    $span = $tracer->spanBuilder('MyService::callRemoteService')->startSpan();
    $scope = $span->activate();
    
    try {
        // 模拟调用远程服务
    
        // 准备 HTTP Header 数组
        $carrier = [];
    
        // 注入 Context 到 HTTP Header
        $propagator->inject($carrier, new ArrayAccessGetterSetter(), Context::getCurrent());
    
        // 打印 HTTP Header,用于传递给下游服务
        print_r($carrier);
    
        // 模拟下游服务接收 HTTP Header
        $remoteCarrier = $carrier;
    
        // 从 HTTP Header 中提取 Context
        $remoteContext = $propagator->extract($remoteCarrier, new ArrayAccessGetterSetter());
    
        // 创建下游服务的 Span,并设置为父 Span
        $remoteSpan = $tracer->spanBuilder('RemoteService::processRequest')
            ->setParent($remoteContext) // 设置父 Span
            ->startSpan();
        $remoteScope = $remoteSpan->activate();
    
        try {
            // 模拟远程服务处理请求
            echo "Remote service processing request...n";
            sleep(1);
        } finally {
            $remoteSpan->end();
            $remoteScope->detach();
        }
    
    } finally {
        $span->end();
        $scope->detach();
    }
    

    代码解释:

    • Globals::propagator():获取 TextMapPropagator,用于将Context注入到HTTP Header中,或者从HTTP Header中提取Context。OpenTelemetry默认使用W3C Trace Context propagator。
    • $propagator->inject($carrier, new ArrayAccessGetterSetter(), Context::getCurrent()):将当前Context注入到HTTP Header数组中。ArrayAccessGetterSetter用于访问和设置数组中的值。
    • $propagator->extract($remoteCarrier, new ArrayAccessGetterSetter()):从HTTP Header数组中提取Context。
    • $tracer->spanBuilder('RemoteService::processRequest')->setParent($remoteContext)->startSpan():创建下游服务的Span,并使用setParent()方法将父Span设置为从HTTP Header中提取的Context。
  • 自定义属性和事件:你可以使用setAttribute()addEvent()方法,为Span添加自定义属性和事件,用于记录Span的额外信息。

    <?php
    
    use OpenTelemetryAPIGlobals;
    
    // 获取Tracer
    $tracer = Globals::tracerProvider()->getTracer('MyService', '1.0.0');
    
    // 创建Span
    $span = $tracer->spanBuilder('MyFunction')->startSpan();
    $scope = $span->activate();
    
    try {
        // 添加自定义属性
        $span->setAttribute('user.id', 123);
        $span->setAttribute('product.id', 456);
    
        // 记录事件
        $span->addEvent('Product added to cart', ['quantity' => 2]);
    
        // 模拟一些业务逻辑
        echo "Processing...n";
        sleep(1);
    
    } finally {
        $span->end();
        $scope->detach();
    }
    
  • 使用Instrumentations:OpenTelemetry提供了一些Instrumentations,可以自动为你创建Span,无需手动编写代码。比如,你可以使用PDO Instrumentation来自动追踪数据库查询。

    <?php
    
    use OpenTelemetryContribInstrumentationPDOPDO;
    use OpenTelemetryAPIGlobals;
    
    // 获取Tracer
    $tracer = Globals::tracerProvider()->getTracer('MyService', '1.0.0');
    
    // 创建PDO连接
    $pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'password');
    
    // 执行查询
    $statement = $pdo->prepare('SELECT * FROM users WHERE id = ?');
    $statement->execute([1]);
    
    // 获取结果
    $result = $statement->fetchAll();
    
    // 打印结果
    print_r($result);
    

    注意: 你需要安装对应的Instrumentation:

    composer require open-telemetry/instrumentation-pdo

总结

Tracing是分布式系统中不可或缺的一部分,它可以帮助你快速定位性能瓶颈和错误,提高开发效率。OpenTelemetry和Jaeger是强大的追踪工具,可以让你轻松地实现分布式调用链追踪。

希望今天的讲座能帮助你更好地理解Tracing,并在你的PHP项目中应用起来!

一些小提示:

  • 在生产环境中,要合理配置采样率,避免追踪数据量过大。
  • 可以使用日志关联,将日志和追踪信息关联起来,方便排查问题。
  • 可以结合Metrics和Logs,构建全面的可观测性体系。

好了,今天的讲座就到这里,感谢大家的观看!有问题欢迎随时提问!

发表回复

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