简介

Jaeger 是一款优秀的链路追踪工具(Tracing),一般来说,它真正发挥能力,是在多服务的串联的场景(如微服务),但即便是简单的单体应用,我们也可以借助它来进行服务监控和 debug,以典型的 web 请求为例,借助它,我们可以观察到

  • 请求参数和响应

  • 请求中涉及到的 DB / Redis / Http 等 IO 请求的时序和耗时

  • 两个请求(Trace)的比对

  • 简单的聚合

本文将介绍如何在 Laravel(8.0)中接入 Jaeger

三个简单概念

以一个单体应用的 HTTP 请求为例

Trace: 一个请求就是一个 trace

Span: 一个请求中的一个片段(DB / Redis..)可以构成一个 Span, 多个 Span 就会组成一个 Trace

Tag: 描述 Span ,如描述一个 DB Query Span 的 tag 可以有 sql 语句,执行时间等等

接入

Jaeger 提供了常见语言的客户端,但是需要注意的是有两种协议, opentracing 和 opentelemetry ,一般情况下两种协议不能互通(但是江湖传言有个 bridge)

首先部署服务端,使用官方提供的 all-in-one 是最方便的,官方地址 https://www.jaegertracing.io/docs/1.41/getting-started/#all-in-one

启动容器后,访问 16686 端口即可

注意到容器支持 UDP/TCP 两种通信协议,在实践中发现 UDP 协议会有包大小限制,如果链路过长导致上传失败可以选择 TCP 端口

然后进行客户端接入

需要指出的是,由于 Jaeger 需要监控每一次 IO 操作,那么天然就会对代码有侵入性,所以并不是任何工程都可以轻松的接入的,但是得益于 Laravel 优秀的设计,我们可以借助事件(Event)系统和依赖注入(DI)来实现

  • 首先引入 PHP 的 jaeger 客户端 composer require jonahgeorge/jaeger-client-php

  • 新建 App\Http\Middleware\JargerMiddleware, handle 方法如下

    public function handle($request, Closure $next)
    {
        $config = new Config(
            [
                'sampler' => [
                    'type' => Jaeger\SAMPLER_TYPE_CONST,
                    'param' => 1,
                ],
                'logging' => true,
                'local_agent' => [
                    'reporting_host' => env('JARGER_REPORTING_HOST', 'localhost'),
                    'reporting_port' => env('JARGER_REPORTING_PORT', 6832),
                ],
                'dispatch_mode' => Config::JAEGER_OVER_BINARY_UDP,
            ],
            'jingyao-' . config('app.env')
        );
        $config->initializeTracer();
        $tracer = GlobalTracer::get();
        $scope = $tracer->startActiveSpan('webHttp', []);
        $span = $scope->getSpan();
        $span->setTag('type', 'http');
        $span->setTag('request_method', $request->method());
        $span->setTag('request_path', $request->path());
        $span->setTag('request_uri', $request->getRequestUri());
        $span->setTag('request_ip', $request->ip());
        $response = $next($request);
        $scope->close();
        $tracer->flush();
        return $response;
    }
    

    将此中间件设置为全局生效,Trace 就从这里开始

  • 在 App\Providers\AppServiceProvider 中的 boot 方法添加代码,其实在哪个 Provider 都无所谓,只要在 boot 方法即可,此时核心类已经加载完成

    DB::listen(function ($query) {
      $tracer = GlobalTracer::get();
      $activeSpan = $tracer->getActiveSpan();
      $endTime = microtime(true);
      $span = $tracer->startSpan('db.query', [
          'start_time' => (int)(($endTime - $query->time / 1000) * 1000 * 1000),
          'child_of' => $activeSpan->getContext(),
      ]);
      $sql = $query->sql;
      if (! Arr::isAssoc($query->bindings)) {
          foreach ($query->bindings as $key => $value) {
              $sql = Str::replaceFirst('?', "'{$value}'", $sql);
          }
      }
      $span->setTag('db.sql', $sql);
      $span->setTag('db.query_time', $query->time);
      $span->finish();
      });
    

    这部分代码的功能是记录 sql 内容

  • 其实到这里就已经是一个相对完善的部分了,在简单的业务中已经足够使用了,但是我们可以继续深入,为 Redis 也添加监控,由于没有 Redis 的事件,所以我们自己来替换 Redis,当然你可以自己封装一个 Redis,但是如果是一个中途接入的工程,这显然不现实,那么我们的替换步骤如下

  • 新建一个 App\Replace 目录,其实目录名字随意啦

  • 新建 App\Replaces\Redis\ReplaceRedisManager , 继承 Illuminate\Redis\RedisManager,内容如下

      namespace App\Replaces\Redis;
    
      use Illuminate\Redis\Connectors\PredisConnector;
      use Illuminate\Redis\RedisManager;
    
      class ReplaceRedisManager extends RedisManager
      {
          /**
          * Get the connector instance for the current driver.
          *
          * @return \Illuminate\Contracts\Redis\Connector
          */
          protected function connector()
          {
              $customCreator = $this->customCreators[$this->driver] ?? null;
    
              if ($customCreator) {
                  return $customCreator();
              }
    
              switch ($this->driver) {
                  case 'predis':
                      return new PredisConnector();
                  case 'phpredis':
                      return new ReplacePhpRedisConnector();
              }
              throw new \Exception('Not found redis driver ' . $this->driver);
          }
      }
    
  • 新建 App\Replaces\Redis\ReplacePhpRedisConnector , 继承 Illuminate\Redis\Connectors\PhpRedisConnector,内容如下

      namespace App\Replaces\Redis;
    
      use Illuminate\Redis\Connectors\PhpRedisConnector;
      use Illuminate\Support\Arr;
    
      class ReplacePhpRedisConnector extends PhpRedisConnector
      {
          /**
          * Create a new clustered PhpRedis connection.
          *
          * @return \Illuminate\Redis\Connections\PhpRedisConnection
          */
          public function connect(array $config, array $options)
          {
              $connector = function () use ($config, $options) {
                  return $this->createClient(array_merge(
                      $config,
                      $options,
                      Arr::pull($config, 'options', [])
                  ));
              };
    
              return new ReplacePhpRedisConnection($connector(), $connector, $config);
          }
      }
    
  • 新建 App\Replaces\Redis\ReplacePhpRedisConnection 继承 Illuminate\Redis\Connections\PhpRedisConnection,内容如下

      class ReplacePhpRedisConnection extends PhpRedisConnection
      {
          public function __construct($client, callable $connector = null, array $config = [])
          {
              parent::__construct($client, $connector, $config);
          }
    
          public function get($key)
          {
              return $this->track(function () use ($key) {
                  return parent::get($key);
              }, __FUNCTION__, $key);
          }
    
          public function set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
          {
              return $this->track(function () use ($key, $value, $expireResolution, $expireTTL, $flag) {
                  return parent::set($key, $value, $expireResolution, $expireTTL, $flag);
              }, __FUNCTION__, $key);
          }
    
          private function track(callable $callback, string $type, string $key)
          {
              if (config('app.env') !== 'test') {
                  return $callback();
              }
              $tracer = GlobalTracer::get();
              $activeSpan = $tracer->getActiveSpan();
              $startTime = microtime(true);
              $res = $callback();
              $endTime = microtime(true);
              $span = $tracer->startSpan('redis.' . $type, [
                  'child_of' => $activeSpan->getContext(),
                  'start_time' => (int)($startTime * 1000 * 1000),
              ]);
              $span->setTag('redis.key', $key);
              $span->setTag('redis.client_time', $endTime - $startTime);
              $span->finish();
              return $res;
          }
      }
    

    这里只重写了 get 和 set 方法,可以将其他方法重写进来

  • 新建 App\Providers\ReplaceRedisServiceProvider ,内容如下

      namespace App\Providers;
    
      use App\Replaces\Redis\ReplaceRedisManager;
      use Illuminate\Contracts\Support\DeferrableProvider;
      use Illuminate\Support\Arr;
      use Illuminate\Support\ServiceProvider;
    
      class ReplaceRedisServiceProvider extends ServiceProvider implements DeferrableProvider
      {
          /**
          * Register the service provider.
          */
          public function register()
          {
              $this->app->singleton('redis', function ($app) {
                  $config = $app->make('config')->get('database.redis', []);
    
                  return new ReplaceRedisManager($app, Arr::pull($config, 'client', 'phpredis'), $config);
              });
    
              $this->app->bind('redis.connection', function ($app) {
                  return $app['redis']->connection();
              });
          }
    
          /**
          * Get the services provided by the provider.
          *
          * @return array
          */
          public function provides()
          {
              return ['redis', 'redis.connection'];
          }
      }
    
  • 替换 RedisServiceProvider

    • 将 config/app.php 中的 providers 数组的 Illuminate\Redis\RedisServiceProvider::class 注释掉,然后将 App\Providers\ReplaceRedisServiceProvider::class 注册进 providers ,到此就完成了

其他框架的实现

  • jonahgeorge/jaeger-client-php 是一个通用的 client 包,但是 Hyperf 框架做了更加完善的封装,与 Laravel 不同的是,其 Redis 等部分是基于 AOP 来完成的,对代码侵入性非常低,有兴趣可以阅读一下 hyperf/tracer 的代码