ChaosBlade 是阿里 2019 年开源的混沌工程项目,包含混沌工程实验工具 chaosblade 和混沌工程平台 chaosblade-box,旨在通过混沌工程帮助企业解决云原生过程中高可用问题。实验工具 chaosblade 支持 3 大系统平台,4 种编程语言应用,共涉及 200 多个实验场景,3000 多个实验参数,可以精细化地控制实验范围。混沌工程平台 chaosblade-box 支持实验工具托管,除已托管 chaosblade 外,还支持 Litmuschaos 实验工具。
混沌实验模型:在给出模型之前先讨论实施一次混沌实验明确的问题:
举个:一台 ip 是 10.0.0.1 机器上的应用,调用 [email protected] Dubbo 服务延迟 3s。根据上述的问题列表,先明确的是要对 Dubbo 组件混沌实验,实施实验的范围是 10.0.0.1 单机,对调用 [email protected] 服务模拟 3s 延迟。 明确以上内容,就可以精准的实施一次混沌实验,抽象出以下模型:
回到上述的,可以叙述为对 Dubbo 组件(Target)进行故障演练,演练的是 10.0.0.1 主机(Scope)的应用,调用 [email protected] (Matcher)服务延迟 3s(Action)。
Chaosblade 是内部 MonkeyKing 对外开源的项目,其建立在阿里巴巴近十年故障测试和演练实践基础上,结合了集团各业务的最佳创意和实践。ChaosBlade 不仅使用简单,而且支持丰富的实验场景,场景包括:
实际上 chaosblade 是一个聚合的父项目,只是把所有实验场景入口封装到一起实现一个命令行工具,底层又去调用了各种场景下的具体实现,它将场景按领域实现封装成一个个单独的项目,这也符合不同平台、语言存在实现差异的情况,不仅可以使领域内场景标准化实现,而且非常方便场景水平和垂直扩展,通过遵循混沌实验模型,实现 chaosblade cli 统一调用。目前包含的项目如下:
Chaosblade-exec-jvm 是通过 JavaAgent attach 方式来实现类的 transform 注入故障,底层使用了 jvm-sandbox 实现,通过插件的可拔插设计来扩展对不同 java 应用的支持。所以 chaosblade-exec-jvm 其实只是一个 java agent 模块,不是一个可执行的工程,必须依赖 jvm-sandbox。
JVM 沙箱容器,一种 JVM 的非侵入式运行期 AOP 解决方案,提供:
JVM-SANDBOX(沙箱)实现了一种在不重启、不侵入目标 JVM 应用的 AOP 解决方案。
沙箱的特性
沙箱常见应用场景
实时无侵入 AOP 框架
在常见的 AOP 框架实现方案中,有静态编织和动态编织两种。
要解决无侵入的特性需要 AOP 框架具备在运行时完成目标方法的增强和替换。在 JDK 的规范中运行期重定义一个类必须准循以下原则
JVM-SANDBOX 属于基于 Instrumentation 的动态编织类的 AOP 框架,通过精心构造了字节码增强逻辑,使得沙箱的模块能在不违反 JDK 约束情况下实现对目标应用方法的无侵入运行时 AOP 拦截。
沙箱启动方式
沙箱有两种启动方式:ATTACH 和 AGENT
ATTACH 方式启动
即插即用的启动模式,可以在不重启目标 JVM 的情况下完成沙箱的植入,JVM 的 Attach 机制实现。
# 假设目标 JVM 进程号为 '2343'
./sandbox.sh -p 2343
如果输出
NAMESPACE : default
VERSION : 1.2.0
MODE : ATTACH
SERVER_ADDR : 0.0.0.0
SERVER_PORT : 55756
UNSAFE_SUPPORT : ENABLE
SANDBOX_HOME : /Users/luanjia/.opt/sandbox
SYSTEM_MODULE_LIB : /Users/luanjia/.opt/sandbox/module
USER_MODULE_LIB : ~/.sandbox-module;
SYSTEM_PROVIDER_LIB : /Users/luanjia/.opt/sandbox/provider
EVENT_POOL_SUPPORT : DISABLE
则说明启动成功,沙箱已经顺利植入了目标 JVM 中,并完打开了 HTTP 端口 55756,完成模块的加载。
AGENT 方式启动
有些时候我们需要沙箱工作在应用代码加载之前,或者一次性渲染大量的类、加载大量的模块,此时如果用ATTACH 方式加载,可能会引起目标 JVM 的卡顿或停顿(GC),这就需要启用到 AGENT 的启动方式。
假设 SANDBOX 被安装在了/Users/luanjia/.opt/sandbox,需要在 JVM 启动参数中增加上
-javaagent:/Users/luanjia/.opt/sandbox/lib/sandbox-agent.jar
这样沙箱将会伴随着 JVM 启动而主动启动并加载对应的沙箱模块。
沙箱启动分析
实现原理解析
JVM Sandbox 主要包含 SandBox Core、Jetty Server 和自定义处理模块三部分:
客户端通过 Attach API 将沙箱挂载到目标 JVM 进程上,启动之后沙箱会一直维护着 Instrumentation 对象引用,通过 Instrumentation 来修改字节码和重定义类。另外,SandBox 启动之后同时会启动一个内部的 Jetty 服务器,这个服务器用于外部和 SandBox 进行通信,对模块的加载、卸载、激活、冻结等命令等命令操作都会通过 Http 请求的方式进行,并提供了一个 sandbox.sh 程序脚本以命令行方式执行挂载、卸载和和所有操作请求。
JVM SandBox 包括如下模块:
JVM SandBox 模块的生命周期:
只有当模块处于激活状态,才会真正调用用户的 AOP 增强逻辑。
类隔离机制
BootstrapClassLoader 加载真正织入代码的 Spy 类,而 JVM Sandbox 中有两个自定义的 ClassLoader:
通过重写 java.lang.ClassLoader 的 loadClass(String name, boolean resolve) 方法,打破了双亲委派约定,达到与目标类隔离的目的,不会引起应用的类污染、冲突。
ModuleClassLoader 类加载流程如下:
SandBox 初始化流程
类增强策略
SandBox 通过在 BootstrapClassLoader 中埋藏的 Spy 类完成目标类和沙箱内核的通讯,最终执行到用户模块的 AOP 方法。
字节码增强和撤销流程
字节码增强时,通过 Instrumentation 的 addTransformer(ClassFileTransformer transformer) 方法注册一个ClassFileTransformer,从此之后的类加载都会被 ClassFileTransformer 拦截,然后调用 Instrumentation 的retransformClasses(Class... classes) 对 JVM 已经加载的类重新触发类加载,类加载时会被ClassFileTransformer 拦截。
字节码增强撤销时,通过 Instrumentation 的 removeTransformer(ClassFileTransformer transformer) 方法移除相应的 ClassFileTransformer,然后调用 Instrumentation的retransformClasses(Class... classes) 重新触发类加载。
工程架构
chaosblade-exec-jvm 工程分 4 个子模块:
工程编译
chaosblade-exec-jvm 的工具编译与其它几个场景的执行器有所不同:
模块管理
SandboxModule
作为 Sandbox(chaosblade)的模块、所有的 Sandbox 事件,如 Agent 挂载(模块加载)、Agent 卸载(模块卸载)、模块激活、模块冻结等都会在此触发,Sandbox 内置 jetty 容器,访问 api 回调到注解为 @Http("/xx") 的方法,来实现故障能力。
StatusManager
blade create 命令在 StatusManager 注册状态、并管理整个实验的状态,包含攻击次数、攻击的百分比、命令参数、攻击方式(Action)等。
ModelSpecManager
管理插件的 ModelSpec,ModelSpec 的注册、卸载。
ListenerManager
管理插件的生命周期,插件的加载、卸载。
RequestHandler
Sandbox 内置 jetty 容器,访问 api 回调到注解为 @Http("/xx") 的方法,由事件分发器(DispatchService)将事件分到 RequestHandler 处理,RequestHandler 分为如下表格(表格中的【一定条件】可以参考下面的 plugin 加载方式):
命令 |
RequestHandler |
blade create |
CreateHandler 创建一个实验,StatusManager 注册状态,满足一定条件的插件加载。 |
blade status |
StatusHandler 去 StatusManager查询实验状态。 |
blade destroy |
DestroyHandlerr 销毁实验,满足一定条件的插件卸载。 |
插件模型
Enhancer
jvm-sandbox 提供了 EventListener 接口给自定义 module 去实现事件的自定义动作(这里是故障注入),通过 onEvent 方法触发事件,chaosblade-exec-jvm 定义了 BeforeEnhancer 和 AfterEnhancer 两种类,分别对应 jvm-sandbox 的 before 和 return 事件来完成方法执行前注入故障和方法返回后注入故障。
由于不同的故障注入操作对应不同的类、方法、参数、动作等,chaosblade-exec-jvm 将这些信息抽象成了一个 EnhancerModel 作为操作媒介,并将故障注入操作实现为一个静态方法 Injector.inject(enhancerModel),不同的故障注入类型自定义 enhancer 去继承 BeforeEnhancer、AfterEnhancer 并返回 EnhancerModel,再去执行 inject。例如 ServletEnhancer,获取 ContextPath、RequestURI、Method 等,将获取到的参数放到 MatcherModel,返回 EnhancerModel,Inject 阶段会与输入的参数做比对。
故障注入操作基本都是响应 before 事件,所以基本都是继承 BeforeEnhancer 去实现方法调用前的 AOP 增强操作,有一个特殊的是 jvm 故障注入类型如 delay、return 等操作是可以定义方法执行完成返回前注入故障的,所以特殊地存在一个 MethodEnhancer 同时实现了 beforeAdvice 和 afterAdvice 支持部分 jvm 场景的故障注入操作。
PointCut
定义故障注入操作执行的AOP 切点,例如 ServletPointCut 拦截类:spring 的 FrameworkServlet、webx 的 WebxFrameworkFilter、及父类为 HttpServletBean 或 HttpServlet 的子类,拦截方法:doGet、doPost、doDelete、doPut、doFilter。
Spec
一个故障注入操作的完整命令实体模型。
插件需要实现的部分是 ModelSpec、ActionSpec 和 FlagSpec,每个插件需要一个 ModelSpec,定义了一个混沌实验模型需要的全部信息,如 Action 则用 ActionSpec 来表示,ActionSpec 中包含了具体的 FlagSpec 和 ActionExcutor 执行器。
插件加载
加载方式 |
加载条件 |
SandboxModule onActive() 事件 |
Pointcut、ClassMatcher、MethodMatcher 都不为空 |
CreateHandler |
ModelSpect 为 PreCreateInjectionModelHandler 类型,且ActionFlag 不为 DirectlyInjectionAction 类型 |
chaosblade 在 prepare jvm 时(新版本可以省略 prepare 操作直接 create)先通过 jvm-sandbox 将 chaosblade-exec-jvm 的 module 加载到目标 jvm 进程后,触发 SandboxModule onLoad() 事件,初始化 PluginLifecycleListener、DispatchService 等,然后 chaosblade 去请求 jvm-sandbox 的 active 接口使模块激活,会触发 SandboxModule onActive() 事件。
onActive() 事件加载 plugin 是有条件的,所以这里特殊的是对于 jvm plugin 是没有切点 PointCut 的,所以在 onActive() 阶段不会去加载该插件的,也就必须要等到 CreateHandler 中才去加载。
onActive() 事件下,会先注册 ModelSpec,然后在 Plugin 加载时,创建事件监听器SandboxEnhancerFactory.createAfterEventListener(plugin),监听器会监听感兴趣的事件,如 BeforeAdvice、AfterAdvice 等。
PointCut 匹配
SandboxModule onActive() 事件触发 Plugin 加载后,SandboxEnhancerFactory 创建 filter,filter 内部通过PointCut 的 ClassMatcher 和 MethodMatcher 过滤。
Enhancer
如果已经加载插件,此时目标应用匹配能匹配到 filter 后,EventListener 已经可以被触发,但是 chaosblade-exec-jvm 内部通过 StatusManager 管理状态,所以故障能力不会被触发。
执行实验
create 命令下发后,触发 SandboxModule @Http("/create") 注解标记的方法,将事件分发给com.alibaba.chaosblade.exec.service.handler.CreateHandler 处理,在判断必要的uid、target、action、model 参数后调用 handleInjection,handleInjection 通过状态管理器注册本次实验,如果插件类型是PreCreateInjectionModelHandler 的类型,将预处理一些东西。同是如果 Action 类型是DirectlyInjectionAction,那么将直接进行故障能力注入,如 jvm oom 等,如果不是那么将加载插件。
DirectlyInjectionAction
如果 ModelSpec 是 PreCreateInjectionModelHandler 类型,且 ActionSpec 的类型是 DirectlyInjectionAction类型,将直接进行故障能力注入,比如 JvmOom 故障能力,ActionSpec 的类型不是 DirectlyInjectionAction 类型,将加载插件。
注入成功后返回 uid,如果本阶段直接进行故障能力注入了,或者自定义 Enhancer advice 返回null,那么后不通过 Inject 类触发故障。
故障能力注入
故障能力注入的方式,最终都是调用 ActionExecutor 执行故障能力。
Inject 注入时因为 StatusManager 已经注册了实验,当事件再次出发后ManagerFactory.getStatusManager().expExists(targetName) 的判断不会被中断,继续往下走,到了自定义的Enhancer。在自定义的 Enhancer 里面可以拿到原方法的参数、类型等,甚至可以反射调原类型的其他方法,这样做风险较大,一般在这里往往是取一些成员变量或者 get 方法等,用户后续参数匹配。如 ServletEnhancer,把一些需要与命令行匹配的参数包装在 MatcherModel 里面,然后包装 EnhancerModel 返回,比如 --requestpath = /index,那么 requestpath 等于 requestURI 去除contextPath。
到了 Injector.inject(model) 阶段进行参数匹配判断,获取 StatusManager 注册的实验,compare(model, enhancerModel) 进行参数比对,失败后return,limitAndIncrease(statusMetric) 判断 --effect-count --effect-percent 来控制影响的次数和百分比。
不论是 Inject 注入或是 DirectlyInjectionAction 直接注入,最后都是调用自定义的 ActionExecutor 生成故障,如 DefaultDelayExecutor,此时故障能力已经生效。
销毁实验
destroy 命令下发后,触发 SandboxModule @Http("/destory") 注解标记的方法,将事件分发给com.alibaba.chaosblade.exec.service.handler.DestroyHandler 处理。注销本次故障的状态。
如果插件的 ModelSpec 是PreDestroyInjectionModelHandler类型,且 ActionSpec 的类型是DirectlyInjectionAction 类型,停止故障能力注入,ActionSpec 的类型不是 DirectlyInjectionAction类型,将卸载插件。
revoke 命令下发后,触发 SandboxModule unload() 事件,同是插件卸载。
chaosblade-operator 项目是针对 Kubernetes 平台所实现的混沌实验注入工具,遵循上述混沌实验模型规范化实验场景,把实验定义为 Kubernetes CRD 资源,将实验模型中的四部分映射为 Kubernetes 资源属性,很友好的将混沌实验模型与 Kubernetes 声明式设计结合在一起,依靠混沌实验模型便捷开发场景的同时,又可以很好的结合 Kubernetes 设计理念,通过 kubectl 或者编写代码直接调用 Kubernetes API 来创建、更新、删除混沌实验,而且资源状态可以非常清晰的表示实验的执行状态,标准化实现 Kubernetes 故障注入。除了使用上述方式执行实验外,还可以使用 chaosblade cli 方式非常方便的执行 kubernetes 实验场景,查询实验状态等。 遵循混沌实验模型实现的 chaosblade operator 除上述优势之外,还可以实现基础资源、应用服务、Docker 容器等场景复用,大大方便了 Kubernetes 场景的扩展,所以在符合 Kubernetes 标准化实现场景方式之上,结合混沌实验模型可以更有效、更清晰、更方便的实现、使用混沌实验场景。
架构说明
主要使用 helm charts 的部署方式,对目标 k8s 集群安装 chaosblade-tool 的 daemonset 和一个 chaosblade-operator:
实验执行
通过 chaosblade-operator 对 k8s 的一个 node:cn-hangzhou.10.101.112.164 22 端口制造 60% 的网络丢包。 用 yaml + kubectl 创建实验,观察实验过程自动开始运行:
loss-node-network-by-names-sofaprehzfin.yaml
测试查看目标节点 cn-hangzhou.10.101.112.164 22 端口访问效果:
通过以上内容可以很清晰的看出混沌实验的运行状态和效果,执行以下命令停止实验:
kubectl delete -f loss-node-network-by-names-sofaprehzfin.yaml
还可以编辑 yaml 文件,更新实验内容执行,chaosblade operator 会完成实验的更新操作,或者直接删除此 blade 资源:
kubectl delete blade loss-node-network-by-names
也可以使用 chaosblade cli 的 blade 命令执行:
blade create k8s node-network loss --percent 60 --interface eth0 --local-port 22 --kubeconfig config --names cn-hangzhou.10.101.112.164
如果执行失败,会返回详细的错误信息;如果执行成功,会返回实验的 UID:
{"code":200,"success":true,"result":"xxxxxxxxxxxxxxxxx"}
可通过以下命令查询实验状态:
blade query k8s create xxxxxxxxxxxxxxxxx --kubeconfig config
blade 命令销毁实验:
blade destroy xxxxxxxxxxxxxxxxx
除了上述两种方式调用外,还可以使用 kubernetes client-go 方式执行,具体可参考:executor.go。
可以看出 ChaosBlade 在设计上考虑了云原生实验场景,将混沌实验模型与 Kubernetes 设计理念友好的结合在一起,不仅可以遵循 Kubernetes 标准化实现,还可以复用其他领域场景和 chaosblade cli 调用方式。
声明式 API 对象
编程范式
像上面 deployment yaml 文件那样,具备以下几个特点的资源对象,就是声明式 API 对象:
声明式 API 对象定义出期望的资源状态,只需要提交给 k8s 而不关心过程,k8s 会负责完成所期望的效果。
组织方式
API 对象在 etcd 里的完整资源路径是由 Group(API 组)、Version(API 版本)和 Resource(API 资源类型)三部分组成。Kubernetes 创建资源对象的流程:
GVKs & GVRs
GVK = GroupVersionKind,GVR = GroupVersionResource。
API Group & Versions(GV)
API Group 是相关 API 功能的集合,每个 Group 拥有一或多个 Versions,用于接口的演进。
Kinds & Resources
每个 GV 都包含多个 API 类型,称为 Kinds,在不同的 Versions 之间同一个 Kind 定义可能不同, Resource 是 Kind 的对象标识(resource type),一般来说 Kinds 和 Resources 是 1:1 的,比如 pods Resource 对应 Pod Kind,但是有时候相同的 Kind 可能对应多个 Resources,比如 Scale Kind 可能对应很多 Resources:deployments/scale,replicasets/scale,对于 CRD 来说,只会是 1:1 的关系。
每一个 GVK 都关联着一个 package 中给定的 root Go type,比如 apps/v1/Deployment 就关联着 K8s 源码里面 k8s.io/api/apps/v1 package 中的 Deployment struct,我们提交的各类资源定义 YAML 文件都需要写:
根据 GVK K8s 就能找到你到底要创建什么类型的资源,根据你定义的 Spec 创建好资源之后就成为了 Resource,也就是 GVR。GVK/GVR 就是 K8s 资源的坐标,是我们创建/删除/修改/读取资源的基础。
Scheme
每一组 Controllers 都需要一个 Scheme,提供了 Kinds 与对应 Go types 的映射,也就是说给定 Go type 就知道他的 GVK,给定 GVK 就知道他的 Go type,比如说我们给定一个 Scheme: "chaosblade.io/api/v1alpha1".ChaosBlade{} 这个 Go type 映射到 chaosblade.io/v1alpha1 的 ChaosBlade GVK,那么从 Api Server 获取到下面的 JSON:
{ "apiVersion": "chaosblade.io/v1alpha1", "kind": "ChaosBlade", ... }
就能构造出对应的 Go type,通过这个 Go type 也能正确地获取 GVR 的一些信息,控制器可以通过该 Go type 获取到期望状态以及其他辅助信息进行调谐逻辑。
Chaosblade CRD
Kubernetes 里资源类型有如下所示:
上述资源类型可以满足大多数分布式系统部署的需求,但是在不同应用业务环境下,对于平台可能有一些特殊的需求,这些需求可以抽象为 Kubernetes 的扩展资源,而 Kubernetes 的 CRD (CustomResourceDefinition) 为这样的需求提供了轻量级的机制,保证新的资源的快速注册和使用。
Kuberentes 里并没有 Chaosblade 这个资源可以使用,所以直接执行 kubectl create -f loss-node-network-by-names-sofaprehzfin.yaml 的时候,会出错。但是 kubernetes 提供的 CRD 机制可以让我们轻松的把自定义功能添加到 kubernetes 里。
我们需要根据需求,先进行自定义资源(CRD - Custom Resource Definition),它将包括API对象组、版本号、资源类型,还包括资源的校验:
将此 crd.yaml 通过 kubectl apply 提交到 k8s 后就可以创建 Chaosblade 类型的资源了,但实际上如果只定义该资源对象,它只会被 API Server 简单地计入到 etcd 中,除了能创建出该类型的资源实例外,不会产生任何其他效果,也不能执行故障实验。
Controller
容器的本质是进程,因为容器中 PID=1 的进程是容器自身,其他进程都是这个 PID=1 进程的子进程。Pod 只是一个逻辑概念,Kubernetes 真正要处理的还是宿主机中构成容器的 Cgroups 和 Namespace,因此也可认为 Pod 扮演了基础设施中"虚拟机"的角色,容器是运行在 Pod 中的"应用程序"。Kubernetes提供了一种实现 Pod 自动伸缩、滚动升级、回滚的机制叫控制器。Kubernetes 提供了很多控制器,如:
尽管以上每一个控制器负责不同资源的编排工作,但是它们都遵循最基本的控制循环(control loop)原理,举个,看一个 deployment 的 yaml 文件:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.13.12 ports: - containerPort: 80
简单的说,上述 deployment 的作用是为了确保携带 app:nginx 标签的 Pod 数量永远等于 spec.replicas指定的数量 3。可以把控制器控制循环的实现原理归纳如下:
如何来完成这个复杂的操作,则是由 Controller,也就是控制器来实现的。Controller 其实是 Kubernetes 提供的一种可插拔式的方法来扩展或者控制声明式的 Kubernetes 资源。它是 Kubernetes 的大脑,负责大部分资源的控制操作。以 Deployment 为例,它就是通过 kube-controller-manager 来部署的。声明一个 Deployment 有 replicas、有 2 个 Pod,kube-controller-manager 在观察 etcd 时接收到了该请求之后,就会去创建两个对应的 Pod 的副本,并且它会去实时地观察着这些 Pod 的状态,如果这些 Pod 发生变化了、回滚了、失败了、重启了等等,它都会去做一些对应的操作。所以 Controller 才是控制整个 Kubernetes 资源最终表现出来的状态的大脑。
如上图所示,左侧是一个 Informer,它的机制就是通过去 watch kube-apiserver,而 kube-apiserver 会去监督所有 etcd 中资源的创建、更新与删除。Informer 主要有两个方法:一个是 ListFunc;一个是 WatchFunc。
Informer 接收到了对象的需求之后,就会调用对应的函数(图中的三个函数 AddFunc、UpdateFunc、DeleteFunc),并将其按照 key 值的格式放到一个队列中去,key 值的命名规则就是 "namespace/name",name 就是对应的资源的名字。比如我们刚才所说的在 default 的 namespace 中创建一个 blade 类型的资源,那么它的 key 值就是 "default/loss-node-network-by-names"。Controller 从队列中拿到一个对象之后,就会去做相应的操作。
Client-Go
Kubernetes 官方将 Kubernetes 资源操作相关的核心源码抽取出来,独立出来一个项目 client-go,作为官方提供的 Go client,是一个调用 kubernetes 集群资源对象 API 的客户端,即通过 client-go 实现对 kubernetes 集群中资源对象(包括 deployment、service、ingress、replicaSet、pod、namespace、node 等)的增删改查等操作,Kubernetes 的部分代码也基于这个 client 实现。
下面是 client-go 的设计思想和工作原理,也是更为详细的控制器工作流程:
Informer 是 APIServer 与 Kubernetes 互相通信的桥梁,它通过 Reflector 实现 ListAndWatch 方法来“获取”和“监听”对象实例的变化。每当 APIServer 接收到创建、更新和删除实例的请求,Refector 都会收到“事件通知”,然后将变更的事件推送到先进先出的队列中。Informer 会不断从上一队列中读取增量,然后根据增量事件的类型创建或者更新本地对象的缓存。Informer 会根据事件类型触发事先定义好的 ResourceEventHandler(具体为 AddFunc、UpdatedFunc、DeleteFunc,分别对应 API 对象的“添加”、“更新”和“删除”事件),同时每隔一定的时间,Informer 也会对本地的缓存进行一次强制更新。
另一张流程图:
在这个流程里,大部分是 client-go 为用户提供的框架和逻辑,可以直接使用,灰色的 AddFunc 等是用户需要实现的关于该扩展资源的业务逻辑。informer 会借助 APIServer 跟踪该扩展资源定义的变化,一旦被触发就会调用回调函数,并把变更的具体内容放到 Workqueue 中,自定义 controller 里面的 worker 会获取 Workqueue 里面内容,并进行相应的业务处理。
Admission Webhooks
什么是 Admission
Admission 也是 k8s 的一种 Controller,即准入控制,是 Kubernetes API Server 用于拦截请求的一种手段。Admission 可以做到对请求的资源对象进行校验,修改。service mesh 的 Istio 天生支持 Kubernetes,利用的就是 admission 对服务实例自动注入 sidecar。
实际上在 Kubernetes 中还有 authn/authz,为什么还会引入 admission 这种机制?
目前 Kubernetes 中已经有非常多内置的 Admission 插件,但是 Admission 也提供了一种 webhook 的扩展机制,即 Admission Webhook 机制,以运行时所配置的 Webhook 的形式运行,是一种用于接收准入请求并对其进行处理的 HTTP 回调机制。可以定义两种类型的准入 webhook,验证性质的准入 Webhook 和 修改性质的准入 Webhook:
k8s api 请求的生命周期和 admission webhooks 的环节如图所示:
Admission Webhooks 的优势:
用户声明完成 CRD 之后,也需要创建一个控制器来完成对应的目标。比如上述的 Chaosblade 需要我们创建一个控制器用于创建对应的 Chaosblade 才能真正实现 CRD 的功能:
Kubebuilder & Operator-sdk
Kubebuilder 和 Operator-sdk 分别都是一个使用 CRDs 构建 K8s API 的 SDK,主要是:
方便用户从零开始开发 CRDs,Controllers 和 Admission Webhooks 来扩展 K8s,两种 sdk 在使用上和生成的 operator 框架代码基本类似,也都是基于 k8s 的 client-go 和 controller-runtime。
Manager
核心组件,具有 3 个职责:
Cache
Kubebuilder 的核心组件,负责在 Controller 进程里面根据 Scheme 同步 Api Server 中所有该 Controller 关心 GVKs 的 GVRs,其核心是 GVK -> Informer 的映射,Informer 会负责监听对应 GVK 的 GVRs 的创建/删除/更新操作,以触发 Controller 的 Reconcile 逻辑。
Controller
Kubebuidler 为我们生成的脚手架文件,我们只需要实现 Reconcile 方法即可。
Clients
在实现 Controller 的时候不可避免地需要对某些资源类型进行创建/删除/更新,就是通过该 Clients 实现的,其中查询功能实际查询是本地的 Cache,写操作直接访问 Api Server。
Finalizer
在一般情况下,如果资源被删除之后,我们虽然能够被触发删除事件,但是这个时候从 Cache 里面无法读取任何被删除对象的信息,这样一来,导致很多垃圾清理工作因为信息不足无法进行,K8s 的 Finalizer 字段用于处理这种情况。在 K8s 中,只要对象 ObjectMeta 里面的 Finalizers 不为空,对该对象的 delete 操作就会转变为 update 操作,具体说就是 update deletionTimestamp 字段,其意义就是告诉 K8s 的 GC“在deletionTimestamp 这个时刻之后,只要 Finalizers 为空,就立马删除掉该对象”。
所以一般的使用姿势就是在创建对象时把 Finalizers 设置好(任意 string),然后处理 DeletionTimestamp 不为空的 update 操作(实际是 delete),根据 Finalizers 的值执行完所有的 pre-delete hook(此时可以在 Cache 里面读取到被删除对象的任何信息)之后将 Finalizers 置为空即可。
OwnerReference
K8s GC 在删除一个对象时,任何 ownerReference 是该对象的对象都会被清除,与此同时,Kubebuidler 支持所有对象的变更都会触发 Owner 对象 controller 的 Reconcile 方法。
Manager-Controller 流程
当前的版本 1.3.0 是基于 operator-sdk 0.19.0 版本来开发的,在脚手架工程的基础上做了调整并扩充了部署文件和 chaosblade-tool daemonset 的代码等。
程序入口
创建了一个 manager,在 addComponents 中添加 scheme、controller 和 webhook 后,启动该 manager。
Manager 初始化
在 manager.New 方法中初始化代码如下:
可以看到主要是创建 Cache 与 Clients。
创建 Cache
Cache 初始化代码如下:
可以看到 Cache 主要就是创建了 InformersMap,Scheme 里面的每个 GVK 都创建了对应的 Informer,通过 informersByGVK 这个 map 做 GVK 到 Informer 的映射,每个 Informer 会根据 ListWatch 函数对对应的 GVK 进行 List 和 Watch。
创建 Clients
读操作使用上面创建的 Cache,写操作使用 K8s go-client 直连。
Controller 初始化
Controller 的启动:
传入 Chaosblade Reconciler 新建一个 controller:
Watch 方法
该方法对本 Controller 负责的 CRD Chaosblade 进行了 watch,Watch 中我们关心两个逻辑:
脚手架为我们注册的 Handler 就是将发生变更的对象的 NamespacedName 入队列,如果在 Reconcile 逻辑中需要判断创建/更新/删除,需要有自己的判断逻辑。
Handler 实际注册到 Informer 上面将整个逻辑串起来,通过 Cache 创建了所有 Scheme 里面 GVKs 的 Informers,然后对应 GVK 的 Controller 注册了 Watch Handler 到对应的 Informer,对应的 GVK 里面的资源有变更都会触发 Handler,将变更事件写到 Controller 的事件队列中,之后触发自定义 Controller 的 Reconcile 方法。
Manager 启动
主要就是启动 Cache,Controller,将整个事件流运转起来并保持。
Cache 启动
Cache 的初始化核心是初始化所有的 Informer,Informer 的初始化核心是创建了 reflector 和内部 controller,reflector 负责监听 Api Server 上指定的 GVK,将变更写入 delta 队列中,可以理解为变更事件的生产者,内部 controller 是变更事件的消费者,他会负责更新本地 indexer,以及计算出 CUD 事件推给我们之前注册的 Watch Handler。
Controller 启动
Controller 的初始化是启动 goroutine 不断地查询队列,如果有变更消息则触发到自定义的 Reconcile 逻辑。
整体逻辑
用一张图来描述上述的 manager-controller 流程:
脚手架工具已经为做了上述的很多工作,到最后自定义的 controller 只需要实现 Reconcile 方法即可。
Chaosblade 业务代码
Reconcile
主要是根据 Chaosblade 资源的状态做调谐的具体逻辑,这里面就涉及到具体的实验创建、查询、销毁。
创建 Chaosblade 调谐器时注册了一个分发调度器,因为存在 node、pod、container 的不同环境范围下的执行器实现有所不同,需要调度到不同的 executor 去执行具体实验。
实验模型与执行
同 jvm 场景的实现一样,operator 根据 chaosblade 的模型实现了具体的 Model、Action、Flag 的 Spec 和转换逻辑,事件触发 Reconcile 逻辑后将 request 解码出 chaosblade 的资源定义,将其转换成标准的 ExperimenSpec 模型。
以创建针对容器的实验为例,Reconciler 根据具体实验范围 container 和资源属性获取到目标容器,再根据模型转换得到的 Executor 去执行该实验。
实际上对于 k8s 场景下的实验,目标 pod、container 甚至 node(会特殊一些)都需要通过 k8s api 去访问。chaosblade-operator 抽象了执行器去向 k8s 发送请求来执行具体的实验命令。
mutating-pods admisson webhook
此 webhook 的目的是对集群上创建 pod 的请求进行变异,即修改 pod,对于存在 chaosblade-inject 相关标签的 pod 注入一个 sidecar。
注入 sidecar 的流程
IO 故障原理
此 sidecar 是运行一个单独的可执行程序 chaos-fuse,通过 hookfs 挂载 mountpoint 和注册了 ChaosbladeHook,此时此挂载点已经注入了 fs hook,在此挂载点上的一些 read、write 等操作会调用到 ChaosbladeHook 的 PreXxx 方法,然后调用到 doInjectFault 方法执行 IO 故障操作。
doInjectFault 方法中需要先 load cache,获取到对应请求中的 method flag 注入 IO 的操作名,如果无此缓存则无后续的故障动作。
chaos-fuse sidecar 启动了一个 http server 注册 InjectHandler 和 RecoverHandler 分别执行 IO 动作的注入和恢复,在这两个 Handler 中是讲要执行的 IO inject message 分别保存到 cache 中和从 cache 中删除,从而在上述的 doInjectFault 操作中能 load 到对应的 method 和 message 继续往下执行 IO 故障。
PodIO 的 Executor 中则获取了一个上述 sidecar server 的 client,向 /inject 和 /recover 路径发送 http 请求完成实验创建和销毁。
webhook 部署
daemonset 上部署的 webhook-server,因此开放一个 webhook 的 service,转发到 443 端口进行监听(因为 admission webhook 的默认端口是 443),通过 secret.yaml 为 operator 配置了一个 MutatingWebhookConfiguration 且 clientConfig 映射到 webhook server service。
chaosblade-tool daemonset
chaosblad-tool 的镜像就是一个包含 chaosblade 完整程序包的 alpine 镜像,以 daemonset 的类型部署到 k8s,之所以能够通过其执行 node 范围的实验和注入 sidecar,是因为该 daemonset 采用了特殊属性配置:
chaosblade-cli 是该项目提供的完整的混沌工程实验命令行工具,提供完整工具部署包,各种场景下的实验操作都最终依赖该命令行的调用,也可以基于此工具启动一个 chaosblade-server 提供接口让客户端发起 http 请求操作实验。
cli 中不包含任何具体实验场景的实现,而是实现了 chaosblade 所定义的实验模型,根据 scope、model、action、flag 去解析具体的实验内容,执行具体实验时是将命令转发给子场景执行器。该工具完整编译包是将所有场景执行器编译的执行文件打包,相当于调用所有子场景实验执行器的客户端集合,对于 os、jvm 等场景是直接命令行本地调用,docker、containerd 是 cri api 的调用,k8s 是 client-go 的调用。
同时 cli 是一个配置文件解析式的命令行工具,依赖包中 yaml 下的各场景实现其的 yaml 文件,各 yaml 文件是通过每个子工程的 spec.go 编译生成的,blade 命令所能执行的实验指令和支持的参数再基于 yaml 定义转换,所有的模型定义通过子场景实现器编译生成 spec.yaml 文件后即是独立的,通过 yaml 的角色完成了模型的中转且可以单独编辑配置,可以做到不修改代码而控制支持的场景。
chaosblade-cli 的逻辑实现如下:
Cobra 是 go 的一个开源工具库,提供简单的接口来创建强大现代的 CLI 接口,类似于 git 或者 go 工具。同时,它也是一个应用,用来生成个人应用框架,组织系统命令、子命令以及相关参数。chaosblade 正是基于此框架实现了所有实验命令的解析,同时该工具还提供一个 http server 的调用方式,执行 blade server start 后启动一个 http 服务注册了一个 chaosblade 接口,将命令转成 url 格式拼接该路径进行请求,如:curl "http:/xxxx:9526/chaosblade?cmd=create%20cpu%20fullload"。
Chaosblade 混沌实验模型
JVM SandBox实现原理详解
JVM源码分析之javaagent原理完全解读
JVM SandBox之调用方式(命令行和http)
JVM SandBox的技术原理与应用分析
JVM Sandbox源码分析–启动简析
JVM Sandbox源码分析–启动时加载模块
JVM SandBox简要介绍
chaosblade-exec-jvm/design.md at master · chaosblade-io/chaosblade-exec-jvm · GitHub
chaosblade-exec-jvm/plugin.md at master · chaosblade-io/chaosblade-exec-jvm · GitHub
https://github.com/chaosblade-io/chaosblade/blob/master/CLOUDNATIVE.md
Kubernetes CRD - 知乎
Kubernetes中CRD的介绍和使用 - 云计算 - 亿速云
熟悉又陌生的 k8s 字段:SecurityContext - InfoQ 写作平台
从零开始入门 K8s | Kubernetes API 编程范式 - 阿里巴巴云原生 - 博客园
kubernetes-dev-docs/client-go at master · opsnull/kubernetes-dev-docs
深入解析 Kubebuilder:让编写 CRD 变得更简单-阿里云开发者社区
client-go 源码学习总结 - 知乎
Operator SDK
使用operator-sdk在Kubernetes中编写自定义控制器CRD_yjk13703623757的博客-CSDN博客
Kubernetes控制器基本原理
Kubernetes 控制器的工作原理解读 – 云原生实验室 - Kubernetes|Docker|Istio|Envoy|Hugo|Golang|云原生
Kubernetes 准入控制器详解!_K8sMeetup 社区的博客-CSDN博客
Kubernetes 准入控制 Admission Controller 介绍_weixin_34014277的博客-CSDN博客
k8s 准入控制器【1】-介绍_地下库-CSDN博客_k8s 准入控制器
kubernetes hostNetwork: true 网络_kozazyh的专栏-CSDN博客_hostnetwork