php全链路监控完全实现(swoft举例)

  微服务架构现在越来越流行了,并且随着业务系统的不断变大臃肿,系统的拆分变得不可或缺,但随着系统逐渐服务化后,迎来的问题就变得多种多样了,本篇主要讲的就是当服务拆分后,如何对我们的系统进行全链路的监控,及时找到问题和瓶颈。
  谷歌的公开论文大规模分布式系统的跟踪系统Dapper,讲了一个分布式跟踪系统的实现流程,这个对我们之后的使用和学习非常有帮助,大家可以参阅。
  像Dapper一样,有许许多多的分布式跟踪系统应运而生,比如ZipkinPinpoint等等,但是这些系统都或多或少都存在相互不统一,而跟踪系统最重要的一点就是与语言、系统无关,而由于这些系统都有着不同的语法,使得各种语言的开发人员很难将其整合,这个时候,我们就需要一个统一的API来规范我们的系统,使得我们系统之间能够相互协调。
  OpenTracing就是为了统一我们的跟踪系统产生的,就像文中所说的,我们为什么需要OpenTracing,

openTracing

也就是说我们的分布式跟踪系统需要实现OpenTracing的api,这样就可以不区分语言地理解api的使用并且能更好应用到到我们的系统中。

跟踪系统的选择

目前实现OpenTracing的系统其实不少

  • zipkin, 由 Twitter 开发,并且支持大部分流行的语言,用官方的UI,社区强大,并且探针对业务系统影响较小,缺点则是需要手动设计代码,通过AOP或者中间件注入,并且是基于JSON传输的
  • jaeger, 由Uber开发,可以说基于zipkin之上开发出来的,传输协议更多样化,也支持大部分语言,zipkin-jaeger对比,但是我没有使用过,大家可以试试深刻感受下
  • lightstep,同样支持多语言,并且有更加复杂,展示更丰富的UI

并且在采用上我还参考了这篇文章全链路监控方案选择,我基于简单高效,文档健全完善文档的原则选择了zipkin(毕竟php是官方文档指定的,jaeger的php-client 的貌似还是第三方实现)。

2019年3月14日更新

目前我线上已经从zipkin迁移到jaeger,jaeger相比zipkin带来了更多的特性和简单,并且我在测试nginx的链路监控已经成功,目前我线上的组件已经替换为https://github.com/masixun71/swoft-jaeger

swoft是什么

这次举例我用的是php的swoft框架,目前也是公司采用的主要框架,swoft框架是一个基于swoole,不依赖fpm的框架,就像它官网描述的一样,

swoft

我大概是17年12月份开始关注这个框架的,也算是这个框架的老用户,而使用这个框架最重要的几点便是常驻内存,注解和协程。常驻内存带来的优点就是节省了sapi请求初始化和请求结束的时间,缺点则是内存泄漏。注解则是模仿java实现的,通过分析注释里的特定注解符实现,AOP也是基于此实现的。最最重要的一点则是协程,协程使得PHP程序并发能力呈百倍增长,我影响最深的一点就是之前线上8台机器使用fpm大概不到1000的qps并且fpm负载极高,采用了swoft之后1台机器大概就是4000左右qps,负载和内存都维护在一个很稳定的状态,所以这也就是我们直接把项目转到swoft的原因。

  swoft崇尚的是简单高效,我们的项目也是,所以swoft适合的就是小服务,微服务,api服务,不应该有太多包袱,所以当我们分割服务时,需要对swoft服务进行链路跟踪,当然,像我上面说的一样,我们的链路系统是不限平台和语言的,只是下面我以swoft系统举例实现。

swoft - zipkin-client 的实现 (给其他php框架提供参照)

看下面的文章需要先了解OpenTracing的API
本次使用的都是github上的swoft最新代码,swoft里面提供了中间件,我们可以直接通过中间件来实现Tracer的统计,

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $spanContext = GlobalTracer::get()->extract(
            TEXT_MAP,
            RequestContext::getRequest()->getSwooleRequest()->header
        );

        if ($spanContext instanceof SpanContext)
        {
            $span = GlobalTracer::get()->startSpan('server', ['child_of' => $spanContext]);
        }
        else
        {
            $rand = env('ZIPKIN_RAND');
            if (rand(0,100) > $rand)
            {
                return $handler->handle($request);
            }

            $span = GlobalTracer::get()->startSpan('server');
        }
        \Swoft::getBean(TracerManager::class)->setServerSpan($span);

        $response = $handler->handle($request);
        GlobalTracer::get()->inject($span->getContext(), TEXT_MAP,
            RequestContext::getRequest()->getSwooleRequest()->header);

        $span->finish();
        GlobalTracer::get()->flush();

        return $response;
    }

通过判断spanContext 是不是 SpanContext来区分是第一个系统还是被调用的子系统,rand则是设置采样率,TracerManager则是我们的一个全局Tracer的管理类,用来管理我们的Tracer和实现上传的配置,之后我会把代码链接放出来,可以看下实现。

  在http请求上,zipkin都是采用把spanId,traceId通过Header来传递的,所以我们需要给client端每次都默认传送这些header信息。

class AddZipkinAdapter extends CoroutineAdapter
{
    public function request(RequestInterface $request, array $options = []): HttpResultInterface
    {
        $options['_headers'] = array_merge($options['_headers'] ?? [], \Swoft::getBean(TracerManager::class)->getHeader());

        return parent::request($request, $options);
    }

}

我们采用的是继承原有的httpClient适配器,然后往上加你的个性化需求。

我们可以看一下效果,这个时候我们需要建立起zipkin的Server端,我们采用最快速的方式,

docker run -d -p 9411:9411 openzipkin/zipkin

默认采用的是数据存储到内存的方式,当然还有其他的方式,大家可以试试。
大致的结果就如下图


zipkin

zipkin2

大家发现只是记录的项目的互相调用信息,但是诸如http调用,mysql,redis这些调用是没有的,这是因为我们没有在这些调用前后记录信息,很蛋疼的是,虽然swoft源代码里有对mysql,http前后打统计标志,但是都没有设置钩子,所以我们需要修改一些库里面的一些代码。
  swoft是组件化的,所以在composer里面有各种各样的组件引入,但是其实它们都维护在一个项目里,swoft-component,因为提pr还有一定的审核时间(而且我担心😓不通过),所以我的建议是fork下来,建立自己的composer私有仓库,然后修改,并且swoft引用你的新项目,然后定期同步官方的更新,这样可以做到两不误。
  swoft虽然没有钩子,但是它提供了事件处理机制,通过触发响应的事件,然后利用监听者监听处理,这样就使得我们的业务代码和核心代码解耦了。
  下面我以httpClient举例来设置,我们需要在请求触发前和请求结束后设置事件,请求触发前大概在CoroutineAdapter.php里面,

        $path = $request->getUri()->getPath();
        $query = $request->getUri()->getQuery();
        if ($path === '') $path = '/';
        if ($query !== '') $path .= '?' . $query;

        $client->setDefer();
        App::trigger('HttpClient', 'start', $request, $options);
        $client->execute($path);
        App::profileEnd($profileKey);

我们在execute函数前面加了事件触发,并且传递了相应的信息,请求结束是在HttpCoResult,

        $client = $this->connection;
        $this->recv();
        $result = $client->body;
        $client->close();
        App::trigger('HttpClient', 'end');

接下来就是我们的监听者,当然这个函数就是触发监听,然后记录相应的信息到zipkin,并确定次序关系

/**
 * http request
 *
 * @Listener("HttpClient")
 */
class ZipkinHttpClientListener implements EventHandlerInterface
{

    protected $profiles = [];


    /**
     * @param EventInterface $event
     * @throws Exception
     */
    public function handle(EventInterface $event)
    {
        if (empty(\Swoft::getBean(TracerManager::class)->getServerSpan()))
        {
            return;
        }


        $cid = Coroutine::tid();
        if ($event->getTarget() == 'start') {
            /** @var Message\RequestInterface $request */
            $request = $event->getParams()[0];
            $options = $event->getParams()[1];
            $uri = $request->getUri();


            $tags = [
                'method' => $request->getMethod(),
                'host' => $uri->getHost(),
                'port' => $uri->getPort(),
                'path' => $uri->getPath(),
                'query' => $uri->getQuery(),
                'headers' => !empty($request->getHeaders()) ? json_encode($request->getHeaders()) : ''
            ];

            if ($request->getMethod() != 'GET')
            {
                $tags['body'] = $options['body'];
            }


            $this->profiles[$cid]['span'] = GlobalTracer::get()->startActiveSpan('httpRequest',
                [
                    'child_of' => \Swoft::getBean(TracerManager::class)->getServerSpan(),
                    'tags' => $tags
                ]);
        } else {
            $this->profiles[$cid]['span']->close();
        }

    }
}

最后完成这些实现后,我们可以看下效果图:


zipkin httpClient
zipkin httpClient2

可以看到我们记录了一次比较完整的调用,像mysql,redis也就类似,这里就不做细讲了,其实讲这个实现的目的就是为了大家了解怎么实现与业务相隔离的统计代码,各个项目都可以根据自己的框架特性来实现,其实并不复杂。

最后

我为swoft重写了我的zipkin-client的组件,可直接组件化到swoft的项目中去,稍微修改 swoft-component的几行代码就可以完美使用,下面是我的https://github.com/masixun71/swoft-zipkin swoft-zipkin-client组件,里面有详细的安装文档,包括上面讲的一些代码都在里面完全实现了,组件提供了mysql,redis,httpClient的监控数据支持。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • jaeger实现了opentracing的规范,对Tracer与Span等接口写了自己的实现。根据opentrac...
    耳边的火阅读 7,797评论 3 8
  • 普元推出DevOps系列课程,5分钟秒懂一个知识点,戳“阅读原文”充电5分钟,掌握黑科技。 转载本文需注明出处:微...
    72a1f772fe47阅读 4,383评论 0 0
  • 0 问题背景 随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建在不...
    七寸知架构阅读 38,918评论 8 91
  • 追求与众不同,喜欢刺激、新鲜与挑战。不甘于平庸,倾向于做一些常人不能理解的事,言语自然地有些特别。谈不上行为怪诞,...
    Hi我是Lucy呀阅读 116评论 0 0