一、項目背景
2017年,vivo互聯網研發團隊認為調用鏈系統對實際業務具有較大的價值,於是開始了研發工作。3年的時間,調用鏈系統整體框架不斷演進……本文將介紹vivo調用鏈系統 Agent 技術原理及實踐經驗。
vivo調用鏈系統的研發,始於對 Google的《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》這篇經典文章的學習,我們調研了行業內相關的系統:鷹眼(EagleEye)、分布式服務跟蹤系統(SGM)、實時應用監控平台(CAT)、Zipkin、PinPoint、SkyWalking 、博睿等。通過研究分析,我們重點參考學習了 SkyWalking 的埋點方式。接下來我將逐步介紹Agent中用到的一些重點技術。
二、調用鏈入門
1、整體架構
為了方便讀者先有個整體的認知,我們先看下圖vivo當前調用鏈系統整體的架構,Agent 承擔了調用鏈數據的埋點及采集工作,當然這個是當前最新架構,相比項目之初有一些變化。

2、核心領域概念
調用鏈內部有兩個非常核心的概念,分別是trace和span,都源自最初google介紹dapper的文章,無論是國內大廠的調用鏈產品還是開源調用鏈的實現,領域模型一般都借鑒了這兩個概念,因此如果想很好理解調用鏈,這兩個概念首先需要有清晰的理解。

上圖模擬了一個簡單的場景:
一次請求從手機端發起,路由到后端后首先由nginx轉發給服務A來處理,服務A先從數據庫里查詢數據,簡單處理后繼續向服務B發起請求,服務B處理完成將結果返回給A,最終手機端成功接收到響應,整個過程是同步處理的。
結合上面模擬的場景,我給出定義:
Trace:相同業務邏輯的調用請求經過的分布式系統完整鏈路。
我們用traceId標志具體某一次請求調用,當然traceId是分布式唯一的,它串聯了整個鏈路,后文中會介紹traceId的生成規則。注意,相同業務邏輯的請求調用,可以理解為發起調用的入口是同一個接口。由於程序邏輯中存在if/else等分支結構,某一次調用不能完整反映出一個trace鏈路,只有相同業務邏輯的多次請求調用觸達的鏈路,合成后才算是一個完整的trace鏈路。
Span:某一次局部請求調用。
一次調用會產生多個span,這些span組成一個不完整的trace;span需要標注本次調用所在調用鏈路(即span數據中要有traceId信息),以及其所在鏈路中的層級;spanId同一層級原子自增,跨層級將拼接“.”以及子序列;例如上圖中span 1.1和1.2屬同一層級,span 1與1.1或者1.2是跨層級;B與D之間的通信是rpc調用,這個過程有4個步驟:B發起調用,接着D接收到請求,然后D將結果返回給B,然后B接收到D的響應。這4個步驟組成一個完整的span,所以B和D各只有這個span的一半,因此spanId需要跨進程傳遞,后面將介紹如何傳遞。
3、調用鏈中數據采集基本邏輯
vivo調用鏈系統的定位是服務層監控,是vivo互聯網監控體系中的重要一環。像服務異常、rpc調用耗時、慢sql等都是基本的監控點。如果埋點采集的數據需要滿足調用耗時監控,那么至少在rpc調用及慢sql監控場景下,將以AOP的形式來實現埋點數據采集。vivo調用鏈Agent除了JVM的指標采集直接使用了java.lang.management.ManagementFactory外,其他都是以類似AOP的形式來實現的。以下為偽代碼:
beginDataCollection(BizRequest req); try{ runBusiness();// 業務代碼執行 }catch(Throwable t){ recordBizRunError(Throwable t); throw t; }finally{ endDataCollection(BizResponse resp); }
三、基礎技術原理
調用鏈 Agent開發,涉及到了大量的技術點,以下挑一些關鍵的來簡單介紹。
1、分布式ID(traceId)的生成規則
調用鏈中的traceId扮演着非常重要的角色,在上面的章節中提到了它用於串聯多個進程間分散生成的span,除此之外,Agent端采樣控制、入口服務識別、后端flink關鍵指標計算、用戶查詢完整調用鏈路、全局業務日志串聯以及 Kafka、HBase和ES數據散列等都依賴於它。vivo 調用鏈系統traceId是長度為30的字符串,下圖中我對有特殊含義的分段進行了着色。

- 0e34:
16進制表示的Linux系統PID,用於單機多進程的區分,做到同一個機器不同的進程traceId不可能重復。
- c0a80001:
16進制的ipv4的表示,可以識別生成這個traceId的機器ip,比如127.0.0.1的16進制表示過程為127.0.0.1->127 0 0 1->7f 00 00 01。
- d:
代表着 vivo 內部的業務運行環境。一般我們會區分線下和線上環境,線下又可分開發、測試、壓測等等環境,而這個 d 代表着某個線上的環境。
- 1603075418361:
毫秒時間戳。用於增加唯一性,可通過此讀取入口請求發生的時間。
- 0001:
原子自增的ID,主要用於分布式ID增加唯一性,當前的設計可容忍單機每秒10000*1000=1千萬的並發。
2、全鏈路數據傳遞能力
全鏈路數據傳遞能力是 vivo 調用鏈系統功能完整性的基石,也是Agent最重要的基礎設施,前面提到過的spanId、traceId及鏈路標志等很多數據傳遞都依賴於全鏈路數據傳遞能力,系統開發中途由於調用鏈系統定位更加具體,當前無實際功能依賴於鏈路標志,本文將不做介紹。項目之初全鏈路數據傳遞能力,僅用於Agent內部數據跨線程及跨進程傳遞,當前已開放給業務方來使用了。
一般 Java 研發同學都知道 JDK 中的ThreadLocal工具類用於多線程場景下的數據安全隔離,並且使用較為頻繁,但是鮮有人使用過JDK 1.2即存在的InheritableThreadLocal,我也是從未使用過。
InheritableThreadLocal用於在通過new Thread()創建線程時將ThreadLocalMap中的數據拷貝到子線程中,但是我們一般較少直接使用new Thread()方法創建線程,取而代之的是JDK1.5提供的線程池ThreadPoolExecutor,而InheritableThreadLocal在線程池場景下就無能為力了。你可以想象下,一旦跨線程或者跨線程池了,traceId及spanId等等重要的數據就丟失不能往后傳遞,導致一次請求調用的鏈路斷開,不能通過traceId連起來,對調用鏈系統來說是多么沉重的打擊。因此這個問題必須解決。
其實跨進程的數據傳遞是容易的,比如http請求我們可以將數據放到http請求的header中,Dubbo 調用可以放到RpcContext中往后傳遞,MQ場景可以放到消息頭中。而跨線程池的數據傳遞是無法做到對業務代碼無侵入的,vivo調用鏈Agent是通過攔截ThreadPoolExecutor的加載,通過字節碼工具修改線程池ThreadPoolExecutor的字節碼來實現的,這個也是一般開源的調用鏈系統不具備的能力。
3、javaagent 介紹
在今年初,調用鏈在 vivo 互聯網業務中的接入率達94%之高,這個數據是值得自豪的,因為項目之初自我安慰的錯誤認知是調用鏈這種大數據系統無需服務於全部互聯網業務,或者當初認為服務於一些核心的業務系統即可。
個人認為能達到這么高的接入率,至少有兩個核心的底層邏輯:
- 之一是 Agent使用了javaagent技術,做到業務方無侵入無感知的接入;
- 之二是 Agent的穩定性得到了互聯網業務線的認可,從17年項目伊始到19年底只有過一次與之相關的業務端故障復盤。
然而一切並不是一開始就如此順利的,一開始 Agent 埋點模塊需要侵入業務邏輯,第一個版本對 SpringMVC 和 Dubbo進行了埋點,需要用戶在代碼中配置mvc filter和dubbo filter,效率極其低,對那個極力配合第一版試用的業務線的兄弟,現在依舊心懷感恩。后面我們就毅然決然換了javaagent方案,下面我介紹下javaagent技術。
javaagent是一個JVM參數,調用鏈通過這個參數實現類加載的攔截,修改對應類的字節碼,插入數據采集邏輯代碼。
開發javaagent應用需要掌握以下知識點:
- javaagent參數使用;
- 了解JDK的Instrumentation機制( premain方法、 ClassFileTransformer接口)及MANIFEST.MF文件中關於Premain-Class參數配置;
- 字節碼工具的使用;
- 類加載隔離技術原理及應用。
下面我逐個說明:
(1)javaagent配置示例如下:
java -javaagent:/test/path/my-agent.jar myApp.jar
此處javaagent參數配置的jar(這里是my-agent.jar)是由AppClassLoader來加載的,后續章節有介紹。
(2)所謂Instrumentation機制指的是通過jdk中java.lang.instrument.Instrumentation與java.lang.instrument.ClassFileTransformer這兩個接口協同進行類的字節碼替換,當然替換邏輯的入口在於攔截類的加載。Java的jar中有一個標准的配置文件META-INF/MANIFEST.MF,可以在文件中添加k-v配置。這里我們需要配置的k是Premain-Class,v是一個全限定名的Java類,這個Java類必須有一個方法是public static void premain(String agentOps,Instrumention instr)。這樣當你使用 Java命令啟動可執行jar時,就會執行到這個方法,我們需要在這個方法里完成字節碼轉換邏輯的注冊,當匹配到特定的類時,就會執行字節碼轉換邏輯,注入你的埋點邏輯。


(3)MANIFEST.M文件 中的配置

圖中Can-Retransform-Classes參數意為是否允許jvm執行轉換邏輯,可以閱讀Instrumentation這個類中的JavaDoc加深理解。Boot-Class-Path參數用於指定后面的jar中的類由BootstapClassLoader來加載。
(4)關於字節碼工具的使用,vivo調用鏈 Agent用到了以下操作:
- 修改指定方法的邏輯(嵌入類似AOP的邏輯);
- 給類增加實例字段;
- 讓類實現某個特定接口;
- 獲取類實例字段與靜態字段值、讀取父類與接口等等讀操作。
4、核心模型數據結構
在上文中我們講到span含義為一次局部調用,這次調用將分別在服務調用雙方產生半個span的數據,在內存中半個span的定義(17年底的定義)如下:
public class Span { final transient AtomicInteger nextId = new AtomicInteger(0);//用於同一層級的spanId自增 String traceId; String spanId; long start; long end; SpanKind type;//Client,Server,Consumer,Producer Component component;//DUBBO,HTTP,REDIS...... ResponseStatus status = ResponseStatus.SUCCESS; int size;//調用結果大小 Endpoint endpoint;//記錄ip、port、http接口、redis命令 List<Annotation> annotations;//記錄事件,比如sql、未捕獲異常、異常日志 Map<String, String> tags;//記錄標簽tag }
看了上面的定義你就能大致知道調用鏈的各個功能是如何計算出來的了。
5、各組件埋點詳情
這里我羅列了截止2019年底vivo調用鏈 Agent埋點覆蓋的組件,及埋點的具體位置。據了解,今年vivo調用鏈系統進入3.0版本后,新增了超過8個埋點組件,采集到的數據越來越豐富了。

6、半自動化埋點能力介紹

經過對埋點能力較深的封裝后,Agent中新增加一個組件的埋點是非常高效的,一般情況步驟如下,可以結合上圖來了解:
- 對需要埋點的第三方框架/組件核心邏輯執行流程進行debug,了解其執行過程,選定合適的aop邏輯切入點,切入點的選取要易於拿到span中各個字段的數據;
- 創建埋點切面類,繼承特定的父類,實現抽象方法,在方法中標注要切入埋點的方法,以及用於實現aop邏輯的interceptor;
- 實現interceptor邏輯,在openSpan方法中獲取部分數據,在closeSpan中完成剩余數據的獲取;
- 設置/控制interceptor邏輯所在類可以被Thread.currentThread().getContextClassLoader()這個類加載器加載到,然后打開此組件埋點邏輯生效的開關。
可見,當前新增一個組件的埋點是非常容易的,2018年2.0版本項目中期的目標是全自動化,期望通過配置即可實現部分類的自動生成,盡可能少的代碼,新增埋點更加高效,但是由於個人精力不足的原因,未能持續優化來實現。
7、span數據流圖
我們再來看下span從產生到發送到kafka的完整生命周期。

圖中可以看出,在生成完整的(closeSpan()完成調用)半個(參考調用鏈入門之核心領域概念小節)span后,會首先緩存在ThreadLocal空間。在完成本線程全部邏輯處理后,執行finish()轉儲到disruptor,再由disruptor的消費者線程定時刷到kafka的客戶端緩存,最終發送到kafka隊列。
在做內部分享的時候,這里有兩個問題有被問到,一是kafka客戶端自身有緩存,為啥中間還要有個disruptor,第二個是執行finish的時機。這里原因也很簡單,首先因為disruptor是無鎖不阻塞並且隊列容量可限定的,jdk中的線程安全的要么是阻塞的要么是無法限制初始容量的,kafka客戶端的緩沖區顯然也不滿足這個條件,我們決不可阻塞業務線程的執行。第二個問題用棧(LinkedList)這種數據結構來解決即可,線程執行到第一個埋點切點處執行openSpan時進行壓棧,執行closeSpan時執行彈棧,當棧中無數據時即應當執行finish。
8、豐富的內部治理策略

項目之初的主要目標是業務的接入量及產品能力的適用性,不會太多考慮內部治理,但是數據量大了后必然要更多的考慮自身的可治理性了,上圖中展示了截止2018年底Agent中的主要的內部治理能力。下面我逐個介紹下各項治理能力的背景。
(1)配置廣播:
置下發能力是其他各項治理能力的基石,Agent在premain方法執行時會去vivo配置中心主動拉取配置,如果配置中心配置有變動,也會主動將配置推送下來。另外,Agent內部依賴配置的地方眾多,內部配置的生效也是基於 JDK 中的Observer監聽機制實現配置分發的。
(2)日志策略:
在2017年的時候,vivo互聯網業務方興未艾,統一日志中心的能力較弱,大量的異常日志會對日志中心造成沖擊,因此需要做異常流控。在異常情況下減少異常堆棧的打印,並且Agent還要能響應業務的需求采集指定級別的業務日志,比如由於日志打印規范不明,日志打印混亂的原因,有業務希望將warn或者某個類的info級別的日志,采集到調用鏈系統中供問題排查。另外,Agent自身是需要打印日志的,這個日志打印的代碼在字節碼增強后是嵌入到三方框架中的,也就是說業務邏輯執行到三方框架中時可能造成執行變慢,影響業務性能,因此需要異步輸出日志。最后需要提到的一點是,日志的打印在Agent中是自己實現的,為了避免與業務方使用的日志框架造成類沖突,是不能使用第三方日志框架的。
(3)采樣策略:
在2018年初,接入不到200個服務時,采集的span數據已經占據了10台 Kafka 物理機的容量了,必須進行流量控制,采樣是重點。但是當初的采樣邏輯會帶來新的問題,就是導致業務tps不精准,因此后面將tps等數據獨立進行采集了。
(4)降級:
這個容易理解,就是要支持動態控制不采集某個服務的數據,或者不采集某個組件的數據,或者業務方希望在活動的時候關閉調用鏈。
(5)異常流控:
調用鏈對日志組件進行了埋點,也能攔截到業務方未捕獲的異常,會將這些數據采集並存儲到調用鏈系統中,如果太多異常了,系統自身也撐不住,因此這里的異常流控指以一定頻率控制相同異常不傳遞到后端。
(6)全流程span流轉監控:
Agent中會監控span的流轉過程進行計數(產生、入隊、出隊、入Kafka成功/失敗、數據丟失),當發現數據丟失時,可選擇調大內存無鎖隊列的容量或者調小Kafka發送間隔,當發現發送 Kafka失敗時,意味着網絡或者kafka隊列出了問題。
(7)數據聚合頻率控制:
在18年中,據評估span原始數據后期將會增長到每天1500億條,調用鏈系統無足夠資源處理這么大規模數據量,因此我們很快在Agent端實現了端的數據聚合能力,將初步聚合后的數據丟給flink做最終的計算,減少Kafka和大數據集群的壓力。
(8)JVM采樣和kafka發送頻率控制:
Agent會定時采集JVM指標,比如gc、cpu、JVM 使用內存、各狀態線程數等等,在經過flink計算后會在頁面顯示出折線圖,這個采集間隔是嚴格的5s,為了控制數據量,需要做到動態調控采集間隔。另外Agent端生成span數據首先緩存到了內存無鎖隊列,然后定時批量發送Kafka,為了兼顧告警的實時性及Agent端的cpu的損耗,這個頻率默認是200ms,同時也支持遠程調控。
四、Agent穩定性保障
上文提到過,當前Agent在幾千個應用中接入率達94%之高,個人認為有一個重要原因是其穩定性被業務方認可。那么如果要保障自身的穩定性,不對業務造成影響,對於調用鏈Agent來說,首先一定要盡可能的減少對業務線程執行的干擾,其次要盡可能多的考慮到邊界問題
1、全程不阻塞業務流程
減少對業務線程執行干擾的出發點在於不阻塞業務線程,我們來梳理下對業務線程的阻塞點,然后逐個介紹下處理辦法。
(1)線程阻塞點1——日志打印:
disruptor處理。使用disruptor對日志進行無阻塞緩存,同時堅持令可直接丟棄日志也不要阻塞的原則。
(2)線程阻塞點2——埋點邏輯:
- 措施1:span生成時緩存到ThreadLocal中,高效批量轉儲到disruptor,避免多次的disruptor生產者屏障的競爭;
- 措施2:埋點過程中必不可少的會使用到反射,但是反射是有坑的(點擊此處了解),分析反射邏輯的源碼,矯正反射使用的姿勢;
- 措施3:可能的話不要使用反射,而是通過字節碼技術讓埋點類實現自定義特定接口,通過執行正常的方法調用來獲取對象實例數據。
- 措施4:在ThreadLocal與disruptor,及disruptor與Kafka的數據轉儲過程中,池化大的集合對象,避免過多的大內存的消耗。
(3)線程阻塞點3——span數據發送 :
同樣,使用disruptor來解決線程阻塞的問題。
2、健壯性
邊界問題的考慮及解決是極大依賴開發人員的個人經驗及技術能力的,下面我列了幾個重點的問題,也是業務方擔憂較多的問題。
(1)如果Agent自身邏輯有問題怎么辦?
全程try-catch、自身異常的話相同異常日志2分鍾內只打印一條。
(2)如果無法及時避免阻塞業務線程怎么辦?
降級,直接退出單次埋點流程。
(3)如果業務太繁忙cpu消耗大怎么辦?
- 采樣控制+頻率控制+降級;
- 直接丟棄數據;
- 自定義disruptor的消費者等待策略,在高性能與高消耗之間做平衡。
(4)如果消耗過多內存怎么辦?
嚴格對內存數據對象進行計數限制;
數據流轉過程中難以控制的大內存消耗點使用SoftReference。
(5)如果Kafka連不上/斷連怎么辦?
支持降級的同時,可選啟動連不上直接退出Agent阻止程序啟動,運行時斷連直接丟棄數據。
五、難點技術及關鍵實現簡介
下面會簡單介紹下Agent中的一些關鍵的難點技術。其中最為難以掌控的是Agent中的類需要控制被哪個類加載器來加載,不然你一定會痛苦的面對各種ClassNotFoundException的。
1、啟動流程

Agent啟動流程看起來是簡單的,這里貼出來可以方便內部的同學閱讀源碼。需要注意的是啟動伊始是以premain方法作為入口,這個方法所在類由AppClasssLoader來加載。啟動流程中需要控制好Agent中的哪些類或者模塊由哪個類加載器來加載,並且部分類是通過自定義類加載器來主動加載的,不同的類加載器邏輯執行空間的銜接,是通過jdk中的代理模式(InvocationHandler)來解決的,后面會做介紹。
2、微內核應用架構
Agent的主要職責是埋點和數據采集,埋點理當是整個Agent中最為核心的邏輯,以下簡單介紹下圍繞核心的各個功能塊功能,圖中除了類隔離功能外,其他功能塊都是可以直接去掉而不影響其他模塊的功能,遵循了微內核應用架構模式。

日志:自定義實現
- 適配環境,不同環境不同行為;
- 適配slf4j;
- 日志級別動態可控;
- 自動識別相同error日志,避免沖擊日志中心。
監控:可靠性的基石
- 監控埋點數據完整的生命周期(產生、入隊、出隊、入kafka成功/失敗、內存隊列消耗狀況、數據丟失情況);
- 監控jvm采樣延時狀況。
策略控制功能塊:
- 基於觀察者模式廣播配置變更事件;
- 控制着采樣、日志級別、業務日志攔截級別、降級、異常流控、監控頻率、jvm打點頻率、數據聚合。
字節碼轉換控制功能塊:
- 組件增強插件化(可配置);
- 增強邏輯之間相互隔離;
- 增強邏輯高度封裝,實現配置化;
- 核心流程模仿spring類的繼承體系,具備強可擴展性。
流程控制功能塊:
- 應用內部高度模塊化;
- SPI機制高可擴展。
類隔離控制單元:
- 自定義多個類加載器,加載不同位置及jar中的類;
- 兼容 Tomcat 和 JDK 的類加載器的繼承關系,主動讓 Tomcat或者 JDK 中特定類加載器顯式加載類;
- 干擾類的雙親委派模型,控制特定類的父類或者接口的加載。
3、核心技術棧

圖中箭頭的方向,意為由上而下的技術使用難度的增大,同時需要用來研究及調優的時間消耗也增加。其中 Java 探針技術即是上文中介紹的javaagent,ByteBuddy的選型報告及背景在下文中有介紹,disruptor主要是需要花費較多時間進行技術背景理解、源碼閱讀及調優,后文也有介紹,而類加載控制的應用,是項目之初最為頭疼的難點,猶記得17年底處理ClassNotFoundException時的絕望,遠遠不是了解如何自定義類加載器及雙親委派這些知識能解決的。當初買了好幾本有相關知識介紹的書籍來研究,哪怕是在這本書的目錄中僅僅發現了可能不到1頁的並且也只是可能相關的篇幅,買書投入都花了好幾百塊。
4、類加載及隔離控制
需要注意的是,類加載隔離的控制目標是自己用到的三方包不與業務方的三方包因版本產生沖突,並且保證Agent中邏輯執行時不出現找不到類的問題,這里簡單畫了Agent中的類加載隔離情況,可以結合上面的小節來簡單理解。

這里我嘗試羅列需要掌握的知識點:
- 類加載的4大時機;
- premain所在類的加載及執行邏輯;
- JDK 的雙親委派模型,及如何實現自定義加載器,如何更改加載順序;
- JDK 中的全部類加載器的研究。當初誤入歧途,恨不得去研究 JVM 部分看不懂的C++源碼;
- Tomcat類加載架構,相應部分源碼閱讀;
- 類加載器執行空間的跳轉。
六、部分選型報告
整個調用鏈系統在開發時涉及到了非常多的關鍵技術的選型,這里僅給出Agent相關的兩個關鍵技術。
1、字節碼操控工具ByteBuddy
字節碼編程對於普通的 Java程序員來說,算是能玩的最牛的黑科技了。什么是字節碼編程呢?相信你一定多多少少了解過 javassist、asm等字節碼編輯庫,我們在進行字節碼編程時,一般會借助這些庫動態的修改或者生成 Java字節碼。例如Dubbo就借助了javassist來動態生成部分類的字節碼。選擇 ByteBuddy的原因主要是項目之初參考了SkyWalking的埋點邏輯,而那時SkyWalking就是使用的ByteBuddy。如果現在來選擇,我會優先Javassist,下面羅列了幾個框架個人理解的優缺點。
(1)ByteBuddy
基於ASM做的封裝,使用到的開源項目:Hibernate、Jackson。
優點:
- 在特定場景下使用非常方便;
- 17年框架的作者非常活躍,支持最新jdk幾乎全部新特性;
- 容易定制擴展。
缺點:
- 領域模型定義混亂,類圖設計復雜,內部類可以深達8層,eclipse都無法反編譯若干類,源碼難以調試及閱讀,對深度使用者極度不友好,我們一般開發使用的內部類極少會超過3層,想象下8層深的內部類是怎么樣的!
(2)ASM
開源項目:Groovy/Kotlin編譯器、CGLIB、Spring。
優點:
- 寫出來的代碼很顯野蠻牛逼的氣息,面向字節碼編程是 Java語言級別的黑科技;
- 願景致力於性能和精小,全部代碼只有28個類。
缺點:
- 使用起來比較復雜,編碼效率低下;
- 需要比較了解 Java語言字節碼指令集,需要比較清楚class文件內容布局。
(3)Javassist
開源項目:Dubbo、MyBatis。
優點:
- 使用簡單快速易上手;
- 先生成字符串再編譯成字節碼的使用方式,對程序員來說很容易理解;
- 官方文檔示例易理解,且很豐富。
缺點:
- 自帶的編譯器與 Javac有一定差距,難以實現復雜功能和新版jdk新特性。
2、環形無鎖隊列Disruptor
使用Disruptor的原因,主要是其高性能的同時,能做到限制容量也不阻塞,這簡直太讓人滿意了,而 JDK 中的線程安全相關集合皆無法滿足。
(1)主要特點:無阻塞、低延遲、高消耗。
(2)使用場景:
- 高並發無阻塞低延遲系統;
- 分段式事件驅動架構。
(3)為何這么快?
- 使用了volatile和cas無鎖操作;
- 使用了緩存行填充手段避免偽共享;
- 數組實現預先分配內存,減少了內存申請和垃圾收集帶來延遲影響;
- 快速指針操作,將模運算轉換成與運算(m % 2^n = m & ( 2^n - 1 ))。
(4)使用注意事項:
消費者等待策略:綜合業務線程阻塞、cpu損耗、數據丟失情況做的綜合考慮。
七、總結
要做好調用鏈系統的研發,顯然是一個困難的工作,難點不僅僅在於 Agent 技術難點解決,也在於產品能力的決策與挖掘,在於怎樣用最少的資源滿足產品需求,更在於當初不懂大數據的 Java開發在有限資源前提下來做海量數據計算。
希望本文能給正在從事以及將會從事調用鏈系統研發的公司及團隊一點參考。感謝閱讀。
作者:Shi Zhengxing