??微服務架構現在越來越流行了,并且隨著業務系統的不斷變大臃腫,系統的拆分變得不可或缺,但隨著系統逐漸服務化后,迎來的問題就變得多種多樣了,本篇主要講的就是當服務拆分后,如何對我們的系統進行全鏈路的監控,及時找到問題和瓶頸。
??谷歌的公開論文大規模分布式系統的跟蹤系統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
默認采用的是數據存儲到內存的方式,當然還有其他的方式,大家可以試試。
大致的結果就如下圖
大家發現只是記錄的項目的互相調用信息,但是諸如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();
}
}
}
最后完成這些實現后,我們可以看下效果圖:
可以看到我們記錄了一次比較完整的調用,像mysql,redis也就類似,這里就不做細講了,其實講這個實現的目的就是為了大家了解怎么實現與業務相隔離的統計代碼,各個項目都可以根據自己的框架特性來實現,其實并不復雜。
最后
我為swoft重寫了我的zipkin-client的組件,可直接組件化到swoft的項目中去,稍微修改 swoft-component的幾行代碼就可以完美使用,下面是我的https://github.com/masixun71/swoft-zipkin swoft-zipkin-client組件,里面有詳細的安裝文檔,包括上面講的一些代碼都在里面完全實現了,組件提供了mysql,redis,httpClient的監控數據支持。