dubbo+zipkin調用鏈監控

来源:http://www.cnblogs.com/ASPNET2008/archive/2017/04/14/6709900.html
-Advertisement-
Play Games

分散式環境下,對於線上出現問題往往比單體應用要複雜的多,原因是前端的一個請求可能對應後端多個系統的多個請求,錯綜複雜。 對於快速問題定位,我們一般希望是這樣的: 從下到下關鍵節點的日誌,入參,出差,異常等。 關鍵節點的響應時間 關鍵節點依賴關係 而這些需求原來在單體應用中可以比較容易實現,但到了分佈 ...


分散式環境下,對於線上出現問題往往比單體應用要複雜的多,原因是前端的一個請求可能對應後端多個系統的多個請求,錯綜複雜。
分散式調用

對於快速問題定位,我們一般希望是這樣的:

  • 從下到下關鍵節點的日誌,入參,出差,異常等。
  • 關鍵節點的響應時間
  • 關鍵節點依賴關係

而這些需求原來在單體應用中可以比較容易實現,但到了分散式環境,可能會出現:

  • 每個系統的技術棧不同
  • 有的系統有日誌有的連日誌都沒有
  • 日誌實現手段不相同

以上系統都是自治的,要想看整體的調用鏈非常困難。

分散式系統日誌統一的手段有很多,比如常見的ELK,但這些日誌都是文本,不太容易做分析。

更希望看到類似如下瀏覽器對於網路請求的分析:將分散的請求串聯在一起

瀏覽器網路

zipkin

這是推特的一個產品,通過API收集各系統的調用鏈信息然後做數據分析,展示調用鏈數據。
zipkin

核心功能:

  • 搜索調用鏈信息
    此處不多說,無非就是從存儲中按一定條件搜索請求信息。

zipkin預設是記憶體存儲,也可以是其它的比如:mysq,elasticsearch

  • 查看某條請求的詳細調用鏈

比如查詢產品明細,除了產品的基本信息還需要展示對產品的所有評論。下圖可以清晰的展示調用關係,product-dubbo-consumer調用product-dubbo-provider,product-dubbo-provider內部再調用comment-dubbo-provider。每步之間的時間也一目瞭然。

上面顯示的時間預設是指調用端發起遠程開始到從服務端接收到數據,其中包含網路連接以及數據傳輸的時間。

詳細請求調用鏈

  • 查看服務之間的依賴關係

互聯網項目目前微服務比較流行,微服務之間可能會存在迴圈引用形成一個網狀關係。當項目規模越來越大後,微服務之間的依賴關係估計誰也理不清,現在可以從請求鏈中清楚查看依賴。

詳細請求調用依賴

幾個關鍵概念

  • traceId
    就是一個全局的跟蹤ID,是跟蹤的入口點,根據需求來決定在哪生成traceId。比如一個http請求,首先入口是web應用,一般看完整的調用鏈這裡自然是traceId生成的起點,結束點在web請求返回點。

  • spanId
    這是下一層的請求跟蹤ID,這個也根據自己的需求,比如認為一次rpc,一次sql執行等都可以是一個span。一個traceId包含一個以上的spanId。

  • parentId
    上一次請求跟蹤ID,用來將前後的請求串聯起來。

  • cs
    客戶端發起請求的時間,比如dubbo調用端開始執行遠程調用之前。

  • cr
    客戶端收到處理完請求的時間。

  • ss
    服務端處理完邏輯的時間。

  • sr
    服務端收到調用端請求的時間。

客戶端調用時間=cr-cs
服務端處理時間=sr-ss

優化考慮

預設系統是通過http請求將數據發送到zipkin,如果系統的調用量比較大,需要考慮如下這些問題:

  • 網路傳輸
    如果一次請求內部包含多次遠程請求,那麼對應span生成的數據會相對較大,可以考慮壓縮之後再傳輸。

  • 阻塞
    調用鏈的功能只是輔助功能,不能影響現有業務系統(比如性能相比之前有下降,zipkin的穩定性影響現有業務等),所以在推送日誌時最好採用非同步+容錯方式進行。

  • 數據丟失
    如果日誌在後臺積壓,未處理完時伺服器出現重啟就會導致未來的急處理的日誌數據會丟失,儘管這種調用數據可以容忍,但如果想做到極致的話,也是有辦法的,比如用消息隊列做緩衝。

dubbo zipkin

由於工作中一直用dubbo這個rpc框架實現微服務,以前我們基本都是在kibana平臺上查詢各自服務的日誌然後分析,比較麻煩,特別是在分析性能瓶頸時。在dubbo中引入zipkin是非常方便的,因為無非就是寫filter,在請求處理前後發送日誌數據,讓zipkin生成調用鏈數據。

調用鏈跟蹤自動配置

由於我的項目環境是spring boot,所以附帶做一個調用鏈追蹤的自動配置。

  • 自動配置的註解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableTraceAutoConfigurationProperties {
}
  • 自動配置的實現,主要是將特定配置節點的值讀取到上下文對象中
@Configuration
@ConditionalOnBean(annotation = EnableTraceAutoConfigurationProperties.class)
@AutoConfigureAfter(SpringBootConfiguration.class)
@EnableConfigurationProperties(TraceConfig.class)
public class EnableTraceAutoConfiguration {

    @Autowired
    private TraceConfig traceConfig;

    @PostConstruct
    public void init() throws Exception {
        TraceContext.init(this.traceConfig);
    }
}
  • 配置類
@ConfigurationProperties(prefix = "dubbo.trace")
public class TraceConfig {

    private boolean enabled=true;

    private int connectTimeout;

    private int readTimeout;

    private int flushInterval=0;

    private boolean compressionEnabled=true;

    private String zipkinUrl;

    @Value("${server.port}")
    private int serverPort;

    @Value("${spring.application.name}")
    private String applicationName;

}
  • spring 配置
    按如下圖配置才能實現自動載入功能。
    spring自動配置

  • 啟動自動配置

最後在啟動類中增加@EnableTraceAutoConfigurationProperties即可顯示啟動。

追蹤上下文數據

因為一個請求內部會多次調用下級遠程服務,所以會共用traceId以及spanId等,設計一個TraceContext用來方便訪問這些共用數據。

這些上下文數據由於是請求級別,所以用ThreadLocal存儲

public class TraceContext extends AbstractContext {

    private static ThreadLocal<Long> TRACE_ID = new InheritableThreadLocal<>();

    private static ThreadLocal<Long> SPAN_ID = new InheritableThreadLocal<>();

    private static ThreadLocal<List<Span>> SPAN_LIST = new InheritableThreadLocal<>();

    public static final String TRACE_ID_KEY = "traceId";

    public static final String SPAN_ID_KEY = "spanId";

    public static final String ANNO_CS = "cs";

    public static final String ANNO_CR = "cr";

    public static final String ANNO_SR = "sr";

    public static final String ANNO_SS = "ss";

    private static TraceConfig traceConfig;


    public static void clear(){
        TRACE_ID.remove();
        SPAN_ID.remove();
        SPAN_LIST.remove();
    }

    public static void init(TraceConfig traceConfig) {
        setTraceConfig(traceConfig);
    }

    public static void start(){
        clear();
        SPAN_LIST.set(new ArrayList<Span>());
    }

}

zipkin日誌收集器

這裡直接使用http發送數據,詳細代碼就不貼了,核心功能就是將數據通過http傳送到zipkin,中間可以配合壓縮等優化手段。

日誌收集器代理

由於考慮到會擴展到多種日誌收集器,所以用代理做封裝。考慮到優化,可以結合線程池來非同步執行日誌發送,避免阻塞正常業務邏輯。

public class TraceAgent {
    private final AbstractSpanCollector collector;

    private final int THREAD_POOL_COUNT=5;

    private final ExecutorService executor =
            Executors.newFixedThreadPool(this.THREAD_POOL_COUNT, new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    Thread worker = new Thread(r);
                    worker.setName("TRACE-AGENT-WORKER");
                    worker.setDaemon(true);
                    return worker;
                }
            });

    public TraceAgent(String server) {

        SpanCollectorMetricsHandler metrics = new SimpleMetricsHandler();

        collector = HttpCollector.create(server, TraceContext.getTraceConfig(), metrics);
    }

    public void send(final List<Span> spans){
        if (spans != null && !spans.isEmpty()){
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    for (Span span : spans){
                        collector.collect(span);
                    }
                    collector.flush();
                }
            });
        }
    }
}

dubbo filter

上面做了那麼的功能,都是為filter實現準備的。使用filter機制基本上可以認為對現有系統是無侵入性的,當然如果公司項目都直接引用dubbo原生包多少有些麻煩,最好的做法是公司對dubbo做一層包裝,然後項目引用包裝之後的包,這樣就可以避免上面提到的問題,如此一來,調用端只涉及到修改配置文件。

  • 調用端filter
    調用端是調用鏈的入口,但需要判斷是第一次調用還是內部多次調用。如果是第一次調用那麼生成全新的traceId以及spanId。如果是內部多次調用,那麼需要從TraceContext中獲取traceId以及spanId。
private Span startTrace(Invoker<?> invoker, Invocation invocation) {

    Span consumerSpan = new Span();

    Long traceId=null;
    long id = IdUtils.get();
    consumerSpan.setId(id);
    if(null==TraceContext.getTraceId()){
        TraceContext.start();
        traceId=id;
    }
    else {
        traceId=TraceContext.getTraceId();
    }

    consumerSpan.setTrace_id(traceId);
    consumerSpan.setParent_id(TraceContext.getSpanId());
    consumerSpan.setName(TraceContext.getTraceConfig().getApplicationName());
    long timestamp = System.currentTimeMillis()*1000;
    consumerSpan.setTimestamp(timestamp);

    consumerSpan.addToAnnotations(
            Annotation.create(timestamp, TraceContext.ANNO_CS,
                    Endpoint.create(
                            TraceContext.getTraceConfig().getApplicationName(),
                            NetworkUtils.ip2Num(NetworkUtils.getSiteIp()),
                            TraceContext.getTraceConfig().getServerPort() )));

    Map<String, String> attaches = invocation.getAttachments();
    attaches.put(TraceContext.TRACE_ID_KEY, String.valueOf(consumerSpan.getTrace_id()));
    attaches.put(TraceContext.SPAN_ID_KEY, String.valueOf(consumerSpan.getId()));
    return consumerSpan;
}

private void endTrace(Span span, Stopwatch watch) {

    span.addToAnnotations(
            Annotation.create(System.currentTimeMillis()*1000, TraceContext.ANNO_CR,
                    Endpoint.create(
                            span.getName(),
                            NetworkUtils.ip2Num(NetworkUtils.getSiteIp()),
                            TraceContext.getTraceConfig().getServerPort())));

    span.setDuration(watch.stop().elapsed(TimeUnit.MICROSECONDS));
    TraceAgent traceAgent=new TraceAgent(TraceContext.getTraceConfig().getZipkinUrl());

    traceAgent.send(TraceContext.getSpans());

}

調用端需要通過Invocation的參數列表將生成的traceId以及spanId傳遞到下游系統中。

Map<String, String> attaches = invocation.getAttachments();
attaches.put(TraceContext.TRACE_ID_KEY, String.valueOf(consumerSpan.getTrace_id()));
attaches.put(TraceContext.SPAN_ID_KEY, String.valueOf(consumerSpan.getId()));
  • 服務端filter
    與調用端的邏輯類似,核心區別在於發送給zipkin的數據是服務端的。
private Span startTrace(Map<String, String> attaches) {

    Long traceId = Long.valueOf(attaches.get(TraceContext.TRACE_ID_KEY));
    Long parentSpanId = Long.valueOf(attaches.get(TraceContext.SPAN_ID_KEY));

    TraceContext.start();
    TraceContext.setTraceId(traceId);
    TraceContext.setSpanId(parentSpanId);

    Span providerSpan = new Span();

    long id = IdUtils.get();
    providerSpan.setId(id);
    providerSpan.setParent_id(parentSpanId);
    providerSpan.setTrace_id(traceId);
    providerSpan.setName(TraceContext.getTraceConfig().getApplicationName());
    long timestamp = System.currentTimeMillis()*1000;
    providerSpan.setTimestamp(timestamp);

    providerSpan.addToAnnotations(
            Annotation.create(timestamp, TraceContext.ANNO_SR,
                    Endpoint.create(
                            TraceContext.getTraceConfig().getApplicationName(),
                            NetworkUtils.ip2Num(NetworkUtils.getSiteIp()),
                            TraceContext.getTraceConfig().getServerPort() )));

    TraceContext.addSpan(providerSpan);
    return providerSpan;
}

private void endTrace(Span span, Stopwatch watch) {

    span.addToAnnotations(
            Annotation.create(System.currentTimeMillis()*1000, TraceContext.ANNO_SS,
                    Endpoint.create(
                            span.getName(),
                            NetworkUtils.ip2Num(NetworkUtils.getSiteIp()),
                            TraceContext.getTraceConfig().getServerPort())));

    span.setDuration(watch.stop().elapsed(TimeUnit.MICROSECONDS));
    TraceAgent traceAgent=new TraceAgent(TraceContext.getTraceConfig().getZipkinUrl());

    traceAgent.send(TraceContext.getSpans());

}

RPC之間的調用之所以能夠串起來,主要是通過dubbo的Invocation所攜帶的參數來傳遞

filter應用

  • 調用端
<dubbo:consumer filter="traceConsumerFilter"></dubbo:consumer>
  • 服務端
<dubbo:provider filter="traceProviderFilter" />

埋點

要想生成調用鏈的數據,就需要確認關鍵節點,不限於遠程調用,也有可能是本地的服務方法的調用,這就需要根據不同的需求來做埋點。

  • web 請求,通過filter機制,粗粒度。
  • rpc 請求,通過filter機制(一般rpc框架都有實現filter做擴展,如果沒有就只能自己實現),粗粒度。
  • 內部服務,通過AOP機制,一般結合註解,類似於Spring Cache的使用,細粒度。
  • 資料庫持久層,比如select,update這類,像mybatis都提供了攔截介面,與filter類似,細粒度。

代碼下載

https://github.com/jiangmin168168/jim-framework

引用

上面博主的思路還是很不錯的,不僅完成了基本功能也提到了需要註意的一些地方,我在此基本上按自己的方式做了一些調整。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 由於不能使用自帶的printf函數,也是哭阿,好了,直接講解題思路:題目說了可以活用setfill和setw控制符,那應該可以解決題目: 直接貼代碼: 沒有百度到解決方法,我也算是原創了。 ...
  • 第8章實踐項目之瘋狂填詞 創建一個一個瘋狂填詞(Mad Libs),程式,它將讀入文本文件,並讓用戶在該文本文件中出現 ADJECTIVE,NOUN,VERB等單詞的地方,加上他們自己的文本。 首先準備一個a.txt的文本文件 程式代碼如下: 輸出結果為: cat下b.txt OK 大功告成。 ...
  • 集合框架包含的內容: 集合框架的介面: List介面實現類 ArrayList LinkedList 迭代器Iterator 如何遍歷List集合? 1、通過for迴圈和get()方法配合實現遍歷 2、通過迭代器Iterator實現遍歷 所有集合介面和類都沒有提供相應遍歷的方法,而是由Iterato ...
  • DFS 1 #include<iostream> 2 #include<queue> 3 #include<cstdio> 4 using namespace std; 5 queue<int>q; 6 int map[1001][1001]; 7 int vis[1001]; 8 int n,m; ...
  • 特點: 安全,速度,併發 文件:hello_world.rs 代碼: 執行:rustc hello_world.rs 執行:./hello_world 結果:屏幕上就出現字元串:hello world ...
  • Docker Docker官網:https://cloud.docker.com/ 1. Docker 基礎用法和命令幫助 參考材料基礎: "Docker學習筆記" Docker手冊翻譯: "中文指南" 菜鳥教程: "Docker實例應用" 比較有深度的教程: "Docker入門教程" 1.1 常用 ...
  • 本文以及接下來的兩篇文章會討論一些性能優化相關的知識,分為上、中、下三個部分。第一部分討論性能分析的基礎內容,第二部分討論實際的性能分析、調優及測試,第三部分討論虛擬化環境和雲計算環境下的性能。文章內容來自於閱讀《圖解性能優化》一書的相關筆記和知識整理以及自己的理解。 轉在此處自己對於本書的豆瓣書評 ...
  • http://www.cnblogs.com/MOBIN/p/5597215.html 請先查看這邊博文 此文主要是在上篇博文的基礎之上,巨集觀的理一下思路,因為之前本人看了上篇之後雲里霧裡(是本人技術不到位)。然後研究了一會兒jdk1.8的源碼之後寫了此篇,不是很準確,源碼上篇博文之中已經很多了,我 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...