微服務監控
微服務監控主要分為兩部分,一部分是對微服務本身的監控,另一方面是對整個呼叫鏈的監控。目前,我們主要採用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、獲取原始碼 ofollow,noindex">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+ SQL/">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級別
可以檢視具體的某一次請求的整個呼叫鏈路資訊可以檢視jvm相關資訊針對某個慢請求,我們可以通過pinpoint跟蹤整個呼叫鏈,從而定位慢在哪裡。