微服務監控

年糕媽媽技術團隊發表於2018-11-20

微服務監控主要分為兩部分,一部分是對微服務本身的監控,另一方面是對整個呼叫鏈的監控。目前,我們主要採用dubbo作為rpc框架,所以下面重點介紹dubbo監控。

1、dubbo監控

1.1、原理

dubbo架構如下:

微服務監控

通過閱讀dubbo原始碼,所有的rpc方法呼叫都會經過MonitorFilter進行攔截,

MonitorFilter.invoke()

public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        if (invoker.getUrl().hasParameter("monitor")) {
            RpcContext context = RpcContext.getContext();
            long start = System.currentTimeMillis();
            this.getConcurrent(invoker, invocation).incrementAndGet();

            Result var7;
            try {
                Result result = invoker.invoke(invocation);
            this.collect(invoker, invocation, result, context, start, false);
                var7 = result;
            } catch (RpcException var11) {
                this.collect(invoker, invocation, (Result)null, context, start, true);
                throw var11;
            } finally {
                this.getConcurrent(invoker, invocation).decrementAndGet();
            }

            return var7;
        } else {
            return invoker.invoke(invocation);
        }
    }
複製程式碼

對於配置了監控的服務,會收集一些方法的基本統計資訊。

MonitorFilter.collect()

private void collect(Invoker<?> invoker, Invocation invocation, Result result, RpcContext context, long start, boolean error) {
        try {
            long elapsed = System.currentTimeMillis() - start;
            int concurrent = this.getConcurrent(invoker, invocation).get();
            String application = invoker.getUrl().getParameter("application");
            String service = invoker.getInterface().getName();
            String method = RpcUtils.getMethodName(invocation);
            URL url = invoker.getUrl().getUrlParameter("monitor");
            Monitor monitor = this.monitorFactory.getMonitor(url);
            int localPort;
            String remoteKey;
            String remoteValue;
            if ("consumer".equals(invoker.getUrl().getParameter("side"))) {
                context = RpcContext.getContext();
                localPort = 0;
                remoteKey = "provider";
                remoteValue = invoker.getUrl().getAddress();
            } else {
                localPort = invoker.getUrl().getPort();
                remoteKey = "consumer";
                remoteValue = context.getRemoteHost();
            }

            String input = "";
            String output = "";
            if (invocation.getAttachment("input") != null) {
                input = invocation.getAttachment("input");
            }

            if (result != null && result.getAttachment("output") != null) {
                output = result.getAttachment("output");
            }

            monitor.collect(new URL("count", NetUtils.getLocalHost(), localPort, service + "/" + method, new String[]{"application", application, "interface", service, "method", method, remoteKey, remoteValue, error ? "failure" : "success", "1", "elapsed", String.valueOf(elapsed), "concurrent", String.valueOf(concurrent), "input", input, "output", output}));
        } catch (Throwable var21) {
            logger.error("Failed to monitor count service " + invoker.getUrl() + ", cause: " + var21.getMessage(), var21);
        }
    }
複製程式碼

DubboMonitor對收集到的資料進行簡單統計,諸如成功次數,失敗次數,呼叫時間等,統計完後儲存資料到本地。

DubboMonitor.collect()

public void collect(URL url) {
        int success = url.getParameter("success", 0);
        int failure = url.getParameter("failure", 0);
        int input = url.getParameter("input", 0);
        int output = url.getParameter("output", 0);
        int elapsed = url.getParameter("elapsed", 0);
        int concurrent = url.getParameter("concurrent", 0);
        Statistics statistics = new Statistics(url);
        AtomicReference<long[]> reference = (AtomicReference)this.statisticsMap.get(statistics);
        if (reference == null) {
            this.statisticsMap.putIfAbsent(statistics, new AtomicReference());
            reference = (AtomicReference)this.statisticsMap.get(statistics);
        }

        long[] update = new long[10];

        long[] current;
        do {
            current = (long[])reference.get();
            if (current == null) {
                update[0] = (long)success;
                update[1] = (long)failure;
                update[2] = (long)input;
                update[3] = (long)output;
                update[4] = (long)elapsed;
                update[5] = (long)concurrent;
                update[6] = (long)input;
                update[7] = (long)output;
                update[8] = (long)elapsed;
                update[9] = (long)concurrent;
            } else {
                update[0] = current[0] + (long)success;
                update[1] = current[1] + (long)failure;
                update[2] = current[2] + (long)input;
                update[3] = current[3] + (long)output;
                update[4] = current[4] + (long)elapsed;
                update[5] = (current[5] + (long)concurrent) / 2L;
                update[6] = current[6] > (long)input ? current[6] : (long)input;
                update[7] = current[7] > (long)output ? current[7] : (long)output;
                update[8] = current[8] > (long)elapsed ? current[8] : (long)elapsed;
                update[9] = current[9] > (long)concurrent ? current[9] : (long)concurrent;
            }
        } while(!reference.compareAndSet(current, update));

    }
複製程式碼

DubboMonitor有非同步執行緒定時(預設每分鐘)將收集到資料傳送到遠端監控服務。

 public DubboMonitor(Invoker<MonitorService> monitorInvoker, MonitorService monitorService) {
        this.monitorInvoker = monitorInvoker;
        this.monitorService = monitorService;
        this.monitorInterval = (long)monitorInvoker.getUrl().getPositiveParameter("interval", 60000);
        this.sendFuture = this.scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
            public void run() {
                try {
                    DubboMonitor.this.send();
                } catch (Throwable var2) {
                    DubboMonitor.logger.error("Unexpected error occur at send statistic, cause: " + var2.getMessage(), var2);
                }

            }
        }, this.monitorInterval, this.monitorInterval, TimeUnit.MILLISECONDS);
    }
複製程式碼

呼叫遠端的MonitorService.collect方法,然後將本地快取資料置置零。

DubboMonitor.send()

 public void send() {
        if (logger.isInfoEnabled()) {
            logger.info("Send statistics to monitor " + this.getUrl());
        }

        String timestamp = String.valueOf(System.currentTimeMillis());
        Iterator i$ = this.statisticsMap.entrySet().iterator();

        while(i$.hasNext()) {
            Entry<Statistics, AtomicReference<long[]>> entry = (Entry)i$.next();
            Statistics statistics = (Statistics)entry.getKey();
            AtomicReference<long[]> reference = (AtomicReference)entry.getValue();
            long[] numbers = (long[])reference.get();
            long success = numbers[0];
            long failure = numbers[1];
            long input = numbers[2];
            long output = numbers[3];
            long elapsed = numbers[4];
            long concurrent = numbers[5];
            long maxInput = numbers[6];
            long maxOutput = numbers[7];
            long maxElapsed = numbers[8];
            long maxConcurrent = numbers[9];
            URL url = statistics.getUrl().addParameters(new String[]{"timestamp", timestamp, "success", String.valueOf(success), "failure", String.valueOf(failure), "input", String.valueOf(input), "output", String.valueOf(output), "elapsed", String.valueOf(elapsed), "concurrent", String.valueOf(concurrent), "max.input", String.valueOf(maxInput), "max.output", String.valueOf(maxOutput), "max.elapsed", String.valueOf(maxElapsed), "max.concurrent", String.valueOf(maxConcurrent)});
            this.monitorService.collect(url);
            long[] update = new long[10];

            while(true) {
                long[] current = (long[])reference.get();
                if (current == null) {
                    update[0] = 0L;
                    update[1] = 0L;
                    update[2] = 0L;
                    update[3] = 0L;
                    update[4] = 0L;
                    update[5] = 0L;
                } else {
                    update[0] = current[0] - success;
                    update[1] = current[1] - failure;
                    update[2] = current[2] - input;
                    update[3] = current[3] - output;
                    update[4] = current[4] - elapsed;
                    update[5] = current[5] - concurrent;
                }

                if (reference.compareAndSet(current, update)) {
                    break;
                }
            }
        }

    }

複製程式碼

dubbo監控的主流開源專案,都是實現了MonitorService介面來實現監控,區別無非就是資料儲存,報表統計邏輯的差異,基本原理都大同小異。

public interface MonitorService {
    String APPLICATION = "application";
    String INTERFACE = "interface";
    String METHOD = "method";
    String GROUP = "group";
    String VERSION = "version";
    String CONSUMER = "consumer";
    String PROVIDER = "provider";
    String TIMESTAMP = "timestamp";
    String SUCCESS = "success";
    String FAILURE = "failure";
    String INPUT = "input";
    String OUTPUT = "output";
    String ELAPSED = "elapsed";
    String CONCURRENT = "concurrent";
    String MAX_INPUT = "max.input";
    String MAX_OUTPUT = "max.output";
    String MAX_ELAPSED = "max.elapsed";
    String MAX_CONCURRENT = "max.concurrent";

    void collect(URL var1);

    List<URL> lookup(URL var1);
}
複製程式碼

1.2、監控選型

主流dubbo監控主要有:

  • dubbo-monitor
  • dubbo-d-monitor
  • dubbokeeper
  • dubbo-monitor-simple

下面進行簡單的對比:

方案 支援版本 基礎功能 開源作者 社群活躍度 資料儲存 維護成本
dubbo-monitor 基於dubbox,理論上也支援dubbo 一般,QPS、RT、服務狀態等,缺乏報表功能 韓都衣舍 513星,兩年前有提交 mysql、mongodb 無侵入、需要定期清理歷史資料
dubbo-d-monitor dubbo 一般,只有一些基礎資料 個人 189星,一年前有提交 mysql、redis(後續不再維護) 無侵入、需要定期清理歷史資料
dubbokeeper dubbo 豐富,除了基礎指標資料,有top200資料包表,還提供了類似dubbo-admin功能(限流、超時時間設定、消費客戶端設定、容錯等),同時支援zk節點視覺化 個人組織 989星,一個月內有提交 mysql、mongodb、lucene 無侵入、需要定期清理歷史記錄
dubbo-monitor-simple dubbo 簡陋 dubbo官方 330星,一個月內有提交 檔案儲存 無侵入、但目前線上使用發現資料量大了經常掛

對比以上幾種,dubbokeeper>dubbo-monitor>dubbo-d-monitor,所以選取dubbokeeper最為dubbo服務監控方案。

1.3、部署

我們採用mongodb儲存方案,採用單機部署。

環境:jdk1.8及以上(低版未測試),安裝tomcat,安裝zookeeper並啟動,安裝啟動mongodb

1、獲取原始碼 github.com/dubboclub/d…

2、解壓下載下來的zip包dubbokeeper-master到任意目錄,修改解壓後的專案中dubbo及資料庫的配置\dubbokeeper-master\conf\dubbo-mongodb.properties。

執行\dubbokeeper-master\install-mongodb.sh 執行完上一步後會生成一個target目錄,目錄下會存在以下三個資料夾及一個壓縮包

 archive-tmp
 mongodb-dubbokeeper-server
 mongodb-dubbokeeper-ui
 mongodb-dubbokeeper-server.tar.gz
複製程式碼

3、執行mongodb-dubbokeeper-server/bin/start-mongodb.sh啟動儲存端(資料儲存和web端是分開獨立部署的)

4、將mongodb-dubbokeeper-ui下的war包拷貝到tomcat的webapps目錄下,啟動tomcat。

5、最後,開啟瀏覽器,輸入http://localhost:8080/dubbokeeper-ui-1.0.1即可。

在業務程式碼中,只需要配置dubbo監控連線到註冊中心,就能完成監控資料採集。

<dubbo:monitor protocol="registry"/>
複製程式碼

主要的配置資訊:

dubbo.application.name=mongodb-monitor
dubbo.application.owner=bieber
dubbo.registry.address=zookeeper://*.*.*.*:2181?backup=*.*.*.*:2181,*.*.*.*:2181
dubbo.protocol.name=dubbo
dubbo.protocol.port=20884
dubbo.protocol.dubbo.payload=20971520

#dubbo資料採集週期 單位毫秒
monitor.collect.interval=60000

#use netty4
dubbo.provider.transporter=netty4

#dubbokeeper寫入mongodb週期 單位秒
monitor.write.interval=60

#mongdb配置
dubbo.monitor.mongodb.url=localhost
dubbo.monitor.mongodb.port=27017
dubbo.monitor.mongodb.dbname=dubbokeeper
dubbo.monitor.mongodb.username=
dubbo.monitor.mongodb.password=
dubbo.monitor.mongodb.storage.timeout=60000
複製程式碼

1.4、主要功能介紹

首頁能看到應用總體資訊(區分應用提供者和消費者),服務數量資訊,節點部署資訊及依賴關係圖等。

微服務監控
微服務監控
微服務監控

Admin提供了所有原生dubbo-admin的絕大部分功能。

微服務監控

ZooPeeper可以檢視zookeeper節點資訊

微服務監控

Monitor可以檢視dubbo監控相關資訊

應用總覽資訊,可根據時間篩選:

微服務監控

應用詳細資訊,有介面耗時、併發、失敗、成功等資料:

微服務監控

方法級別總覽及詳細資訊:

微服務監控

微服務監控

1.5、遇到的坑

1、官方預設monitor.write.interval(儲存週期)配置的是6000,閱讀原始碼發現單位是秒,也就是預設配置100分鐘才會寫入mongodb,要把它改成60。

2、dubbokeeper預設沒有對collections加索引,資料量大了之後開啟會異常慢,所以需要自己通過指令碼對collection加索引。

import pymongo
from pymongo import MongoClient
import time
import datetime
import sys
import os


client = MongoClient('127.0.0.1', 27017)
db = client['dubbokeeper']

collectionlist = db.collection_names()

for collection in collectionlist:
    if collection!='application':
        db[collection].ensure_index([("timestamp",pymongo.DESCENDING)])
        db[collection].ensure_index([("serviceInterface",pymongo.DESCENDING)])
        db[collection].ensure_index([("method",pymongo.DESCENDING)])
        db[collection].ensure_index([("serviceInterface",pymongo.DESCENDING),("method",pymongo.DESCENDING),("timestamp",pymongo.DESCENDING)])
        db[collection].ensure_index([("serviceInterface",pymongo.DESCENDING),("timestamp",pymongo.DESCENDING)])
        db[collection].ensure_index([("concurrent",pymongo.DESCENDING),("timestamp",pymongo.DESCENDING)])
        db[collection].ensure_index([("elapsed",pymongo.DESCENDING),("timestamp",pymongo.DESCENDING)])
        db[collection].ensure_index([("failureCount",pymongo.DESCENDING),("timestamp",pymongo.DESCENDING)])
        db[collection].ensure_index([("successCount",pymongo.DESCENDING),("timestamp",pymongo.DESCENDING)])
        db[collection].ensure_index([("serviceInterface",pymongo.DESCENDING),("elapsed",pymongo.DESCENDING),("timestamp",pymongo.DESCENDING)])
        db[collection].ensure_index([("serviceInterface",pymongo.DESCENDING),("concurrent",pymongo.DESCENDING),("timestamp",pymongo.DESCENDING)])
        db[collection].ensure_index([("serviceInterface",pymongo.DESCENDING),("failureCount",pymongo.DESCENDING),("timestamp",pymongo.DESCENDING)])
        db[collection].ensure_index([("serviceInterface",pymongo.DESCENDING),("successCount",pymongo.DESCENDING),("timestamp",pymongo.DESCENDING)])
        
print 'success'
複製程式碼

3、一般歷史資料基本不用儲存太久,目前我們線上保留2週資料,提供了以下指令碼定期刪除資料。

import pymongo
from pymongo import MongoClient
import time
import datetime
import sys
import os

day=int(sys.argv[1])
print day
timestamp = time.time()*1000-1000*24*3600*day

print timestamp

client = MongoClient('127.0.0.1', 27017)
db = client['dubbokeeper']

collectionlist = db.collection_names()

for collection in collectionlist:
    if collection!='application':
        db[collection].remove({"timestamp": {"$lt": timestamp}})

print 'clean mongodb data success'
複製程式碼

每天定時清理15天的資料

0 3 * * * python /home/monitor/shell/clean-mongodb.py 15
複製程式碼

4、mongodb快取比較吃記憶體,最好配置8G以上的伺服器,或者量大可以考慮叢集部署

5、dubbokeeper-ui原生互動有點坑,有些頁面會遍歷展示所有應用的資料,效率比較低下。如果應用過多可能會超時打不開,服務端團隊對互動進行了簡單優化,每次只能檢視一個應用或一個介面,如果大家有需求可以留言,我們後續會開源出來。

2、應用效能監控(APM)

2.1、主要目標

考慮接入應用效能監控主要想解決以下問題:

  • 分散式鏈路追蹤
  • 應用級別效能監控(jvm等)
  • 低侵入

2.2、選型

方案 cat zipkin pinpoint skywalking
依賴 Java 6 7 8、Maven 3+ MySQL 5.6 5.7、Linux 2.6+ hadoop可選 Java 6,7,8 Maven3.2+ rabbitMQ Java 6,7,8 maven3+ Hbase0.94+ Java 6,7,8 maven3.0+ nodejs zookeeper elasticsearch
實現方式 程式碼埋點(攔截器,註解,過濾器等) 攔截請求,傳送(HTTP,mq)資料至zipkin服務 java探針,位元組碼增強 java探針,位元組碼增強
儲存 mysql , hdfs in-memory , mysql , Cassandra , Elasticsearch HBase elasticsearch , H2
jvm監控 不支援 不支援 支援 支援
trace查詢 支援 支援 需要二次開發 支援
stars 5.5k 9.1k 6.5k 4k
侵入 高,需要埋點 高,需要開發
部署成本 較高

基於對應用盡可能的低侵入考慮,以上方案選型優先順序pinpoint>skywalking>zipkin>cat。

2.3、原理

基於我們的選型,重點關注pinpoint和skywalking。

2.3.1 google dapper 主流的分散式呼叫鏈跟蹤技術大都和google dapper相似。簡單介紹下dapper原理:

span 基本工作單元,一次鏈路呼叫(可以是RPC,DB等沒有特定的限制)建立一個span,通過一個64位ID標識它,uuid較為方便,span中還有其他的資料,例如描述資訊,時間戳,key-value對的(Annotation)tag資訊,parent_id等,其中parent-id可以表示span呼叫鏈路來源。

微服務監控

上圖說明了span在一次大的跟蹤過程中是什麼樣的。Dapper記錄了span名稱,以及每個span的ID和父ID,以重建在一次追蹤過程中不同span之間的關係。如果一個span沒有父ID被稱為root span。所有span都掛在一個特定的跟蹤上,也共用一個跟蹤id。 trace 類似於 樹結構的Span集合,表示一次完整的跟蹤,從請求到伺服器開始,伺服器返回response結束,跟蹤每次rpc呼叫的耗時,存在唯一標識trace_id。比如:你執行的分散式大資料儲存一次Trace就由你的一次請求組成。

微服務監控

每種顏色的note標註了一個span,一條鏈路通過TraceId唯一標識,Span標識發起的請求資訊。樹節點是整個架構的基本單元,而每一個節點又是對span的引用。節點之間的連線表示的span和它的父span直接的關係。

整體部署結構:

微服務監控

  • 通過AGENT生成呼叫鏈日誌。
  • 通過logstash採集日誌到kafka。
  • kafka負責提供資料給下游消費。
  • storm計算匯聚指標結果並落到es。
  • storm抽取trace資料並落到es,這是為了提供比較複雜的查詢。比如通過時間維度查詢呼叫鏈,可以很快查詢出所有符合的traceID,根據這些traceID再去 Hbase 查資料就快了。
  • logstash將kafka原始資料拉取到hbase中。hbase的rowkey為traceID,根據traceID查詢是很快的。

2.3.2 pinpoint

微服務監控

2.3.3 skywalking

微服務監控

以上幾種方案資料採集端都採用了位元組碼增強技術,原理如下:

微服務監控
在類載入的過程中,執行main方法前,會先執行premain方法來載入各種監控外掛,從而在執行時實現整個鏈路的監控。

2.4、部署

下面重點介紹pinpoint部署,目前我們線上是叢集部署,整體架構如下:

機器 部署應用
master zookeeper,hadoop,hbase,pinpoint-collector
node1 zookeeper,hadoop,hbase
node2 zookeeper,nginx,hadoop,hbase,pinpoint-web,pinpoint-collector

微服務監控

搭建pinpoint線上用了三臺伺服器,master、node1、node2。應用資料採集端agent-client將採集到的資料通過udp傳送到部署在node2的nginx,通過負載均衡分流到兩臺pinpoint-collector伺服器,落庫通過hadoop叢集master節點負載均衡到兩臺hbase伺服器上。

2.4.1 編譯

pinpoint編譯條件比較苛刻,需要jdk6,7,8環境。

2.4.2 hbase

叢集部署,需要先搭建hadoop叢集,hbase叢集。搭建完成後初始化表,執行 ./hbase shell /pinpoint-1.7.2/hbase/scripts/hbase-create.hbase,可以根據自己對歷史資料的需求設定表的ttl時間。

2.4.3 pinpoint-web

/pinpoint-1.7.2/web/target/pinpoint-web-1.7.2.war拷貝到tomcat webapps目錄下 修改tomcat目錄/webapps/pinpoint-web-1.7.2/WEB-INF/classes/hbase.properties hbase配置啟動

2.4.4 pinpoint-collector

/pinpoint-1.7.2/collector/target/pinpoint-collector-1.7.2.war拷貝到tomcat webapps目錄下,修改tomcat目錄/webapps/pinpoint-collector-1.7.2/WEB-INF/classes/hbase.properties和pinpoint-collector.properties配置並啟動

2.4.5 agent

將/pinpoint-1.7.2/agent整個目錄拷貝到應用伺服器指定目錄下修改/agent/target/pinpoint-agent-1.7.2/pinpoint.config配置業務應用啟動時增加引數-javaagent:/root/agent/target/pinpoint-agent-1.7.2/pinpoint-bootstrap-1.7.2.jar -Dpinpoint.agentId=application01 -Dpinpoint.applicationName=application

具體叢集部署可以參考: blog.csdn.net/yue530tomto…

需要注意: 預設配置的日誌級別是DEBUG,會產生海量日誌,要將其修改成INFO級別

2.5、功能簡介

首頁能看到應用的拓撲資訊,介面呼叫的成功失敗數,響應時間等。

微服務監控
可以檢視具體的某一次請求的整個呼叫鏈路資訊
微服務監控
可以檢視jvm相關資訊
微服務監控
針對某個慢請求,我們可以通過pinpoint跟蹤整個呼叫鏈,從而定位慢在哪裡。

年糕媽媽--洛特

相關文章