1. 引言
1.1 項目的背景及意義
在當今的微服務架構中,應用程序通常被拆分成多個獨立的服務,這些服務通過網絡進行通信。這種架構的優勢在于可以提高系統的可擴展性和靈活性,但也帶來了新的挑戰,比如:
服務間通信的復雜性:不同服務之間需要進行可靠的通信,處理失敗重試、負載均衡等問題。
故障的容錯處理:系統的復雜性給與運維及故障處理帶來更大的挑戰,如何快速處理故障解決線上問題,這是考驗一個企業基礎設施建設的重要關卡。
最初,開發者使用SDK來解決這些問題,通過在代碼中集成各種庫和工具來實現服務治理。然而,隨著微服務架構的規模不斷擴大,這種方法逐漸顯現出局限性:
代碼侵入性:需要在每個服務的代碼中集成和配置各種庫,增加了代碼的復雜性和維護成本。
一致性問題:不同服務可能使用不同版本的庫,導致治理邏輯不一致,SDK的升級難度凸顯。
為了解決這些問題,服務網格(Service Mesh)應運而生。服務網格通過在服務間引入一個代理層(通常稱為Sidecar),將服務治理的邏輯從應用代碼中分離出來,實現了更好的治理和管理。然而,服務網格的引入也帶來了額外的復雜性和性能開銷。
在這樣的背景下,我們通過Java字節碼增強技術,在服務治理領域提供了一種創新的解決方案。它結合了SDK和服務網格的優點,提供了無侵入性的治理邏輯注入、靈活的擴展性和高效的性能優化。這種方法不僅簡化了微服務架構中的服務治理,還提升了系統的可觀測性和安全性,對于現代微服務環境具有重要意義。
1.2 項目概述
Joylive Agent 是一個基于字節碼增強的框架,專注于多活和單元化場景下的流量治理。它提供了以下功能:多活流量調度、全鏈路灰度發布、QPS和并發限制、標簽路由、負載均衡,熔斷降級,鑒權等流量治理策略。其特性包括微內核架構、強類隔離、業務零侵入等,使其在保持高性能的同時對業務代碼影響最小,是面向Java領域的新一代Proxyless Service Mesh探索實現。
項目地址:
https://github.com/jd-opensource/joylive-agent
重要的事情說三遍:求Star,求Star,求Star。請Star完畢后繼續閱讀后文。
2. 微服務架構演進及優缺點
2.1 單體架構階段
最初,大多數應用都是作為單體應用開發的。所有功能都集中在一個代碼庫中,部署也是作為一個整體。這種形式也是我們學習編程之初,最原始的模樣。確切的說,這種形態并不屬于微服務。如下圖所示:
優點: 簡單、易于開發和測試,適合小團隊和小規模應用。
缺點: 這種架構隨著應用規模增大,可能會面臨維護困難、擴展性差等問題。
2.2 垂直拆分階段
隨著應用規模的成長,此時會考慮將每個功能模塊(服務)拆分為獨立的應用,也就是垂直拆分,擁有自己的代碼庫、數據庫和部署生命周期。服務之間通過輕量級協議(如HTTP、gRPC)通信。也就是正式開啟了面向服務的架構(SOA)。這種形態體現為:服務發現通過DNS解析,Consumer與Provider之間會有LB進行流量治理。服務間通過API進行通信。如下圖所示:
優點: 獨立部署和擴展,每個服務可以由獨立的團隊開發和維護,提高了敏捷性。
缺點: 增加了分布式系統的復雜性,需要處理服務間通信、數據一致性、服務發現、負載均衡等問題,也因為中間引入LB而降低了性能。
2.3 微服務成熟階段
這個階段引入更多的微服務治理和管理工具,使用專業的微服務框架或中間件,通過專門定制的微服務通訊協議,讓應用取得更高的吞吐性能。如API網關、注冊中心、分布式追蹤等。DevOps和持續集成/持續部署(CI/CD)流程成熟。代表產物如Spring Cloud,Dubbo等。此時典型的微服務場景還都是具體的微服務SDK提供的治理能力。通訊流程為:SDK負責向注冊中心注冊當前服務信息,當需要進行服務消費時,同樣向注冊中心請求服務提供者信息,然后直連服務提供者IP及端口并發送請求。如下圖所示:
優點: 高度可擴展、彈性和靈活性,支持高頻率的發布和更新。
缺點: 系統復雜性和運維成本較高,需要成熟的技術棧和團隊能力。微服務治理能力依賴SDK,升級更新成本高,需要綁定業務應用更新。
2.4 服務網格架構
隨著云原生容器化時代的到來,服務網格是一種專門用于管理微服務之間通信的基礎設施層。它通常包含一組輕量級的網絡代理(通常稱為 sidecar),這些代理與每個服務實例一起部署,這利用了K8s中Pod的基礎能力。服務網格負責處理服務間的通信、流量管理、安全性、監控和彈性等功能。這種微服務治理方式也可以稱之為Proxy模式,其中SideCar即作為服務之間的Proxy。如下圖所示:
優點: 主要優點是解耦業務邏輯與服務治理的能力,通過集中控制平面(control plane)簡化了運維管理。
缺點: 增加了資源消耗,更高的運維挑戰。
3. 項目架構設計
有沒有一種微服務治理方案,既要有SDK架構的高性能、多功能的好處,又要有邊車架構的零侵入優勢, 還要方便好用?這就是項目設計的初衷。項目的設計充分考慮了上面微服務的架構歷史,結合多活流量治理模型,進行了重新設計。其中項目設計到的主要技術點如下,并進行詳細解析。如下圖所示:
3.1 Proxyless模式
Proxyless模式(無代理模式)是為了優化性能和減少資源消耗而引入的。傳統的微服務網格通常使用邊車代理(Sidecar Proxy)來處理服務之間的通信、安全、流量管理等功能。
我們選擇通過Java Agent模式實現Proxyless模式是一種將服務網格的功能(如服務發現、負載均衡、流量管理和安全性)直接集成到Java應用程序中的方法。這種方式可以利用Java Agent在運行時對應用程序進行字節碼操作,從而無縫地將服務網格功能注入到應用程序中,而無需顯式修改應用代碼。Java Agent模式實現Proxyless的優點如下:
性能優化:
減少網絡延遲:傳統的邊車代理模式會引入額外的網絡跳數,因為每個請求都需要通過邊車代理進行處理。通過Java Agent直接將服務網格功能注入到應用程序中,可以減少這些額外的網絡開銷,從而降低延遲。
降低資源消耗:不再需要運行額外的邊車代理,從而減少了CPU、內存和網絡資源的占用。這對需要高效利用資源的應用非常重要。
簡化運維:
統一管理:通過Java Agent實現Proxyless模式,所有服務網格相關的配置和管理可以集中在控制平面進行,而無需在每個服務實例中單獨配置邊車代理。這簡化了運維工作,特別是在大型分布式系統中。
減少環境復雜性:通過消除邊車代理的配置和部署,環境的復雜性降低,減少了可能出現的配置錯誤或版本不兼容問題。
數據局面升級:Java Agent作為服務治理數據面,天然與應用程序解耦,這點是相對于SDK的最大優點。當數據面面臨版本升級迭代時,可以統一管控而不依賴于用戶應用的重新打包構建。
靈活性:
無需修改源代碼與現有生態系統兼容:Java Agent可以在運行時對應用程序進行字節碼操作,直接在字節碼層面插入服務網格相關的邏輯,而無需開發者修改應用程序的源代碼。這使得現有應用能夠輕松集成Proxyless模式。
動態加載和卸載:Java Agent可以在應用程序啟動時或運行時動態加載和卸載。這意味著服務網格功能可以靈活地添加或移除,適應不同的運行時需求。
適用性廣:
支持遺留系統:對于無法修改源代碼的遺留系統,Java Agent是一種理想的方式,能夠將現代化的服務網格功能集成到老舊系統中,提升其功能和性能。
通過Java Agent實現Proxyless模式,能夠在保持現有系統穩定性的同時,享受服務網格帶來的強大功能,是一種高效且靈活的解決方案。
3.2 微內核架構概述
微內核架構是一種軟件設計模式,主要分為核心功能(微內核)和一系列的插件或服務模塊。微內核負責處理系統的基礎功能,而其他功能則通過獨立的插件或模塊實現。這種架構的主要優點是模塊化、可擴展性強,并且系統的核心部分保持輕量級。
核心組件:框架的核心組件更多的定義核心功能接口的抽象設計,模型的定義以及agent加載與類隔離等核心功能,為達到最小化依賴,很多核心功能都是基于自研代碼實現。具體可參見joylive-core代碼模塊。
插件化設計:使用了模塊化和插件化的設計,分別抽象了像保護插件,注冊插件,路由插件,透傳插件等豐富的插件生態,極大的豐富了框架的可擴展性,為適配多樣化的開源生態奠定了基礎。具體可參見joylive-plugin代碼模塊。
3.3 插件擴展體系
項目基于Java的SPI機制實現了插件化的擴展方式,這也是Java生態的主流方式。
3.3.1 定義擴展
定義擴展接口,并使用@Extensible注解來進行擴展的聲明。下面是個負載均衡擴展示例:
@Extensible("LoadBalancer")public interface LoadBalancer { int ORDER_RANDOM_WEIGHT = 0; int ORDER_ROUND_ROBIN = ORDER_RANDOM_WEIGHT + 1; default T choose(List endpoints, Invocation??> invocation) { Candidate candidate = elect(endpoints, invocation); return candidate == null ? null : candidate.getTarget(); } Candidate elect(List endpoints, Invocation??> invocation);}
3.3.2 實現擴展
實現擴展接口,并使用@Extension注解來進行擴展實現的聲明。如下是實現了LoadBalancer接口的實現類:
@Extension(value = RoundRobinLoadBalancer.LOAD_BALANCER_NAME, order = LoadBalancer.ORDER_ROUND_ROBIN)@ConditionalOnProperties(value = { @ConditionalOnProperty(value = GovernanceConfig.CONFIG_LIVE_ENABLED, matchIfMissing = true), @ConditionalOnProperty(value = GovernanceConfig.CONFIG_LANE_ENABLED, matchIfMissing = true), @ConditionalOnProperty(value = GovernanceConfig.CONFIG_FLOW_CONTROL_ENABLED, matchIfMissing = true)}, relation = ConditionalRelation.OR)public class RoundRobinLoadBalancer extends AbstractLoadBalancer { public static final String LOAD_BALANCER_NAME = "ROUND_ROBIN"; private static final Function COUNTER_FUNC = s -> new AtomicLong(0L); private final Map counters = new ConcurrentHashMap?>(); private final AtomicLong global = new AtomicLong(0); @Override public Candidate doElect(List endpoints, Invocation??> invocation) { AtomicLong counter = global; ServicePolicy servicePolicy = invocation.getServiceMetadata().getServicePolicy(); LoadBalancePolicy loadBalancePolicy = servicePolicy == null ? null : servicePolicy.getLoadBalancePolicy(); if (loadBalancePolicy != null) { counter = counters.computeIfAbsent(loadBalancePolicy.getId(), COUNTER_FUNC); } long count = counter.getAndIncrement(); if (count < 0) { counter.set(0); count = counter.getAndIncrement(); } // Ensure the index is within the bounds of the endpoints list. int index = (int) (count % endpoints.size()); return new Candidate?>(endpoints.get(index), index); }}
該類上的注解如下:
@Extension注解聲明擴展實現,并提供了名稱
@ConditionalOnProperty注解聲明啟用的條件,可以組合多個條件
3.3.3 啟用擴展
在SPI文件
META-INF/services/com.jd.live.agent.governance.invoke.loadbalance.LoadBalancer中配置擴展全路徑名
com.jd.live.agent.governance.invoke.loadbalance.roundrobin.RoundRobinLoadBalancer即達到啟用效果。
更多詳情可查閱:
https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/extension.md
3.4 依賴注入設計
說到依賴注入估計大家會立馬想到Spring,的確這是Spring的看家本領。在復雜的工程中,自動化的依賴注入確實會簡化工程的實現復雜度。讓開發人員從復雜的依賴構建中脫離出來,專注于功能點設計開發。依賴注入的實現是基于上面插件擴展體系,是插件擴展的功能增強。并且依賴注入支持了兩類場景:注入對象與注入配置。該功能主要有4個注解類與3個接口構成。
3.4.1 @Injectable
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Injectable { boolean enable() default true;}
這是一個非常簡潔的可以應用于類、接口(包括注解類型)或枚舉注解。其目的為了標識哪些類開啟了自動注入對象的要求。這點不同于Spring的控制范圍,而是按需注入。實例構建完成后,在自動注入的邏輯過程中會針對添加@Injectable注解的實例進行依賴對象注入。
3.4.2 @Inject
@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Inject { String value() default ""; boolean nullable() default false; ResourcerType loader() default ResourcerType.CORE_IMPL;}
該注解用于自動注入值到字段。它用于指定一個字段在運行時應該注入一個值,支持基于配置的注入。還可以指示被注入的值是否可以為 null,如果注入過程中無注入實例或注入實例為null,而nullable配置為false,則會拋出異常。loader定義了指定要為注釋字段加載的資源或類實現的類型。ResourcerType為枚舉類型,分別是:CORE,CORE_IMPL,PLUGIN。劃分依據是工程打包后的jar文件分布目錄。因為不同目錄的類加載器是不同的(類隔離的原因,后面會講到)。所以可以簡單理解,這個配置是用于指定加載該對象所對應的類加載器。
3.4.3 @Configurable
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Configurable { String prefix() default ""; boolean auto() default false;}
這個注解類似于@Injectable,用于指定哪些類啟用自動注入配置文件的支持。prefix指定用于配置鍵的前綴。這通常意味著前綴將來自類名或基于某種約定。auto指示配置值是否應自動注入到注解類的所有合規字段中。默認值為 false,這意味著默認情況下未啟用自動注入。
3.4.4 @Config
@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Config { String value() default ""; boolean nullable() default true;}
該注解用于指定字段配置詳細信息。它定義了字段的配置鍵以及配置是否為可選。此注釋可用于在運行時自動將配置值加載到字段中,并支持指定缺少配置(無配置)是否被允許。nullable指示字段的配置是否是可選的。如果為真,則系統將允許配置缺失而不會導致錯誤。
下面是具體的使用示例:
@Injectable@Extension(value = "CircuitBreakerFilter", order = OutboundFilter.ORDER_CIRCUIT_BREAKER)public class CircuitBreakerFilter implements OutboundFilter, ExtensionInitializer { @Inject private Map factories; @Inject(nullable = true) private CircuitBreakerFactory defaultFactory; @Inject(GovernanceConfig.COMPONENT_GOVERNANCE_CONFIG) private GovernanceConfig governanceConfig; ...}@Configurable(prefix = "app")public class Application { @Setter @Config("name") private String name; @Setter @Config("service") private AppService service; ...}
更多細節,因為篇幅原因不再展開。詳情可以了解:
https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/extension.md
3.5 字節碼增強機制
Java 的字節碼增強(Bytecode Enhancement)是一種動態改變 Java 字節碼的技術,允許開發者在類加載之前或加載過程中修改類的字節碼。這種機制在 AOP(面向切面編程)、框架增強、性能監控、日志記錄等領域廣泛應用。目前看此技術在APM產品使用較多,而針對流量治理方向的開源實現還是比較少的。
Java字節碼增強的主要方法有:
運行時增強:使用Java Agent在類加載時修改字節碼。
加載時增強:在類加載到JVM之前修改字節碼。
編譯時增強:在編譯階段修改或生成字節碼。
進行字節碼增強的框架有很多,例如:JavaAssist、ASM、ByteBuddy、ByteKit等。我們針對字節碼增強的過程及重要對象進行了接口抽象,并以插件化方式適配了ByteBuddy,開發了一種默認實現。當然你也可以使用其他的框架實現相應的接口,作為擴展的其他實現方式。下面以ByteBuddy為例,展示一個入門實例:
import net.bytebuddy.ByteBuddy;import net.bytebuddy.implementation.MethodDelegation;import net.bytebuddy.implementation.SuperMethodCall;import net.bytebuddy.matcher.ElementMatchers;import java.lang.reflect.Method;// 原始類class SimpleClass { public void sayHello() { System.out.println("Hello, World!"); }}// 攔截器class SimpleInterceptor { public static void beforeMethod() { System.out.println("Before saying hello"); } public static void afterMethod() { System.out.println("After saying hello"); }}public class ByteBuddyExample { public static void main(String[] args) throws Exception { // 使用ByteBuddy創建增強類 Class??> dynamicType = new ByteBuddy() .subclass(SimpleClass.class) .method(ElementMatchers.named("sayHello")) .intercept(MethodDelegation.to(SimpleInterceptor.class) .andThen(SuperMethodCall.INSTANCE)) .make() .load(ByteBuddyExample.class.getClassLoader()) .getLoaded(); // 創建增強類的實例 Object enhancedInstance = dynamicType.getDeclaredConstructor().newInstance(); // 調用增強后的方法 Method sayHelloMethod = enhancedInstance.getClass().getMethod("sayHello"); sayHelloMethod.invoke(enhancedInstance); }}
這個例子展示了如何使用ByteBuddy來增強SimpleClass的sayHello方法。讓我解釋一下這個過程:
我們定義了一個簡單的SimpleClass,它有一個sayHello方法。
我們創建了一個SimpleInterceptor類,包含了我們想要在原方法執行前后添加的邏輯。
在ByteBuddyExample類的main方法中,我們使用ByteBuddy來創建一個增強的類:
我們創建了SimpleClass的一個子類。
我們攔截了名為"sayHello"的方法。
我們使用MethodDelegation.to(SimpleInterceptor.class)來添加前置和后置邏輯。
我們使用SuperMethodCall.INSTANCE來確保原始方法被調用。
我們創建了增強類的實例,并通過反射調用了sayHello方法。
當你運行這個程序時,輸出將會是:
Before saying hello Hello, World!After saying hello
當然,工程級別的實現是遠比上面Demo的組織形式復雜的。插件是基于擴展實現的,有多個擴展組成,對某個框架進行特定增強,實現了多活流量治理等等業務邏輯。一個插件打包成一個目錄,如下圖所示:
.└── plugin ├── dubbo │ ├── joylive-registry-dubbo2.6-1.0.0.jar │ ├── joylive-registry-dubbo2.7-1.0.0.jar │ ├── joylive-registry-dubbo3-1.0.0.jar │ ├── joylive-router-dubbo2.6-1.0.0.jar │ ├── joylive-router-dubbo2.7-1.0.0.jar │ ├── joylive-router-dubbo3-1.0.0.jar │ ├── joylive-transmission-dubbo2.6-1.0.0.jar │ ├── joylive-transmission-dubbo2.7-1.0.0.jar │ └── joylive-transmission-dubbo3-1.0.0.jar
該dubbo插件,支持了3個版本,增強了注冊中心,路由和鏈路透傳的能力。下面介紹一下在joylive-agent中如何實現一個字節碼增強插件。
3.5.1 增強插件定義接口
增強插件(功能實現層面的插件)接口的定義采用了插件(系統架構層面的插件)擴展機制。如下代碼所示,定義的增強插件名稱為PluginDefinition。
@Extensible("PluginDefinition")public interface PluginDefinition { ElementMatcher getMatcher(); InterceptorDefinition[] getInterceptors();}
接口共定義了兩個方法:getMatcher用于獲取匹配要增強類的匹配器,getInterceptors是返回要增強目標類的攔截器定義對象。
3.5.2 攔截器定義接口
攔截器定義接口主要是用來確定攔截增強位置(也就是方法),定位到具體方法也就找到了具體增強邏輯執行的位置。攔截器定義接口并沒有采用擴展機制,這是因為具體到某個增強目標類后,要增強的方法與增強邏輯已經是確定行為,不再需要通過擴展機制實例化對象,在具體的增強插件定義接口實現里會直接通過new的方式構造蘭機器定義實現。攔截器定義接口如下:
public interface InterceptorDefinition { ElementMatcher getMatcher(); Interceptor getInterceptor();}
該接口同樣抽象了兩個方法:getMatcher用于獲取匹配要增強方法的匹配器,getInterceptor是用于返回具體的增強邏輯實現,我們稱之為攔截器(Interceptor)。
3.5.3 攔截器接口
攔截器的實現類是具體增強邏輯的載體,當我們要增強某個類的某個方法時,與AOP機制同理,我們抽象了幾處攔截位置。分別是:方法執行前(剛進入方法執行邏輯);方法執行結束時;方法執行成功時(無異常);方法執行出錯時(有異常)。接口定義如下:
public interface Interceptor { void onEnter(ExecutableContext ctx); void onSuccess(ExecutableContext ctx); void onError(ExecutableContext ctx); void onExit(ExecutableContext ctx);}
增強邏輯的實現可以針對不同的功能目標,選擇合適的增強點。同樣,攔截點的入參ExecutableContext也是非常重要的組成部分,它承載了運行時的上下文信息,并且針對不同的增強目標我們做了不同的實現。如下圖所示:
更多詳情可查看:
https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/plugin.md
3.6 類加載與類隔離
類加載的原理比較容易理解,因為在Java Agent模式下,應用啟動時agent需要加載它所依賴的jar包。然而,如果這些類在加載后被用戶應用的類加載器感知到,就可能導致類沖突甚至不兼容的風險。因此,引入類隔離機制是為了解決這個問題。類隔離的實現原理并不復雜。首先,需要實現自定義的類加載器;其次,需要打破默認的雙親委派機制。通過這兩步,類隔離可以實現多層次的隔離,從而避免類沖突和不兼容問題。如下圖所示:
3.7 面向請求的抽象
整個框架的核心行為就是治理請求,而站在一個應用的視角我們可以大體把請求抽象為兩類:InboundRequest與OutboundRequest。InboundRequest是外部進入當前應用的請求,OutboundRequest是當前應用發往外部資源的請求。同樣的處理這些請求的過濾器也同樣分為InboundFilter與OutboundFilter。
請求接口
請求抽象實現
具體框架的請求適配,如Dubbo
如上圖所示,展現了適配Dubbo的請求對象的DubboOutboundRequest與DubboInboundRequest,體現了一個OutboundRequest與InboundRequest的實現與繼承關系。整體看起來確實比較復雜。這是因為在請求抽象時,不僅要考慮請求是Inbound還是Outbound,還要適配不同的協議框架。例如,像Dubbo和JSF這樣的私有通訊協議框架需要統一為RpcRequest接口的實現,而SpringCloud這樣的HTTP通訊協議則統一為HttpRequest。再加上Inbound和Outbound的分類維度,整體的抽象在追求高擴展性的同時也增加了復雜性。
4. 核心功能
下面提供的流量治理功能,以API網關作為東西向流量第一入口進行流量識別染色。在很大程度上,API網關作為東西向流量識別的第一入口發揮了重要作用。API網關在接收到南北向流量后,后續將全部基于東西向流量治理。
4.1 多活模型及流量調度
應用多活通常包括同城多活和異地多活,異地多活可采用單元化技術來實現。下面描述整個多活模型涉及到的概念及嵌套關系。具體實現原理如下圖所示:
4.1.1 多活空間
在模型和權限設計方面,我們支持多租戶模式。一個租戶可以有多個多活空間,多活空間構成如下所示:
.└── 多活空間 ├── 單元路由變量(*) ├── 單元(*) │ ├── 分區(*) ├── 單元規則(*) ├── 多活域名(*) │ ├── 單元子域名(*) │ ├── 路徑(*) │ │ ├── 業務參數(*)
4.1.2 單元
單元是邏輯上的概念,一般對應一個地域。常用于異地多活場景,通過用戶維度拆分業務和數據,每個單元獨立運作,降低單元故障對整體業務的影響。
單元的屬性包括單元代碼、名稱、類型(中心單元或普通單元)、讀寫權限、標簽(如地域和可用區)、以及單元下的分區。
單元分區是單元的組成部分,單元內的邏輯分區,對應云上的可用區或物理數據中心,屬性類似單元。
4.1.3 路由變量
路由變量是決定流量路由到哪個單元的依據,通常是用戶賬號。每個變量可以通過不同的取值方式(如Cookie、請求頭等)獲取,且可以定義轉換函數來獲取實際用戶標識。
變量取值方式則描述如何從請求參數、請求頭或Cookie中獲取路由變量。
4.1.4 單元規則
單元規則定義了單元和分區之間的流量調度規則。根據路由變量計算出的值,通過取模判斷流量應路由到哪個單元。它的屬性包括多活類型、變量、變量取值方式、計算函數、變量缺失時的操作、以及具體的單元路由規則。
單元路由規則定義了單元內的流量路由規則,包括允許的路由變量白名單、前綴、值區間等。
分區路由規則定義了單元內各個分區的流量路由規則,包括允許的變量白名單、前綴及權重。
4.1.5 多活域名
多活域名描述啟用多活的域名,用于在網關層進行流量攔截和路由。支持跨地域和同城多活的流量管理,配置路徑規則以匹配請求路徑并執行相應的路由規則。
單元子域名描述各個單元的子域名,通常用于在HTTP請求或回調時閉環在單元內進行路由。
路徑規則定義了根據請求路徑匹配的路由規則,取最長匹配路徑來選擇適用的路由規則。
業務參數規則基于請求參數值進一步精細化路由,選擇特定的單元規則。
4.1.6 模型骨架
以下是多活治理模型的基本配置樣例,包括API版本、空間名稱、單元、域名、規則、變量等。
[ { "apiVersion": "apaas.cos.com/v2alpha1", "kind": "MultiLiveSpace", "metadata": { "name": "mls-abcdefg1", "namespace": "apaas-livespace" }, "spec": { "id": "v4bEh4kd6Jvu5QBX09qYq-qlbcs", "code": "7Jei1Q5nlDbx0dRB4ZKd", "name": "TestLiveSpace", "version": "2023120609580935201", "tenantId": "tenant1", "units": [ ], "domains": [ ], "unitRules": [ ], "variables": [ ] } }]
以上概念會比較晦澀難懂,更多詳情可以訪問:
https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/livespace.md
4.2 全鏈路灰度(泳道)
泳道是一種隔離和劃分系統中不同服務或組件的方式,類似于游泳池中的泳道劃分,確保每個服務或組件在自己的“泳道”中獨立運作。泳道概念主要用于以下幾種場景:
多租戶架構中的隔離
在多租戶系統中,泳道通常用于隔離不同租戶的資源和服務。每個租戶有自己的獨立“泳道”,以確保數據和流量的隔離,防止不同租戶之間的相互影響。
流量隔離與管理
泳道可以用于根據特定規則(例如用戶屬性、地理位置、業務特性等)將流量分配到不同的微服務實例或集群中。這種方式允許團隊在某些條件下測試新版本、進行藍綠部署、金絲雀發布等,而不會影響到其他泳道中的流量。
業務邏輯劃分
在某些場景下,泳道也可以代表業務邏輯的劃分。例如,一個電商平臺可能會針對不同的用戶群體(如普通用戶、VIP用戶)提供不同的服務路徑和處理邏輯,形成不同的泳道。
版本管理
泳道可以用來管理微服務的不同版本,使得新版本和舊版本可以在不同的泳道中并行運行,從而降低升級時的風險。
開發和測試
在開發和測試過程中,不同的泳道可以用于隔離開發中的新功能、測試環境、甚至是不同開發團隊的工作,從而減少互相干擾。
泳道的核心目的是通過隔離服務或資源,提供獨立性和靈活性,確保系統的穩定性和可擴展性。這種隔離機制幫助組織更好地管理復雜系統中的多樣性,尤其是在處理高并發、多租戶、或者需要快速迭代的場景下。功能流程如下圖所示:
模型定義及更多詳情可以訪問:
https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/lane.md
4.3 微服務治理策略
微服務治理策略是指在微服務架構中,為確保服務的穩定性、可靠性、安全性以及高效運作而制定的一系列管理和控制措施。這些策略幫助企業有效地管理、監控、協調和優化成百上千個微服務的運行,以應對分布式系統的復雜性。目前我們實現了部分主流的微服務策略例如:負載均衡,重試,限流,熔斷降級,標簽路由,訪問鑒權等,更多的實用策略也在陸續補充中。微服務框架方面已經支持主流框架,如:Spring Cloud,Dubbo2/3,JSF,SofaRpc等。
由于篇幅原因,具體的治理策略與模型就不詳盡展開介紹了,下圖概括了服務治理的全貌。
值得一提有兩點:
策略的實現屏蔽了底層框架的差異性,這得益于上面提到的面向請求的抽象。
統一治理層級的劃分,多層級的策略掛載框架允許治理策略可以靈活的控制策略生效的影響半徑。
統一HTTP和傳統RPC的治理策略配置層級具體細節如下:
.└── 服務 ├── 分組* │ ├── 路徑* │ │ ├── 方法*
服務治理策略放在分組、路徑和方法上,可以逐級設置,下級默認繼承上級的配置。服務的默認策略設置到默認分組default上。
類型 | 服務 | 分組 | 路徑 | 方法 |
HTTP | 域名 | 分組 | URL路徑 | HTTP方法 |
RPC 應用級注冊 | 應用名 | 分組 | 接口名 | 方法名 |
RPC 接口級注冊 | 接口名 | 分組 | / | 方法名 |
模型定義及更多詳情可以訪問:
https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/governance.md
5. 功能實現示例
5.1 服務注冊
5.1.1 服務注冊
在應用啟動過程中,注冊插件會攔截獲取到消費者和服務提供者的初始化方法,會修改其元數據,增加多活和泳道的標簽。后續往注冊中心注冊的時候就會帶有相關的標簽了。這是框架所有治理功能的前提基礎。以下是Dubbo服務提供者注冊樣例:
@Injectable@Extension(value = "ServiceConfigDefinition_v3", order = PluginDefinition.ORDER_REGISTRY)@ConditionalOnProperties(value = { @ConditionalOnProperty(value = GovernanceConfig.CONFIG_LIVE_ENABLED, matchIfMissing = true), @ConditionalOnProperty(value = GovernanceConfig.CONFIG_LANE_ENABLED, matchIfMissing = true), @ConditionalOnProperty(value = GovernanceConfig.CONFIG_FLOW_CONTROL_ENABLED, matchIfMissing = true)}, relation = ConditionalRelation.OR) @ConditionalOnClass(ServiceConfigDefinition.TYPE_CONSUMER_CONTEXT_FILTER)@ConditionalOnClass(ServiceConfigDefinition.TYPE_SERVICE_CONFIG)public class ServiceConfigDefinition extends PluginDefinitionAdapter { protected static final String TYPE_SERVICE_CONFIG = "org.apache.dubbo.config.ServiceConfig"; private static final String METHOD_BUILD_ATTRIBUTES = "buildAttributes"; private static final String[] ARGUMENT_BUILD_ATTRIBUTES = new String[]{ "org.apache.dubbo.config.ProtocolConfig" }; // ...... public ServiceConfigDefinition() { this.matcher = () -> MatcherBuilder.named(TYPE_SERVICE_CONFIG); this.interceptors = new InterceptorDefinition[]{ new InterceptorDefinitionAdapter( MatcherBuilder.named(METHOD_BUILD_ATTRIBUTES). and(MatcherBuilder.arguments(ARGUMENT_BUILD_ATTRIBUTES)), () -> new ServiceConfigInterceptor(application, policySupplier)) }; }}
public class ServiceConfigInterceptor extends InterceptorAdaptor { // ...... @Override public void onSuccess(ExecutableContext ctx) { MethodContext methodContext = (MethodContext) ctx; Map map = (Map) methodContext.getResult(); application.label(map::putIfAbsent); // ...... }}
上面例子所呈現的效果是,當dubbo應用啟動時,增強插件攔截dubbo框架
org.apache.dubbo.config.ServiceConfig中的buildAttributes方法進行增強處理。從ServiceConfigInterceptor的實現中可以看出,當buildAttributes方法執行成功后,對該方法的返回的Map對象繼續增加了框架額外的元數據標簽。
5.1.2 服務策略訂閱
如果注意ServiceConfigInterceptor的增強會發現,在給注冊示例打標之后,還有一部分邏輯,如下:
public class ServiceConfigInterceptor extends InterceptorAdaptor { @Override public void onSuccess(ExecutableContext ctx) { MethodContext methodContext = (MethodContext) ctx; // ...... AbstractInterfaceConfig config = (AbstractInterfaceConfig) ctx.getTarget(); ApplicationConfig application = config.getApplication(); String registerMode = application.getRegisterMode(); if (DEFAULT_REGISTER_MODE_INSTANCE.equals(registerMode)) { policySupplier.subscribe(application.getName()); } else if (DEFAULT_REGISTER_MODE_INTERFACE.equals(registerMode)) { policySupplier.subscribe(config.getInterface()); } else { policySupplier.subscribe(application.getName()); policySupplier.subscribe(config.getInterface()); } }}
policySupplier.subscribe所執行的是策略訂閱邏輯。因為策略是支持熱更新并實時生效的,策略訂閱邏輯便是開啟了訂閱當前服務在控制臺所配置策略的邏輯。
5.2 流量控制
5.2.1 入流量攔截點
入流量攔截也就是攔截Inbound請求,攔截相關框架的入流量處理鏈的入口或靠前的處理器的相關邏輯并予以增強。下面以Dubbo3的攔截點為例。
@Injectable@Extension(value = "ClassLoaderFilterDefinition_v3")@ConditionalOnProperty(value = GovernanceConfig.CONFIG_LIVE_ENABLED, matchIfMissing = true)@ConditionalOnProperty(value = GovernanceConfig.CONFIG_LIVE_DUBBO_ENABLED, matchIfMissing = true)@ConditionalOnProperty(value = GovernanceConfig.CONFIG_REGISTRY_ENABLED, matchIfMissing = true)@ConditionalOnProperty(value = GovernanceConfig.CONFIG_TRANSMISSION_ENABLED, matchIfMissing = true)@ConditionalOnClass(ClassLoaderFilterDefinition.TYPE_CLASSLOADER_FILTER)public class ClassLoaderFilterDefinition extends PluginDefinitionAdapter { protected static final String TYPE_CLASSLOADER_FILTER = "org.apache.dubbo.rpc.filter.ClassLoaderFilter"; private static final String METHOD_INVOKE = "invoke"; protected static final String[] ARGUMENT_INVOKE = new String[]{ "org.apache.dubbo.rpc.Invoker", "org.apache.dubbo.rpc.Invocation" }; // ...... public ClassLoaderFilterDefinition() { this.matcher = () -> MatcherBuilder.named(TYPE_CLASSLOADER_FILTER); this.interceptors = new InterceptorDefinition[]{ new InterceptorDefinitionAdapter( MatcherBuilder.named(METHOD_INVOKE). and(MatcherBuilder.arguments(ARGUMENT_INVOKE)), () -> new ClassLoaderFilterInterceptor(context) ) }; }}
public class ClassLoaderFilterInterceptor extends InterceptorAdaptor { private final InvocationContext context; public ClassLoaderFilterInterceptor(InvocationContext context) { this.context = context; } @Override public void onEnter(ExecutableContext ctx) { MethodContext mc = (MethodContext) ctx; Object[] arguments = mc.getArguments(); Invocation invocation = (Invocation) arguments[1]; try { context.inbound(new DubboInboundInvocation(new DubboInboundRequest(invocation), context)); } catch (RejectException e) { Result result = new AppResponse(new RpcException(RpcException.FORBIDDEN_EXCEPTION, e.getMessage())); mc.setResult(result); mc.setSkip(true); } }}
public interface InvocationContext { // ...... default void inbound(InboundInvocation invocation) { InboundFilterChain.Chain chain = new InboundFilterChain.Chain(getInboundFilters()); chain.filter(invocation); } }
上面展示了針對Dubbo框架的增強處理是選擇了
org.apache.dubbo.rpc.filter.ClassLoaderFilter的invoke方法作為攔截點,織入我們統一的InboundFilterChain對象作為入流量處理鏈。我們可以根據需求實現不同的InboundFilter即可,我們內置了一部分實現。如下所示:
過濾器 | 名稱 | 說明 |
RateLimitInboundFilter | 限流過濾器 | 根據當前服務的限流策略來進行限流 |
ConcurrencyLimitInboundFilter | 并發過濾器 | 根據當前服務的并發策略來進行限流 |
ReadyInboundFilter | 治理就緒過濾器 | 判斷治理狀態,只有就緒狀態才能進入流量 |
UnitInboundFilter | 單元過濾器 | 判斷當前請求是否匹配當前單元,以及當前單元是否可以訪問 |
CellInboundFilter | 分區過濾器 | 判斷當前分區是否可以訪問 |
FailoverInboundFilter | 糾錯過濾器 | 目前對錯誤流量只實現了拒絕 |
5.2.2 出流量攔截點
出流量攔截也就是攔截Outbound請求,攔截相關框架的出流量處理鏈的入口并予以增強。下面以Dubbo2的攔截點為例。
如果只開啟了多活或泳道治理,則只需對后端實例進行過濾,可以攔截負載均衡或服務實例提供者相關方法
@Injectable@Extension(value = "LoadBalanceDefinition_v2.7")@ConditionalOnProperties(value = { @ConditionalOnProperty(name = { GovernanceConfig.CONFIG_LIVE_ENABLED, GovernanceConfig.CONFIG_LANE_ENABLED }, matchIfMissing = true, relation = ConditionalRelation.OR), @ConditionalOnProperty(name = GovernanceConfig.CONFIG_FLOW_CONTROL_ENABLED, value = "false"), @ConditionalOnProperty(name = GovernanceConfig.CONFIG_LIVE_DUBBO_ENABLED, matchIfMissing = true)}, relation = ConditionalRelation.AND)@ConditionalOnClass(LoadBalanceDefinition.TYPE_ABSTRACT_CLUSTER)@ConditionalOnClass(ClassLoaderFilterDefinition.TYPE_CONSUMER_CLASSLOADER_FILTER)public class LoadBalanceDefinition extends PluginDefinitionAdapter { protected static final String TYPE_ABSTRACT_CLUSTER = "com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker"; private static final String METHOD_SELECT = "select"; private static final String[] ARGUMENT_SELECT = new String[]{ "org.apache.dubbo.rpc.cluster.LoadBalance", "org.apache.dubbo.rpc.Invocation", "java.util.List", "java.util.List" }; // ...... public LoadBalanceDefinition() { this.matcher = () -> MatcherBuilder.isSubTypeOf(TYPE_ABSTRACT_CLUSTER) .and(MatcherBuilder.not(MatcherBuilder.isAbstract())); this.interceptors = new InterceptorDefinition[]{ new InterceptorDefinitionAdapter( MatcherBuilder.named(METHOD_SELECT) .and(MatcherBuilder.arguments(ARGUMENT_SELECT)), () -> new LoadBalanceInterceptor(context) ) }; }}
攔截器里面調用上下文的路由方法
public class LoadBalanceInterceptor extends InterceptorAdaptor { // ...... @Override public void onEnter(ExecutableContext ctx) { MethodContext mc = (MethodContext) ctx; Object[] arguments = ctx.getArguments(); List> invokers = (List>) arguments[2]; List> invoked = (List>) arguments[3]; DubboOutboundRequest request = new DubboOutboundRequest((Invocation) arguments[1]); DubboOutboundInvocation invocation = new DubboOutboundInvocation(request, context); DubboCluster3 cluster = clusters.computeIfAbsent((AbstractClusterInvoker??>) ctx.getTarget(), DubboCluster3::new); try { List> instances = invokers.stream().map(DubboEndpoint::of).collect(Collectors.toList()); if (invoked != null) { invoked.forEach(p -> request.addAttempt(new DubboEndpoint?>(p).getId())); } List? extends Endpoint?> endpoints = context.route(invocation, instances); if (endpoints != null && !endpoints.isEmpty()) { mc.setResult(((DubboEndpoint??>) endpoints.get(0)).getInvoker()); } else { mc.setThrowable(cluster.createNoProviderException(request)); } } catch (RejectException e) { mc.setThrowable(cluster.createRejectException(e, request)); } mc.setSkip(true); }}
如果開啟了微服務治理,則設計到重試,需要對集群調用進行攔截
@Injectable@Extension(value = "ClusterDefinition_v2.7")@ConditionalOnProperty(name = GovernanceConfig.CONFIG_FLOW_CONTROL_ENABLED, matchIfMissing = true)@ConditionalOnProperty(name = GovernanceConfig.CONFIG_LIVE_DUBBO_ENABLED, matchIfMissing = true)@ConditionalOnClass(ClusterDefinition.TYPE_ABSTRACT_CLUSTER)@ConditionalOnClass(ClassLoaderFilterDefinition.TYPE_CONSUMER_CLASSLOADER_FILTER)public class ClusterDefinition extends PluginDefinitionAdapter { protected static final String TYPE_ABSTRACT_CLUSTER = "org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker"; private static final String METHOD_DO_INVOKE = "doInvoke"; private static final String[] ARGUMENT_DO_INVOKE = new String[]{ "org.apache.dubbo.rpc.Invocation", "java.util.List", "org.apache.dubbo.rpc.cluster.LoadBalance" }; // ...... public ClusterDefinition() { this.matcher = () -> MatcherBuilder.isSubTypeOf(TYPE_ABSTRACT_CLUSTER) .and(MatcherBuilder.not(MatcherBuilder.isAbstract())); this.interceptors = new InterceptorDefinition[]{ new InterceptorDefinitionAdapter( MatcherBuilder.named(METHOD_DO_INVOKE) .and(MatcherBuilder.arguments(ARGUMENT_DO_INVOKE)), () -> new ClusterInterceptor(context) ) }; }}
攔截器里面構造集群對象進行同步或異步調用
public class ClusterInterceptor extends InterceptorAdaptor { // ...... @Override public void onEnter(ExecutableContext ctx) { MethodContext mc = (MethodContext) ctx; Object[] arguments = ctx.getArguments(); DubboCluster3 cluster = clusters.computeIfAbsent((AbstractClusterInvoker??>) ctx.getTarget(), DubboCluster3::new); List> invokers = (List>) arguments[1]; List> instances = invokers.stream().map(DubboEndpoint::of).collect(Collectors.toList()); DubboOutboundRequest request = new DubboOutboundRequest((Invocation) arguments[0]); DubboOutboundInvocation invocation = new DubboOutboundInvocation(request, context); DubboOutboundResponse response = cluster.request(context, invocation, instances); if (response.getThrowable() != null) { mc.setThrowable(response.getThrowable()); } else { mc.setResult(response.getResponse()); } mc.setSkip(true); }}
同樣,出流量攔截也是采用了責任鏈模式設計了OutboundFilterChain,用戶可以根據自己的需求擴展實現OutboundFilter,目前針對已支持功能內置了部分實現,如下所示:
過濾器 | 名稱 | 說明 |
StickyFilter | 粘連過濾器 | 根據服務的粘連策略進行過濾 |
LocalhostFilter | 本機過濾器 | 本地開發調試插件 |
HealthyFilter | 健康過濾器 | 根據后端實例的健康狀態進行過濾 |
VirtualFilter | 虛擬節點過濾器 | 復制出指定數量的節點,用于開發測試 |
UnitRouteFilter | 單元路由過濾器 | 根據多活路由規則及微服務的多活策略,根據請求的目標單元進行過濾 |
TagRouteFilter | 標簽路由過濾器 | 根據服務配置的標簽路由策略進行過濾 |
LaneFilter | 泳道過濾器 | 根據泳道策略進行過濾 |
CellRouteFilter | 分區路由過濾器 | 根據多活路由規則及微服務的多活策略,根據請求的目標分區進行過濾 |
RetryFilter | 重試過濾器 | 嘗試過濾掉已經重試過的節點 |
LoadBalanceFilter | 負載均衡過濾器 | 根據服務配置的負載均衡策略進行路由 |
6. 部署實踐
6.1 基于Kubernates實踐場景
我們開源的另一個項目joylive-injector是針對K8s場景打造的自動注入組件。joylive-injector是基于kubernetes的動態準入控制webhook,它可以用于修改kubernete資源。它會監視工作負載(如deployments)的CREATE、UPDATE、DELETE事件和pods的CREATE事件,并為POD添加initContainer、默認增加環境變量JAVA_TOOL_OPTIONS、掛載configmap、修改主容器的卷裝載等操作。目前已支持的特性如下:
支持自動將joylive-agent注入應用的Pod。
支持多版本joylive-agent與對應配置管理。
支持注入指定版本joylive-agent及對應配置。
所以,針對采用K8s進行應用發布管理的場景中,集成joylive-agent變得非常簡單,在安裝joylive-injector組件后,只需要在對應的deployment文件中加入標簽x-live-enabled: "true"即可,如下所示:
apiVersion: apps/v1kind: Deploymentmetadata: labels: app: joylive-demo-springcloud2021-provider x-live-enabled: "true" name: joylive-demo-springcloud2021-providerspec: replicas: 1 selector: matchLabels: app: joylive-demo-springcloud2021-provider template: metadata: labels: app: joylive-demo-springcloud2021-provider x-live-enabled: "true" spec: containers: - env: - name: CONFIG_LIVE_SPACE_API_TYPE value: multilive - name: CONFIG_LIVE_SPACE_API_URL value: http://api.live.local/v1 - name: CONFIG_LIVE_SPACE_API_HEADERS value: pin=demo - name: CONFIG_SERVICE_API_TYPE value: jmsf - name: CONFIG_SERVICE_API_URL value: http://api.jmsf.local/v1 - name: LIVE_LOG_LEVEL value: info - name: CONFIG_LANE_ENABLED value: "false" - name: NACOS_ADDR value: nacos-server.nacos.svc:8848 - name: NACOS_USERNAME value: nacos - name: NACOS_PASSWORD value: nacos - name: APPLICATION_NAME value: springcloud2021-provider - name: APPLICATION_SERVICE_NAME value: service-provider - name: APPLICATION_SERVICE_NAMESPACE value: default - name: SERVER_PORT value: "18081" - name: APPLICATION_LOCATION_REGION value: region1 - name: APPLICATION_LOCATION_ZONE value: zone1 - name: APPLICATION_LOCATION_LIVESPACE_ID value: v4bEh4kd6Jvu5QBX09qYq-qlbcs - name: APPLICATION_LOCATION_UNIT value: unit1 - name: APPLICATION_LOCATION_CELL value: cell1 - name: APPLICATION_LOCATION_LANESPACE_ID value: "1" - name: APPLICATION_LOCATION_LANE value: production image: hub-vpc.jdcloud.com/jmsf/joylive-demo-springcloud2021-provider:1.1.0-5aab82b3-AMD64 imagePullPolicy: Always name: joylive-demo-springcloud2021-provider ports: - containerPort: 18081 name: http protocol: TCP resources: requests: cpu: "4" memory: "8Gi" limits: cpu: "4" memory: "8Gi" terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst restartPolicy: Always schedulerName: default-scheduler securityContext: { } terminationGracePeriodSeconds: 30
啟動后Pod如下圖所示即代表注入成功,隨后觀察應用日志及功能測試即可。
我們有更高的目標和方向,希望更多有志于打造開源優品的朋友加入進來,一起為開源事業貢獻自己的光與熱。
下一篇:KubeCon China 2024全球大會在香港舉行,京東云受邀參加探討云原生、開源及 AI
審核編輯 黃宇
-
SDK
+關注
關注
3文章
1029瀏覽量
45782 -
微服務
+關注
關注
0文章
134瀏覽量
7328 -
微服務架構
+關注
關注
0文章
24瀏覽量
2952
發布評論請先 登錄
相關推薦
評論