01 引言
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
02 微服务架构演进及优缺点
1.1 项目的背景及意义
服务间通信的复杂性:不同服务之间需要进行可靠的通信,处理失败重试、负载均衡等问题。
故障的容错处理:系统的复杂性给与运维及故障处理带来更大的挑战,如何快速处理故障解决线上问题,这是考验一个企业基础设施建设的重要关卡。
代码侵入性:需要在每个服务的代码中集成和配置各种库,增加了代码的复杂性和维护成本。
一致性问题:不同服务可能使用不同版本的库,导致治理逻辑不一致,SDK的升级难度凸显。
1.2 项目概述
项目地址:https://github.com/jd-opensource/joylive-agent
微服务架构演进及优缺点
2.1 单体架构阶段
最初,大多数应用都是作为单体应用开发的。所有功能都集中在一个代码库中,部署也是作为一个整体。这种形式也是我们学习编程之初,最原始的模样。确切的说,这种形态并不属于微服务。如下图所示:
优点: 简单、易于开发和测试,适合小团队和小规模应用。
2.2 垂直拆分阶段
随着应用规模的成长,此时会考虑将每个功能模块(服务)拆分为独立的应用,也就是垂直拆分,拥有自己的代码库、数据库和部署生命周期。服务之间通过轻量级协议(如HTTP、gRPC)通信。也就是正式开启了面向服务的架构(SOA)。这种形态体现为:服务发现通过DNS解析,Consumer与Provider之间会有LB进行流量治理。服务间通过API进行通信。如下图所示:
优点: 独立部署和扩展,每个服务可以由独立的团队开发和维护,提高了敏捷性。
2.3 微服务成熟阶段
这个阶段引入更多的微服务治理和管理工具,使用专业的微服务框架或中间件,通过专门定制的微服务通讯协议,让应用取得更高的吞吐性能。如API网关、注册中心、分布式追踪等。DevOps和持续集成/持续部署(CI/CD)流程成熟。代表产物如Spring Cloud,Dubbo等。此时典型的微服务场景还都是具体的微服务SDK提供的治理能力。通讯流程为:SDK负责向注册中心注册当前服务信息,当需要进行服务消费时,同样向注册中心请求服务提供者信息,然后直连服务提供者IP及端口并发送请求。如下图所示:
优点: 高度可扩展、弹性和灵活性,支持高频率的发布和更新。
2.4 服务网格架构
随着云原生容器化时代的到来,服务网格是一种专门用于管理微服务之间通信的基础设施层。它通常包含一组轻量级的网络代理(通常称为 sidecar),这些代理与每个服务实例一起部署,这利用了K8s中Pod的基础能力。服务网格负责处理服务间的通信、流量管理、安全性、监控和弹性等功能。这种微服务治理方式也可以称之为Proxy模式,其中SideCar即作为服务之间的Proxy。如下图所示:
优点: 主要优点是解耦业务逻辑与服务治理的能力,通过集中控制平面(control plane)简化了运维管理。
缺点: 增加了资源消耗,更高的运维挑战。
项目架构设计
3.1 Proxyless模式
性能优化:
减少网络延迟:传统的边车代理模式会引入额外的网络跳数,因为每个请求都需要通过边车代理进行处理。通过Java Agent直接将服务网格功能注入到应用程序中,可以减少这些额外的网络开销,从而降低延迟。
降低资源消耗:不再需要运行额外的边车代理,从而减少了CPU、内存和网络资源的占用。这对需要高效利用资源的应用非常重要。
简化运维:
统一管理:通过Java Agent实现Proxyless模式,所有服务网格相关的配置和管理可以集中在控制平面进行,而无需在每个服务实例中单独配置边车代理。这简化了运维工作,特别是在大型分布式系统中。
减少环境复杂性:通过消除边车代理的配置和部署,环境的复杂性降低,减少了可能出现的配置错误或版本不兼容问题。
数据局面升级:Java Agent作为服务治理数据面,天然与应用程序解耦,这点是相对于SDK的最大优点。当数据面面临版本升级迭代时,可以统一管控而不依赖于用户应用的重新打包构建。
灵活性:
无需修改源代码与现有生态系统兼容:Java Agent可以在运行时对应用程序进行字节码操作,直接在字节码层面插入服务网格相关的逻辑,而无需开发者修改应用程序的源代码。这使得现有应用能够轻松集成Proxyless模式。
动态加载和卸载:Java Agent可以在应用程序启动时或运行时动态加载和卸载。这意味着服务网格功能可以灵活地添加或移除,适应不同的运行时需求。
适用性广:
支持遗留系统:对于无法修改源代码的遗留系统,Java Agent是一种理想的方式,能够将现代化的服务网格功能集成到老旧系统中,提升其功能和性能。
3.2 微内核架构概述
joylive-core
代码模块。joylive-plugin
代码模块。3.3 插件扩展体系
3.3.1 定义扩展
@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 实现扩展
实现扩展接口,并使用@Extension
注解来进行扩展实现的声明。如下是实现了LoadBalancer
接口的实现类:
(value = RoundRobinLoadBalancer.LOAD_BALANCER_NAME, order = LoadBalancer.ORDER_ROUND_ROBIN)
(value = {
true), (value = GovernanceConfig.CONFIG_LIVE_ENABLED, matchIfMissing =
true), (value = GovernanceConfig.CONFIG_LANE_ENABLED, matchIfMissing =
true) (value = GovernanceConfig.CONFIG_FLOW_CONTROL_ENABLED, matchIfMissing =
}, 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);
public <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
注解声明扩展实现,并提供了名称@ConditionalOnProperty
注解声明启用的条件,可以组合多个条件
3.3.3 启用扩展
META-INF/services/com.jd.live.agent.governance.invoke.loadbalance.LoadBalancer
中配置扩展全路径名com.jd.live.agent.governance.invoke.loadbalance.roundrobin.RoundRobinLoadBalancer
即达到启用效果。3.4 依赖注入设计
3.4.1 @Injectable
(ElementType.TYPE)
(RetentionPolicy.RUNTIME)
public Injectable {
boolean enable() default true;
}
这是一个非常简洁的可以应用于类、接口(包括注解类型)或枚举注解。其目的为了标识哪些类开启了自动注入对象的要求。这点不同于Spring的控制范围,而是按需注入。实例构建完成后,在自动注入的逻辑过程中会针对添加@Injectable
注解的实例进行依赖对象注入。
3.4.2 @Inject
(ElementType.FIELD)
(RetentionPolicy.RUNTIME)
public Inject {
String value() default "";
boolean nullable() default false;
ResourcerType loader() default ResourcerType.CORE_IMPL;
}
3.4.3 @Configurable
(ElementType.TYPE)
(RetentionPolicy.RUNTIME)
public Configurable {
String prefix() default "";
boolean auto() default false;
}
@Injectable
,用于指定哪些类启用自动注入配置文件的支持。prefix
指定用于配置键的前缀。这通常意味着前缀将来自类名或基于某种约定。auto
指示配置值是否应自动注入到注解类的所有合规字段中。默认值为 false,这意味着默认情况下未启用自动注入。3.4.4 @Config
(ElementType.FIELD)
(RetentionPolicy.RUNTIME)
public Config {
String value() default "";
boolean nullable() default true;
}
该注解用于指定字段配置详细信息。它定义了字段的配置键以及配置是否为可选。此注释可用于在运行时自动将配置值加载到字段中,并支持指定缺少配置(无配置)是否被允许。nullable
指示字段的配置是否是可选的。如果为真,则系统将允许配置缺失而不会导致错误。
下面是具体的使用示例:
public class CircuitBreakerFilter implements OutboundFilter, ExtensionInitializer {
private Map<String, CircuitBreakerFactory> factories;
private CircuitBreakerFactory defaultFactory;
private GovernanceConfig governanceConfig;
...
}
public class Application {
private String name;
private AppService service;
...
}
更多细节,因为篇幅原因不再展开。详情可以了解:https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/extension.md
3.5 字节码增强机制
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
World!
After saying hello
.
└── 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<TypeDesc> getMatcher();
InterceptorDefinition[] getInterceptors();
}
接口共定义了两个方法:getMatcher
用于获取匹配要增强类的匹配器,getInterceptors
是返回要增强目标类的拦截器定义对象。
3.5.2 拦截器定义接口
拦截器定义接口主要是用来确定拦截增强位置(也就是方法),定位到具体方法也就找到了具体增强逻辑执行的位置。拦截器定义接口并没有采用扩展机制,这是因为具体到某个增强目标类后,要增强的方法与增强逻辑已经是确定行为,不再需要通过扩展机制实例化对象,在具体的增强插件定义接口实现里会直接通过new
的方式构造兰机器定义实现。拦截器定义接口如下:
public interface InterceptorDefinition {
ElementMatcher<MethodDesc> 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
也是非常重要的组成部分,它承载了运行时的上下文信息,并且针对不同的增强目标我们做了不同的实现。如下图所示:3.6 类加载与类隔离
类加载的原理比较容易理解,因为在Java Agent模式下,应用启动时agent需要加载它所依赖的jar包。然而,如果这些类在加载后被用户应用的类加载器感知到,就可能导致类冲突甚至不兼容的风险。因此,引入类隔离机制是为了解决这个问题。类隔离的实现原理并不复杂。首先,需要实现自定义的类加载器;其次,需要打破默认的双亲委派机制。通过这两步,类隔离可以实现多层次的隔离,从而避免类冲突和不兼容问题。如下图所示:
3.7 面向请求的抽象
请求接口
请求抽象实现
具体框架的请求适配,如Dubbo
如上图所示,展现了适配Dubbo的请求对象的DubboOutboundRequest
与DubboInboundRequest
,体现了一个OutboundRequest与InboundRequest的实现与继承关系。整体看起来确实比较复杂。这是因为在请求抽象时,不仅要考虑请求是Inbound还是Outbound,还要适配不同的协议框架。例如,像Dubbo和JSF这样的私有通讯协议框架需要统一为RpcRequest接口的实现,而SpringCloud这样的HTTP通讯协议则统一为HttpRequest。再加上Inbound和Outbound的分类维度,整体的抽象在追求高扩展性的同时也增加了复杂性。
下面提供的流量治理功能,以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 全链路灰度(泳道)
多租户架构中的隔离
流量隔离与管理
业务逻辑划分
版本管理
开发和测试
泳道的核心目的是通过隔离服务或资源,提供独立性和灵活性,确保系统的稳定性和可扩展性。这种隔离机制帮助组织更好地管理复杂系统中的多样性,尤其是在处理高并发、多租户、或者需要快速迭代的场景下。功能流程如下图所示:
4.3 微服务治理策略
由于篇幅原因,具体的治理策略与模型就不详尽展开介绍了,下图概括了服务治理的全貌。
值得一提有两点:
策略的实现屏蔽了底层框架的差异性,这得益于上面提到的面向请求的抽象。
统一治理层级的划分,多层级的策略挂载框架允许治理策略可以灵活的控制策略生效的影响半径。
统一HTTP和传统RPC的治理策略配置层级具体细节如下:
.
└── 服务
├── 分组*
│ ├── 路径*
│ │ ├── 方法*
default
上。类型 | 服务 | 分组 | 路径 | 方法 |
---|---|---|---|---|
HTTP | 域名 | 分组 | URL路径 | HTTP方法 |
RPC 应用级注册 | 应用名 | 分组 | 接口名 | 方法名 |
RPC 接口级注册 | 接口名 | 分组 | / | 方法名 |
模型定义及更多详情可以访问:https://github.com/jd-opensource/joylive-agent/blob/main/docs/cn/governance.md
功能实现示例
5.1 服务注册
5.1.1 服务注册
在应用启动过程中,注册插件会拦截获取到消费者和服务提供者的初始化方法,会修改其元数据,增加多活和泳道的标签。后续往注册中心注册的时候就会带有相关的标签了。这是框架所有治理功能的前提基础。以下是Dubbo服务提供者注册样例:
"ServiceConfigDefinition_v3", order = PluginDefinition.ORDER_REGISTRY) (value =
(value = {
true), (value = GovernanceConfig.CONFIG_LIVE_ENABLED, matchIfMissing =
true), (value = GovernanceConfig.CONFIG_LANE_ENABLED, matchIfMissing =
true) (value = GovernanceConfig.CONFIG_FLOW_CONTROL_ENABLED, matchIfMissing =
}, relation = ConditionalRelation.OR) (ServiceConfigDefinition.TYPE_CONSUMER_CONTEXT_FILTER)
(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<String, String> map = (Map<String, String>) 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 {
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的拦截点为例。
"ClassLoaderFilterDefinition_v3") (value =
true) (value = GovernanceConfig.CONFIG_LIVE_ENABLED, matchIfMissing =
true) (value = GovernanceConfig.CONFIG_LIVE_DUBBO_ENABLED, matchIfMissing =
true) (value = GovernanceConfig.CONFIG_REGISTRY_ENABLED, matchIfMissing =
true) (value = GovernanceConfig.CONFIG_TRANSMISSION_ENABLED, matchIfMissing =
(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;
}
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 <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
方法作为拦截点,织入我们统一的InboundFilterChain
对象作为入流量处理链。我们可以根据需求实现不同的InboundFilter即可,我们内置了一部分实现。如下所示:过滤器 | 名称 | 说明 |
---|---|---|
RateLimitInboundFilter | 限流过滤器 | 根据当前服务的限流策略来进行限流 |
ConcurrencyLimitInboundFilter | 并发过滤器 | 根据当前服务的并发策略来进行限流 |
ReadyInboundFilter | 治理就绪过滤器 | 判断治理状态,只有就绪状态才能进入流量 |
UnitInboundFilter | 单元过滤器 | 判断当前请求是否匹配当前单元,以及当前单元是否可以访问 |
CellInboundFilter | 分区过滤器 | 判断当前分区是否可以访问 |
FailoverInboundFilter | 纠错过滤器 | 目前对错误流量只实现了拒绝 |
5.2.2 出流量拦截点
1. 如果只开启了多活或泳道治理,则只需对后端实例进行过滤,可以拦截负载均衡或服务实例提供者相关方法
"LoadBalanceDefinition_v2.7") (value =
(value = {
(name = {
GovernanceConfig.CONFIG_LIVE_ENABLED,
GovernanceConfig.CONFIG_LANE_ENABLED
}, matchIfMissing = true, relation = ConditionalRelation.OR),
"false"), (name = GovernanceConfig.CONFIG_FLOW_CONTROL_ENABLED, value =
true) (name = GovernanceConfig.CONFIG_LIVE_DUBBO_ENABLED, matchIfMissing =
}, relation = ConditionalRelation.AND)
(LoadBalanceDefinition.TYPE_ABSTRACT_CLUSTER)
(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<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(((DubboEndpoint0)).getInvoker()); >) endpoints.get(
} else {
mc.setThrowable(cluster.createNoProviderException(request));
}
} catch (RejectException e) {
mc.setThrowable(cluster.createRejectException(e, request));
}
mc.setSkip(true);
}
}
"ClusterDefinition_v2.7") (value =
true) (name = GovernanceConfig.CONFIG_FLOW_CONTROL_ENABLED, matchIfMissing =
true) (name = GovernanceConfig.CONFIG_LIVE_DUBBO_ENABLED, matchIfMissing =
(ClusterDefinition.TYPE_ABSTRACT_CLUSTER)
(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<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);
}
}
同样,出流量拦截也是采用了责任链模式设计了OutboundFilterChain
,用户可以根据自己的需求扩展实现OutboundFilter
,目前针对已支持功能内置了部分实现,如下所示:
过滤器 | 名称 | 说明 |
---|---|---|
StickyFilter | 粘连过滤器 | 根据服务的粘连策略进行过滤 |
LocalhostFilter | 本机过滤器 | 本地开发调试插件 |
HealthyFilter | 健康过滤器 | 根据后端实例的健康状态进行过滤 |
VirtualFilter | 虚拟节点过滤器 | 复制出指定数量的节点,用于开发测试 |
UnitRouteFilter | 单元路由过滤器 | 根据多活路由规则及微服务的多活策略,根据请求的目标单元进行过滤 |
TagRouteFilter | 标签路由过滤器 | 根据服务配置的标签路由策略进行过滤 |
LaneFilter | 泳道过滤器 | 根据泳道策略进行过滤 |
CellRouteFilter | 分区路由过滤器 | 根据多活路由规则及微服务的多活策略,根据请求的目标分区进行过滤 |
RetryFilter | 重试过滤器 | 尝试过滤掉已经重试过的节点 |
LoadBalanceFilter | 负载均衡过滤器 | 根据服务配置的负载均衡策略进行路由 |
部署实践
基于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/v1
kind: Deployment
metadata:
labels:
app: joylive-demo-springcloud2021-provider
"true" :
name: joylive-demo-springcloud2021-provider
spec:
replicas: 1
selector:
matchLabels:
app: joylive-demo-springcloud2021-provider
template:
metadata:
labels:
app: joylive-demo-springcloud2021-provider
"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
我们有更高的目标和方向,希望更多有志于打造开源优品的朋友加入进来(仓库首页有微信群二维码),一起为开源事业贡献自己的光与热。