Proxyless的多活流量和微服务治理
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 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
接口的实现类:
@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);
@Override
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)