Proxyless的多活流量和微服務(wù)治理
共 34437字,需瀏覽 69分鐘
·
2024-08-28 18:04
1. 引言
1.1 項(xiàng)目的背景及意義
服務(wù)間通信的復(fù)雜性:不同服務(wù)之間需要進(jìn)行可靠的通信,處理失敗重試、負(fù)載均衡等問題。
故障的容錯(cuò)處理:系統(tǒng)的復(fù)雜性給與運(yùn)維及故障處理帶來更大的挑戰(zhàn),如何快速處理故障解決線上問題,這是考驗(yàn)一個(gè)企業(yè)基礎(chǔ)設(shè)施建設(shè)的重要關(guān)卡。
最初,開發(fā)者使用SDK來解決這些問題,通過在代碼中集成各種庫(kù)和工具來實(shí)現(xiàn)服務(wù)治理。然而,隨著微服務(wù)架構(gòu)的規(guī)模不斷擴(kuò)大,這種方法逐漸顯現(xiàn)出局限性:
代碼侵入性:需要在每個(gè)服務(wù)的代碼中集成和配置各種庫(kù),增加了代碼的復(fù)雜性和維護(hù)成本。
一致性問題:不同服務(wù)可能使用不同版本的庫(kù),導(dǎo)致治理邏輯不一致,SDK的升級(jí)難度凸顯。
1.2 項(xiàng)目概述
項(xiàng)目地址:https://github.com/jd-opensource/joylive-agent
2. 微服務(wù)架構(gòu)演進(jìn)及優(yōu)缺點(diǎn)
2.1 單體架構(gòu)階段
優(yōu)點(diǎn): 簡(jiǎn)單、易于開發(fā)和測(cè)試,適合小團(tuán)隊(duì)和小規(guī)模應(yīng)用。
缺點(diǎn): 這種架構(gòu)隨著應(yīng)用規(guī)模增大,可能會(huì)面臨維護(hù)困難、擴(kuò)展性差等問題。
2.2 垂直拆分階段
優(yōu)點(diǎn):獨(dú)立部署和擴(kuò)展,每個(gè)服務(wù)可以由獨(dú)立的團(tuán)隊(duì)開發(fā)和維護(hù),提高了敏捷性。
缺點(diǎn):增加了分布式系統(tǒng)的復(fù)雜性,需要處理服務(wù)間通信、數(shù)據(jù)一致性、服務(wù)發(fā)現(xiàn)、負(fù)載均衡等問題,也因?yàn)橹虚g引入LB而降低了性能。
2.3 微服務(wù)成熟階段
優(yōu)點(diǎn): 高度可擴(kuò)展、彈性和靈活性,支持高頻率的發(fā)布和更新。
缺點(diǎn): 系統(tǒng)復(fù)雜性和運(yùn)維成本較高,需要成熟的技術(shù)棧和團(tuán)隊(duì)能力。微服務(wù)治理能力依賴SDK,升級(jí)更新成本高,需要綁定業(yè)務(wù)應(yīng)用更新。
2.4 服務(wù)網(wǎng)格架構(gòu)
優(yōu)點(diǎn): 主要優(yōu)點(diǎn)是解耦業(yè)務(wù)邏輯與服務(wù)治理的能力,通過集中控制平面(control plane)簡(jiǎn)化了運(yùn)維管理。
缺點(diǎn): 增加了資源消耗,更高的運(yùn)維挑戰(zhàn)。
3. 項(xiàng)目架構(gòu)設(shè)計(jì)
3.1 Proxyless模式
性能優(yōu)化:
減少網(wǎng)絡(luò)延遲:傳統(tǒng)的邊車代理模式會(huì)引入額外的網(wǎng)絡(luò)跳數(shù),因?yàn)槊總€(gè)請(qǐng)求都需要通過邊車代理進(jìn)行處理。通過Java Agent直接將服務(wù)網(wǎng)格功能注入到應(yīng)用程序中,可以減少這些額外的網(wǎng)絡(luò)開銷,從而降低延遲。
降低資源消耗:不再需要運(yùn)行額外的邊車代理,從而減少了CPU、內(nèi)存和網(wǎng)絡(luò)資源的占用。這對(duì)需要高效利用資源的應(yīng)用非常重要。
簡(jiǎn)化運(yùn)維:
統(tǒng)一管理:通過Java Agent實(shí)現(xiàn)Proxyless模式,所有服務(wù)網(wǎng)格相關(guān)的配置和管理可以集中在控制平面進(jìn)行,而無需在每個(gè)服務(wù)實(shí)例中單獨(dú)配置邊車代理。這簡(jiǎn)化了運(yùn)維工作,特別是在大型分布式系統(tǒng)中。
減少環(huán)境復(fù)雜性:通過消除邊車代理的配置和部署,環(huán)境的復(fù)雜性降低,減少了可能出現(xiàn)的配置錯(cuò)誤或版本不兼容問題。
數(shù)據(jù)局面升級(jí):Java Agent作為服務(wù)治理數(shù)據(jù)面,天然與應(yīng)用程序解耦,這點(diǎn)是相對(duì)于SDK的最大優(yōu)點(diǎn)。當(dāng)數(shù)據(jù)面面臨版本升級(jí)迭代時(shí),可以統(tǒng)一管控而不依賴于用戶應(yīng)用的重新打包構(gòu)建。
靈活性:
無需修改源代碼與現(xiàn)有生態(tài)系統(tǒng)兼容:Java Agent可以在運(yùn)行時(shí)對(duì)應(yīng)用程序進(jìn)行字節(jié)碼操作,直接在字節(jié)碼層面插入服務(wù)網(wǎng)格相關(guān)的邏輯,而無需開發(fā)者修改應(yīng)用程序的源代碼。這使得現(xiàn)有應(yīng)用能夠輕松集成Proxyless模式。
動(dòng)態(tài)加載和卸載:Java Agent可以在應(yīng)用程序啟動(dòng)時(shí)或運(yùn)行時(shí)動(dòng)態(tài)加載和卸載。這意味著服務(wù)網(wǎng)格功能可以靈活地添加或移除,適應(yīng)不同的運(yùn)行時(shí)需求。
適用性廣:
支持遺留系統(tǒng):對(duì)于無法修改源代碼的遺留系統(tǒng),Java Agent是一種理想的方式,能夠?qū)F(xiàn)代化的服務(wù)網(wǎng)格功能集成到老舊系統(tǒng)中,提升其功能和性能。
通過Java Agent實(shí)現(xiàn)Proxyless模式,能夠在保持現(xiàn)有系統(tǒng)穩(wěn)定性的同時(shí),享受服務(wù)網(wǎng)格帶來的強(qiáng)大功能,是一種高效且靈活的解決方案。
3.2 微內(nèi)核架構(gòu)概述
核心組件:框架的核心組件更多的定義核心功能接口的抽象設(shè)計(jì),模型的定義以及agent加載與類隔離等核心功能,為達(dá)到最小化依賴,很多核心功能都是基于自研代碼實(shí)現(xiàn)。具體可參見joylive-core代碼模塊。
插件化設(shè)計(jì):使用了模塊化和插件化的設(shè)計(jì),分別抽象了像保護(hù)插件,注冊(cè)插件,路由插件,透?jìng)鞑寮蓉S富的插件生態(tài),極大的豐富了框架的可擴(kuò)展性,為適配多樣化的開源生態(tài)奠定了基礎(chǔ)。具體可參見joylive-plugin代碼模塊。
3.3 插件擴(kuò)展體系
項(xiàng)目基于Java的SPI機(jī)制實(shí)現(xiàn)了插件化的擴(kuò)展方式,這也是Java生態(tài)的主流方式。
3.3.1 定義擴(kuò)展
定義擴(kuò)展接口,并使用@Extensible注解來進(jìn)行擴(kuò)展的聲明。下面是個(gè)負(fù)載均衡擴(kuò)展示例:
@Extensible("LoadBalancer")public interface LoadBalancer {int ORDER_RANDOM_WEIGHT = 0;int ORDER_ROUND_ROBIN = ORDER_RANDOM_WEIGHT + 1;default <T extends Endpoint> T choose(List<T> endpoints, Invocation<?> invocation) {Candidate<T> candidate = elect(endpoints, invocation);return candidate == null ? null : candidate.getTarget();}<T extends Endpoint> Candidate<T> elect(List<T> endpoints, Invocation<?> invocation);}
3.3.2 實(shí)現(xiàn)擴(kuò)展
實(shí)現(xiàn)擴(kuò)展接口,并使用@Extension注解來進(jìn)行擴(kuò)展實(shí)現(xiàn)的聲明。如下是實(shí)現(xiàn)了LoadBalancer接口的實(shí)現(xiàn)類:
@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<Long, AtomicLong> COUNTER_FUNC = s -> new AtomicLong(0L);private final Map<Long, AtomicLong> counters = new ConcurrentHashMap<>();private final AtomicLong global = new AtomicLong(0);@Overridepublic <T extends Endpoint> Candidate<T> doElect(List<T> 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注解聲明擴(kuò)展實(shí)現(xiàn),并提供了名稱@ConditionalOnProperty注解聲明啟用的條件,可以組合多個(gè)條件
3.3.3 啟用擴(kuò)展
在 SPI 文件 META-INF/servicescom.jd.live.agent.governance.invoke.loadbalance.LoadBalancer 中配置擴(kuò)展全路徑名com.jd.live.agent.governance.invoke.loadbalance.roundrobin.RoundRobinLoadBalancer 即達(dá)到啟用效果。
更多詳情可查閱:https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/extension.md
3.4 依賴注入設(shè)計(jì)
3.4.1 @Injectable
(ElementType.TYPE)(RetentionPolicy.RUNTIME) Injectable {boolean enable() default true;}
@Injectable注解的實(shí)例進(jìn)行依賴對(duì)象注入。
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;}
3.4.3 @Configurable
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Configurable {String prefix() default "";boolean auto() default false;}
@Injectable,用于指定哪些類啟用自動(dòng)注入配置文件的支持。prefix指定用于配置鍵的前綴。這通常意味著前綴將來自類名或基于某種約定。auto指示配置值是否應(yīng)自動(dòng)注入到注解類的所有合規(guī)字段中。默認(rèn)值為 false,這意味著默認(rèn)情況下未啟用自動(dòng)注入。
3.4.4 @Config
@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Config {String value() default "";boolean nullable() default true;}
nullable指示字段的配置是否是可選的。如果為真,則系統(tǒng)將允許配置缺失而不會(huì)導(dǎo)致錯(cuò)誤。
下面是具體的使用示例:
@Injectable@Extension(value = "CircuitBreakerFilter", order = OutboundFilter.ORDER_CIRCUIT_BREAKER)public class CircuitBreakerFilter implements OutboundFilter, ExtensionInitializer {@Injectprivate Map<String, CircuitBreakerFactory> 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;...}
更多細(xì)節(jié),因?yàn)槠虿辉僬归_。詳情可以了解:https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/extension.md
3.5 字節(jié)碼增強(qiáng)機(jī)制
Java字節(jié)碼增強(qiáng)的主要方法有:
運(yùn)行時(shí)增強(qiáng):使用Java Agent在類加載時(shí)修改字節(jié)碼。
加載時(shí)增強(qiáng):在類加載到JVM之前修改字節(jié)碼。
編譯時(shí)增強(qiáng):在編譯階段修改或生成字節(jié)碼。
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創(chuàng)建增強(qiáng)類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();// 創(chuàng)建增強(qiáng)類的實(shí)例Object enhancedInstance = dynamicType.getDeclaredConstructor().newInstance();// 調(diào)用增強(qiáng)后的方法Method sayHelloMethod = enhancedInstance.getClass().getMethod("sayHello");sayHelloMethod.invoke(enhancedInstance);}}
這個(gè)例子展示了如何使用ByteBuddy來增強(qiáng)SimpleClass的sayHello方法。讓我解釋一下這個(gè)過程:
我們定義了一個(gè)簡(jiǎn)單的
SimpleClass,它有一個(gè)sayHello方法。我們創(chuàng)建了一個(gè)
SimpleInterceptor類,包含了我們想要在原方法執(zhí)行前后添加的邏輯。在
ByteBuddyExample類的main方法中,我們使用ByteBuddy來創(chuàng)建一個(gè)增強(qiáng)的類:我們創(chuàng)建了
SimpleClass的一個(gè)子類。我們攔截了名為"sayHello"的方法。
我們使用
MethodDelegation.to(SimpleInterceptor.class)來添加前置和后置邏輯。我們使用
SuperMethodCall.INSTANCE來確保原始方法被調(diào)用。我們創(chuàng)建了增強(qiáng)類的實(shí)例,并通過反射調(diào)用了
sayHello方法。
當(dāng)你運(yùn)行這個(gè)程序時(shí),輸出將會(huì)是:
Before saying helloHello, World!After saying hello
當(dāng)然,工程級(jí)別的實(shí)現(xiàn)是遠(yuǎn)比上面Demo的組織形式復(fù)雜的。插件是基于擴(kuò)展實(shí)現(xiàn)的,有多個(gè)擴(kuò)展組成,對(duì)某個(gè)框架進(jìn)行特定增強(qiáng),實(shí)現(xiàn)了多活流量治理等等業(yè)務(wù)邏輯。一個(gè)插件打包成一個(gè)目錄,如下圖所示:
.└── 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個(gè)版本,增強(qiáng)了注冊(cè)中心,路由和鏈路透?jìng)鞯哪芰ΑO旅娼榻B一下在joylive-agent中如何實(shí)現(xiàn)一個(gè)字節(jié)碼增強(qiáng)插件。
3.5.1 增強(qiáng)插件定義接口
PluginDefinition。
@Extensible("PluginDefinition")public interface PluginDefinition {ElementMatcher<TypeDesc> getMatcher();InterceptorDefinition[] getInterceptors();}
接口共定義了兩個(gè)方法:getMatcher用于獲取匹配要增強(qiáng)類的匹配器,getInterceptors是返回要增強(qiáng)目標(biāo)類的攔截器定義對(duì)象。
3.5.2 攔截器定義接口
new的方式構(gòu)造蘭機(jī)器定義實(shí)現(xiàn)。攔截器定義接口如下:
public interface InterceptorDefinition {ElementMatcher<MethodDesc> getMatcher();Interceptor getInterceptor();}
該接口同樣抽象了兩個(gè)方法:getMatcher用于獲取匹配要增強(qiáng)方法的匹配器,getInterceptor是用于返回具體的增強(qiáng)邏輯實(shí)現(xiàn),我們稱之為攔截器(Interceptor)。
3.5.3 攔截器接口
public interface Interceptor {void onEnter(ExecutableContext ctx);void onSuccess(ExecutableContext ctx);void onError(ExecutableContext ctx);void onExit(ExecutableContext ctx);}
ExecutableContext也是非常重要的組成部分,它承載了運(yùn)行時(shí)的上下文信息,并且針對(duì)不同的增強(qiáng)目標(biāo)我們做了不同的實(shí)現(xiàn)。如下圖所示:
更多詳情可查看:https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/plugin.md
3.6 類加載與類隔離
3.7 面向請(qǐng)求的抽象
請(qǐng)求接口
請(qǐng)求抽象實(shí)現(xiàn)
具體框架的請(qǐng)求適配,如Dubbo
DubboOutboundRequest與DubboInboundRequest,體現(xiàn)了一個(gè)OutboundRequest與InboundRequest的實(shí)現(xiàn)與繼承關(guān)系。整體看起來確實(shí)比較復(fù)雜。這是因?yàn)樵谡?qǐng)求抽象時(shí),不僅要考慮請(qǐng)求是Inbound還是Outbound,還要適配不同的協(xié)議框架。例如,像Dubbo和JSF這樣的私有通訊協(xié)議框架需要統(tǒng)一為RpcRequest接口的實(shí)現(xiàn),而SpringCloud這樣的HTTP通訊協(xié)議則統(tǒng)一為HttpRequest。再加上Inbound和Outbound的分類維度,整體的抽象在追求高擴(kuò)展性的同時(shí)也增加了復(fù)雜性。
4. 核心功能
下面提供的流量治理功能,以API網(wǎng)關(guān)作為東西向流量第一入口進(jìn)行流量識(shí)別染色。在很大程度上,API網(wǎng)關(guān)作為東西向流量識(shí)別的第一入口發(fā)揮了重要作用。API網(wǎng)關(guān)在接收到南北向流量后,后續(xù)將全部基于東西向流量治理。
4.1 多活模型及流量調(diào)度
4.1.1 多活空間
.└── 多活空間├── 單元路由變量(*)├── 單元(*)│ ├── 分區(qū)(*)├── 單元規(guī)則(*)├── 多活域名(*)│ ├── 單元子域名(*)│ ├── 路徑(*)│ │ ├── 業(yè)務(wù)參數(shù)(*)
4.1.2 單元
單元是邏輯上的概念,一般對(duì)應(yīng)一個(gè)地域。常用于異地多活場(chǎng)景,通過用戶維度拆分業(yè)務(wù)和數(shù)據(jù),每個(gè)單元獨(dú)立運(yùn)作,降低單元故障對(duì)整體業(yè)務(wù)的影響。
單元的屬性包括單元代碼、名稱、類型(中心單元或普通單元)、讀寫權(quán)限、標(biāo)簽(如地域和可用區(qū))、以及單元下的分區(qū)。
單元分區(qū)是單元的組成部分,單元內(nèi)的邏輯分區(qū),對(duì)應(yīng)云上的可用區(qū)或物理數(shù)據(jù)中心,屬性類似單元。
4.1.3 路由變量
路由變量是決定流量路由到哪個(gè)單元的依據(jù),通常是用戶賬號(hào)。每個(gè)變量可以通過不同的取值方式(如Cookie、請(qǐng)求頭等)獲取,且可以定義轉(zhuǎn)換函數(shù)來獲取實(shí)際用戶標(biāo)識(shí)。
變量取值方式則描述如何從請(qǐng)求參數(shù)、請(qǐng)求頭或Cookie中獲取路由變量。
4.1.4 單元規(guī)則
單元規(guī)則定義了單元和分區(qū)之間的流量調(diào)度規(guī)則。根據(jù)路由變量計(jì)算出的值,通過取模判斷流量應(yīng)路由到哪個(gè)單元。它的屬性包括多活類型、變量、變量取值方式、計(jì)算函數(shù)、變量缺失時(shí)的操作、以及具體的單元路由規(guī)則。
單元路由規(guī)則定義了單元內(nèi)的流量路由規(guī)則,包括允許的路由變量白名單、前綴、值區(qū)間等。
分區(qū)路由規(guī)則定義了單元內(nèi)各個(gè)分區(qū)的流量路由規(guī)則,包括允許的變量白名單、前綴及權(quán)重。
4.1.5 多活域名
多活域名描述啟用多活的域名,用于在網(wǎng)關(guān)層進(jìn)行流量攔截和路由。支持跨地域和同城多活的流量管理,配置路徑規(guī)則以匹配請(qǐng)求路徑并執(zhí)行相應(yīng)的路由規(guī)則。
單元子域名描述各個(gè)單元的子域名,通常用于在HTTP請(qǐng)求或回調(diào)時(shí)閉環(huán)在單元內(nèi)進(jìn)行路由。
路徑規(guī)則定義了根據(jù)請(qǐng)求路徑匹配的路由規(guī)則,取最長(zhǎng)匹配路徑來選擇適用的路由規(guī)則。
業(yè)務(wù)參數(shù)規(guī)則基于請(qǐng)求參數(shù)值進(jìn)一步精細(xì)化路由,選擇特定的單元規(guī)則。
4.1.6 模型骨架
以下是多活治理模型的基本配置樣例,包括API版本、空間名稱、單元、域名、規(guī)則、變量等。
[{"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": []}}]
以上概念會(huì)比較晦澀難懂,更多詳情可以訪問:https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/livespace.md
4.2 全鏈路灰度(泳道)
泳道是一種隔離和劃分系統(tǒng)中不同服務(wù)或組件的方式,類似于游泳池中的泳道劃分,確保每個(gè)服務(wù)或組件在自己的“泳道”中獨(dú)立運(yùn)作。泳道概念主要用于以下幾種場(chǎng)景:
多租戶架構(gòu)中的隔離
在多租戶系統(tǒng)中,泳道通常用于隔離不同租戶的資源和服務(wù)。每個(gè)租戶有自己的獨(dú)立“泳道”,以確保數(shù)據(jù)和流量的隔離,防止不同租戶之間的相互影響。
流量隔離與管理
泳道可以用于根據(jù)特定規(guī)則(例如用戶屬性、地理位置、業(yè)務(wù)特性等)將流量分配到不同的微服務(wù)實(shí)例或集群中。這種方式允許團(tuán)隊(duì)在某些條件下測(cè)試新版本、進(jìn)行藍(lán)綠部署、金絲雀發(fā)布等,而不會(huì)影響到其他泳道中的流量。
業(yè)務(wù)邏輯劃分
在某些場(chǎng)景下,泳道也可以代表業(yè)務(wù)邏輯的劃分。例如,一個(gè)電商平臺(tái)可能會(huì)針對(duì)不同的用戶群體(如普通用戶、VIP用戶)提供不同的服務(wù)路徑和處理邏輯,形成不同的泳道。
版本管理
泳道可以用來管理微服務(wù)的不同版本,使得新版本和舊版本可以在不同的泳道中并行運(yùn)行,從而降低升級(jí)時(shí)的風(fēng)險(xiǎn)。
開發(fā)和測(cè)試
在開發(fā)和測(cè)試過程中,不同的泳道可以用于隔離開發(fā)中的新功能、測(cè)試環(huán)境、甚至是不同開發(fā)團(tuán)隊(duì)的工作,從而減少互相干擾。
泳道的核心目的是通過隔離服務(wù)或資源,提供獨(dú)立性和靈活性,確保系統(tǒng)的穩(wěn)定性和可擴(kuò)展性。這種隔離機(jī)制幫助組織更好地管理復(fù)雜系統(tǒng)中的多樣性,尤其是在處理高并發(fā)、多租戶、或者需要快速迭代的場(chǎng)景下。功能流程如下圖所示:
模型定義及更多詳情可以訪問:https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/lane.md
4.3 微服務(wù)治理策略
由于篇幅原因,具體的治理策略與模型就不詳盡展開介紹了,下圖概括了服務(wù)治理的全貌。
值得一提有兩點(diǎn):
策略的實(shí)現(xiàn)屏蔽了底層框架的差異性,這得益于上面提到的面向請(qǐng)求的抽象。
統(tǒng)一治理層級(jí)的劃分,多層級(jí)的策略掛載框架允許治理策略可以靈活的控制策略生效的影響半徑。
統(tǒng)一HTTP和傳統(tǒng)RPC的治理策略配置層級(jí)具體細(xì)節(jié)如下:
.└── 服務(wù)├── 分組*│ ├── 路徑*│ │ ├── 方法*
服務(wù)治理策略放在分組、路徑和方法上,可以逐級(jí)設(shè)置,下級(jí)默認(rèn)繼承上級(jí)的配置。服務(wù)的默認(rèn)策略設(shè)置到默認(rèn)分組default上。
模型定義及更多詳情可以訪問:https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/governance.md
5. 功能實(shí)現(xiàn)示例
5.1 服務(wù)注冊(cè)
5.1.1 服務(wù)注冊(cè)
@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 {// ......@Overridepublic void onSuccess(ExecutableContext ctx) {MethodContext methodContext = (MethodContext) ctx;Map<String, String> map = (Map<String, String>) methodContext.getResult();application.label(map::putIfAbsent);// ......}}
上面例子所呈現(xiàn)的效果是,當(dāng)dubbo應(yīng)用啟動(dòng)時(shí),增強(qiáng)插件攔截dubbo框架org.apache.dubbo.config.ServiceConfig中的buildAttributes方法進(jìn)行增強(qiáng)處理。從ServiceConfigInterceptor的實(shí)現(xiàn)中可以看出,當(dāng)buildAttributes方法執(zhí)行成功后,對(duì)該方法的返回的Map對(duì)象繼續(xù)增加了框架額外的元數(shù)據(jù)標(biāo)簽。
5.1.2 服務(wù)策略訂閱
ServiceConfigInterceptor的增強(qiáng)會(huì)發(fā)現(xiàn),在給注冊(cè)示例打標(biāo)之后,還有一部分邏輯,如下:
public class ServiceConfigInterceptor extends InterceptorAdaptor {@Overridepublic 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所執(zhí)行的是策略訂閱邏輯。因?yàn)椴呗允侵С譄岣虏?shí)時(shí)生效的,策略訂閱邏輯便是開啟了訂閱當(dāng)前服務(wù)在控制臺(tái)所配置策略的邏輯。
5.2 流量控制
5.2.1 入流量攔截點(diǎn)
入流量攔截也就是攔截Inbound請(qǐng)求,攔截相關(guān)框架的入流量處理鏈的入口或靠前的處理器的相關(guān)邏輯并予以增強(qiáng)。下面以Dubbo3的攔截點(diǎn)為例。
@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;}@Overridepublic 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 <R extends InboundRequest> void inbound(InboundInvocation<R> invocation) {InboundFilterChain.Chain chain = new InboundFilterChain.Chain(getInboundFilters());chain.filter(invocation);}}
org.apache.dubbo.rpc.filter.ClassLoaderFilter的invoke方法作為攔截點(diǎn),織入我們統(tǒng)一的InboundFilterChain對(duì)象作為入流量處理鏈。我們可以根據(jù)需求實(shí)現(xiàn)不同的InboundFilter即可,我們內(nèi)置了一部分實(shí)現(xiàn)。如下所示:
5.2.2 出流量攔截點(diǎn)
出流量攔截也就是攔截Outbound請(qǐng)求,攔截相關(guān)框架的出流量處理鏈的入口并予以增強(qiáng)。下面以Dubbo2的攔截點(diǎn)為例。
如果只開啟了多活或泳道治理,則只需對(duì)后端實(shí)例進(jìn)行過濾,可以攔截負(fù)載均衡或服務(wù)實(shí)例提供者相關(guān)方法
-
@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) ) }; }}
攔截器里面調(diào)用上下文的路由方法
public class LoadBalanceInterceptor extends InterceptorAdaptor {// ......@Overridepublic void onEnter(ExecutableContext ctx) {MethodContext mc = (MethodContext) ctx;Object[] arguments = ctx.getArguments();List<Invoker<?>> invokers = (List<Invoker<?>>) arguments[2];List<Invoker<?>> invoked = (List<Invoker<?>>) 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<DubboEndpoint<?>> 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);}}
如果開啟了微服務(wù)治理,則設(shè)計(jì)到重試,需要對(duì)集群調(diào)用進(jìn)行攔截
@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))};}}
攔截器里面構(gòu)造集群對(duì)象進(jìn)行同步或異步調(diào)用
public class ClusterInterceptor extends InterceptorAdaptor {// ......@Overridepublic void onEnter(ExecutableContext ctx) {MethodContext mc = (MethodContext) ctx;Object[] arguments = ctx.getArguments();DubboCluster3 cluster = clusters.computeIfAbsent((AbstractClusterInvoker<?>) ctx.getTarget(), DubboCluster3::new);List<Invoker<?>> invokers = (List<Invoker<?>>) arguments[1];List<DubboEndpoint<?>> 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);}}
同樣,出流量攔截也是采用了責(zé)任鏈模式設(shè)計(jì)了OutboundFilterChain,用戶可以根據(jù)自己的需求擴(kuò)展實(shí)現(xiàn)OutboundFilter,目前針對(duì)已支持功能內(nèi)置了部分實(shí)現(xiàn),如下所示:
6. 部署實(shí)踐
6.1 基于Kubernates實(shí)踐場(chǎng)景
joylive-injector是針對(duì)K8s場(chǎng)景打造的自動(dòng)注入組件。joylive-injector是基于kubernetes的動(dòng)態(tài)準(zhǔn)入控制webhook,它可以用于修改kubernete資源。它會(huì)監(jiān)視工作負(fù)載(如deployments)的CREATE、UPDATE、DELETE事件和pods的CREATE事件,并為POD添加initContainer、默認(rèn)增加環(huán)境變量JAVA_TOOL_OPTIONS、掛載configmap、修改主容器的卷裝載等操作。目前已支持的特性如下:
支持自動(dòng)將
joylive-agent注入應(yīng)用的Pod。支持多版本
joylive-agent與對(duì)應(yīng)配置管理。支持注入指定版本
joylive-agent及對(duì)應(yīng)配置。
joylive-agent變得非常簡(jiǎn)單,在安裝joylive-injector組件后,只需要在對(duì)應(yīng)的deployment文件中加入標(biāo)簽x-live-enabled: "true"即可,如下所示:
apiVersion: apps/v1kind: Deploymentmetadata:labels:app: joylive-demo-springcloud2021-provider x-live-enabled: "true"name: joylive-demo-springcloud2021-providerspec:replicas: 1selector: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: 18081name: 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
啟動(dòng)后Pod如下圖所示即代表注入成功,隨后觀察應(yīng)用日志及功能測(cè)試即可。
掃一掃,加入技術(shù)交流群
