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閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,836評論 18 139
  • jaeger實現了opentracing的規范,對Tracer與Span等接口寫了自己的實現。根據opentrac...
    耳邊的火閱讀 7,985評論 3 8
  • 普元推出DevOps系列課程,5分鐘秒懂一個知識點,戳“閱讀原文”充電5分鐘,掌握黑科技。 轉載本文需注明出處:微...
    72a1f772fe47閱讀 4,538評論 0 0
  • 0 問題背景 隨著微服務架構的流行,服務按照不同的維度進行拆分,一次請求往往需要涉及到多個服務。互聯網應用構建在不...
    七寸知架構閱讀 39,369評論 8 91
  • 追求與眾不同,喜歡刺激、新鮮與挑戰。不甘于平庸,傾向于做一些常人不能理解的事,言語自然地有些特別。談不上行為怪誕,...
    Hi我是Lucy呀閱讀 131評論 0 0