微服務監控

微服務監控主要分爲兩部分,一部分是對微服務自己的監控,另外一方面是對整個調用鏈的監控。目前,咱們主要採用dubbo做爲rpc框架,因此下面重點介紹dubbo監控。java

一、dubbo監控

1.一、原理

dubbo架構以下: node

經過閱讀dubbo源碼,全部的rpc方法調用都會通過MonitorFilter進行攔截,python

MonitorFilter.invoke()mysql

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);
        }
    }
複製代碼

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

MonitorFilter.collect()git

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對收集到的數據進行簡單統計,諸如成功次數,失敗次數,調用時間等,統計完後存儲數據到本地。github

DubboMonitor.collect()web

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有異步線程定時(默認每分鐘)將收集到數據發送到遠端監控服務。redis

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方法,而後將本地緩存數據置置零。sql

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.二、監控選型

主流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.三、部署

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

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

一、獲取源碼 github.com/dubboclub/d…

二、解壓下載下來的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
複製代碼

三、執行mongodb-dubbokeeper-server/bin/start-mongodb.sh啓動存儲端(數據存儲和web端是分開獨立部署的)

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

五、最後,打開瀏覽器,輸入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.四、主要功能介紹

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

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

ZooPeeper能夠查看zookeeper節點信息

Monitor能夠查看dubbo監控相關信息

應用總覽信息,可根據時間篩選:

應用詳細信息,有接口耗時、併發、失敗、成功等數據:

方法級別總覽及詳細信息:

1.五、遇到的坑

一、官方默認monitor.write.interval(存儲週期)配置的是6000,閱讀源碼發現單位是秒,也就是默認配置100分鐘纔會寫入mongodb,要把它改爲60。

二、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'
複製代碼

三、通常歷史數據基本不用保存過久,目前咱們線上保留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
複製代碼

四、mongodb緩存比較吃內存,最好配置8G以上的服務器,或者量大能夠考慮集羣部署

五、dubbokeeper-ui原生交互有點坑,有些頁面會遍歷展現全部應用的數據,效率比較低下。若是應用過多可能會超時打不開,服務端團隊對交互進行了簡單優化,每次只能查看一個應用或一個接口,若是你們有需求能夠留言,咱們後續會開源出來。

二、應用性能監控(APM)

2.一、主要目標

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

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

2.二、選型

方案 cat zipkin pinpoint skywalking
依賴 Java 6 7 八、Maven 3+ MySQL 5.6 5.七、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.三、原理

基於咱們的選型,重點關注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.四、部署

下面重點介紹pinpoint部署,目前咱們線上是集羣部署,總體架構以下:

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

搭建pinpoint線上用了三臺服務器,master、node一、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.五、功能簡介

首頁能看到應用的拓撲信息,接口調用的成功失敗數,響應時間等。

能夠查看具體的某一次請求的整個調用鏈路信息
能夠查看jvm相關信息
針對某個慢請求,咱們能夠經過pinpoint跟蹤整個調用鏈,從而定位慢在哪裏。

年糕媽媽--洛特

相關文章
相關標籤/搜索