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