深度剖析线上应用节点流量隔离技术
作者:谢文欣(风敬)
为什么要做流量隔离
源于一个 EDAS 客户遇到的棘手情况:他们线上的一个 Pod CPU 指标异常,为了进一步诊断问题,客户希望在不重建此 Pod 的情况下保留现场,但诊断期间流量还会经过这个异常 Pod,导致影响服务质量,于是询问我们有没有办法可以把流入异常节点的流量摘除掉,形成一个隔离的诊断环境。经诊断后,如果异常可以修复,待修复完成后,再解除流量隔离,节点恢复正常工作。
除了在诊断场景需要对所有输入流量进行隔离外,在一些线上演练中还需对特定流量进行隔离以实现模拟演练效果。面对这类流量隔离问题时,我们首先考虑的是全链路流量控制。目前,EDAS 上的全链路流控能够在不重启应用节点的情况下控制流量走向。然而,全链路流控仅能控制微服务框架流量,无法满足隔离所有或特定流量的需求。
为此,我们进行了深入研究,实现了一套开箱即用的流量隔离工具,能够动态隔离特定流量,并在隔离后可随时恢复,以满足各种场景下的流量隔离需求。
隔离哪些流量
流量隔离的目的是阻断应用节点的流入流量,首先明确下微服务应用节点流入的流量有哪些。
流入微服务应用节点的流量大致可以分为两大类:服务流量、事件流量。以常见的微服务应用为例,其流量组成如下图所示。
服务流量指一个微服务应用的所有节点作为一个网络实体,对外提供一组服务,被其他系统、服务或用户发起请求产生的调用。对于服务流量,节点本身不直接决定流量的流入与否,而是由一套服务注册与发现机制维护流量路径的逻辑关系。节点经过注册,成为服务的一个端点。调用方对服务发起请求时,被调用方是服务的逻辑地址,经过转发和地址转换,请求被路由到服务端点的实体节点。隔离服务流量的一个可选方案是破坏服务调用的通信连接,但这种方法势必会影响服务质量。在保持服务整体功能正常运行的同时,一个更优雅的方案是破坏服务与实体节点之间的映射关系。这样,在路由过程中,流量将按照预期避开特定节点,而被引导至其他节点。服务流量主要涵盖了 K8s Service 以及使用 Nacos 等注册中心发布的由 Spring Cloud、Dubbo 等微服务框架构建的服务。
事件流量指应用内部的事件驱动架构产生的流量,包括由中间件传递至应用节点的事件或消息,这类通信通常是异步的,例如来自消息队列 RocketMQ 的消息流量,来自调度框架 SchedulerX 触发调度的事件流量。中间件和应用节点之间通常遵循 client-server 通信,因此可以考虑通过破坏通信连接来隔离中间件发来的消息或事件流量。
服务流量隔离
K8s Service
对于使用 K8s Service 暴露服务的应用,Service 声明的服务与应用 Pod 之间的映射关系由 Endpoints 对象维护。Endpoints 对象的 subsets 字段表示 Serivce 的一组端点,每个端点代表一个应用 Pod 的网络地址,即一个实际提供服务的 Pod 实例。subsets 字段包含了这些端点的详细信息,如 IP 地址和端口。Endpoints 控制器通过 API Server 监听 Pod 的变更情况,并随后同步更新 Endpoints 的端点列表。因此,要隔离 K8s Service 的流量,需要破坏 Endpoints 对 Pod 的指向,将待隔离的 Pod 网络地址从 Endpoints 的端点列表中移除。同时,需要通过 Informer 机制监听 Endpoints 对象的变化,以保证 Endpoints 在后续变更或控制器 Reconcile 过程中也能维持预期状态。
Dubbo
对于使用注册中心暴露服务的应用,注册中心负责管理服务节点。只要注册关系存在且应用节点存活,注册中心会将流量调度到该应用节点。而破坏服务注册关系的操作被称为服务注销,应用节点进行服务注销之后,注册中心便不会将流量导入到注销节点,也就形成了流量隔离。
要实现 Dubbo 微服务的动态注销,首先需要从源码级别了解 Dubbo 服务注册原理。以 Dubbo 2.7.0 为例,其服务注册模块的大致结构如下:
- Dubbo 应用中存在一个 AbstractRegistryFactory 单例,负责注册中心 Registry 的容器初始化。类属性 REGISTRIES 维护了微服务列表与注册中心实例的映射关系。
- AbstractRegistry 实现了 Registry 接口,作为一个模板,实现了特定的公共方法,如服务注册(register)、服务注销(unregister)等。它还维护了已注册服务 URL 列表。
- FailbackRegistry 基于 AbstractRegistry,提供了失败重试机制。同时,它提供了注册中心的 doRegister 和 doUnregister 抽象方法。当执行 register/unregister 时,会调用 doRegister/doUnregister 方法。
- 注册中心(如 NacosRegistry、RedisRegistry)实现了具体的服务注册(doRegister)和服务注销(doUnregister)逻辑。
由源码可见,Dubbo 的服务注册模块已经内置了可动态注销/重注册服务的方法。因此, Dubbo 微服务隔离可通过主动触发其注册中心对象的服务注销方法来实现。同理,如果需要恢复服务节点,主动触发服务注册方法,更新注册中心的服务映射关系。
在确定「触发注册中心对象的服务注销方法」这一技术方向之后,需要解决如何获取对象和触发方法这两个问题。在 Java 环境中,我们很容易想到使用 Agent 技术对进程行为进行干预。然而,常规的基于字节码埋点的 Agent 无法满足随时启用的需求,因为它依赖于应用代码的具体执行路径。只有当执行路径触及埋点时,Agent 代码才会被触发,从而从上下文中获取对象并通过反射调用相关方法。然而,与注册中心相关的埋点通常设置在程序启动初期,此时会执行注册中心初始化、服务注册等操作,比较容易找到合适的埋点。在程序对外提供服务期间,程序主动发起的注册中心操作较少,因此很难找到合适的埋点来获取预期的上下文。在需要隔离应用流量时,此时动态挂入 Agent,由于执行路径中没有能获取注册中心上下文的埋点,Agent 代码将无法生效。
因此,我们需要一个能够主动获取对象并触发对象方法的即开即用的 Agent 工具。在这里,我们引入了 JVMTI 技术。JVMTI(JVM Tool Interface)是一种虚拟机提供的原生编程接口,允许开发人员创建 Agent 以探查 JVM 内部的运行状态,甚至控制 JVM 应用程序的执行。JVMTI 能够从 Java 堆中获取特定类和对象信息,然后通过反射触发方法,完美地满足了我们的需求。
由于 JVMTI 是一套 JVM 原生编程接口,需要使用 C/C++ 进行编写。编译后的产物是动态链接库(.so 或 .dll 文件)。Java 运行环境通过 JNI(Java Native Interface)与 JVMTI 进行交互。整体作为一个 Java Agent,通过 Attach API 动态地挂载到目标 JVM 中。
得益于 JVMTI Agent 的强大功能,我们能够在 Java 应用内相对简便地实施某些控制逻辑。为实现 Dubbo 服务流量隔离,首先需要获取 AbstractRegistryFactory 类的静态属性 REGISTRIES,它包含应用当前已注册服务的服务列表以及相应的注册中心 Registry 实例。对于特定的微服务,仅需调用其注册中心 Registry 的 register/unregister 方法,便可实现服务的动态摘除和恢复。这一方案直接在较高抽象层级上操作,而无需依赖具体的注册中心 Registry 实现类,使其兼容所有注册中心。
Spring CLoud
Spring Cloud 服务流量隔离方法类似于 Dubbo,在了解 Spring Cloud 服务注册原理后,获得服务注册/注销方法路径,然后通过 JVMTI 干预应用的服务注册/注销行为。
Spring Cloud 的服务注册原理较为简单。在 Spring 容器启动时,AbstractAutoServiceRegistration 监听启动事件,并调用 ServiceRegistry 的 register 方法将 Registration(服务实例数据)注册到注册中心。例如,Nacos服务注册类 NacosServiceRegistry 实现了 ServiceRegistry 接口,通过重载 register/deregister 方法完成服务在注册中心的注册和注销。
// 服务注册类 public abstract class AbstractAutoServiceRegistration<R extends Registration>...{ // 注册中心实例 private final ServiceRegistry<R> serviceRegistry; // 服务注册 protected void register() { this.serviceRegistry.register(getRegistration()); } // 服务注销 protected void deregister() { this.serviceRegistry.deregister(getRegistration()); } }
在处理 Spring Cloud 服务流量隔离时,首先获取 AbstractAutoServiceRegistration 的服务注册实例,然后调用 register/deregister 方法以在注册中心上完成服务的注销和重注册。这种方法同样不依赖于某个特定注册中心的具体实现类,兼容所有注册中心。
事件流量隔离
应用节点和中间件通常采用 client-server 模式通信,像 RocketMQ 和 SchedulerX 使用了 Netty 作为底层网络框架完成客户端和服务端通信 。在此,我们以 RocketMQ 为例,来说明如何实现类似的事件驱动中间件的流量隔离。
RocketMQ client 端的主要实现类是 NettyRemotingClient。如下图所示,NettyRemotingClient 类中的属性 channelTables 存储了用于传输数据的 Channel,而 lockChannelTables 是用于控制 channelTables 更新的锁。与此同时,有几个 invoke 方法负责处理通信过程。
通信处理流程如下图。首先,尝试从 channelTables 中获取用于通信的 Channel。如果没有可用的 Channel,则重新连接 server 端以创建 Channel。为了保证线程间同步,新 Channel 更新到 channelTables 时需要获得 lockChannelTables 锁。如果在指定时间窗口内 lockChannelTables 一直被占用,将会抛出连接异常。
根据以上的原理分析,我们可以通过占用 lockChannelTables 锁来阻止 Channel 的建立,再把现存的 Channel 关闭,则 client 端在 lockChannelTables 被释放之前都无法与 server 端建立通信连接。若要恢复流量,仅需释放 lockChannelTables 锁,client 端将自动重建 Channel 并恢复通信。由于这种管控是在网络客户端层进行的,因此它不受应用消息模型的影响,既适用于同步消息也适用于异步消息;同时也与 client 角色无关,既适用于消费者也适用于生产者。
结语
目前流量隔离工具在 EDAS-云原生工具箱 可试用体验。如果对流量隔离以及更多云原生工具感兴趣,欢迎留言或加入钉群:21958624 与我们进行沟通与交流。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
MySQL 数据传输参数设置对数据一致性的影响
作者通过全面系统的测试,揭秘 lower_case_table_names 设置对数据一致性的影响。 作者:刘安 爱可生测试团队成员,主要负责 DTLE 开源项目相关测试任务,擅长 Python 自动化测试开发。 本文来源:原创投稿 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。 背景 最近有客户询问:源端 MySQL 和目标端 MySQL 的 lower_case_table_names 的配置不一致时,DTLE 是否能正常同步数据? 本文就这个问题测试一下 lower_case_table_names 的设置对 DTLE 同步数据的影响。 为了简化场景这里只讨论 Linux 环境下 lower_case_table_names 配置为 0 或 1 的情况。 环境准备 部署 DTLE 4.23.04.2 两个 MySQL 实例,lower_case_table_names 配置不同 # lower_case_table_names=0 $ dbdeployer deploy single 5.7 --port 3306 --sandbox-directo...
- 下一篇
走向 Native 化:Spring&Dubbo AOT 技术示例与原理讲解
作者:刘军 Java 应用在云计算时代面临“冷启动”慢、内存占用高、预热时间长等问题,无法很好的适应 Serverless 等云上部署模式,GraalVM 通过静态编译、打包等技术在很大程度上解决了这些问题,同时针对 GraalVM 的一些使用限制,Spring 和 Dubbo 等主流框架也都提供了相应的 AOT 解决方案。 本文我们将详细分析 Java 应用在云时代面临的挑战,GraalVM Native Image 是如何解决这些问题,GraalVM 的基本概念与工作原理,最后我们通过一个 Spring6 + Dubbo3 的微服务应用示例演示了如何将一个普通微服务应用进行静态化打包。 本文主要分为以下四个部分展开: 首先我们会先看一下在云计算快速发展的当下,云上应用应该具备的特点,Java 应用在云上所面临的挑战有哪些。 其次,我会介绍一下 GraalVM,什么是 Native Image,如何通过 GraalVM 对 Java 应用进行静态化打出 Native Image 可执行的二进制程序。 第三部分,我们知道 GraalVM 的使用是有一定限制的,比如 Java 的反射等动...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Mario游戏-低调大师作品
- CentOS7安装Docker,走上虚拟化容器引擎之路