作者:谢文欣(风敬)
为什么要做流量隔离
源于一个 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 与咱们进行沟通与交换。