我们在上一篇文章《 》中讲到,混沌工程是通过在系统环境中主动注入故障,分析故障对系统产生的影响,提前发现系统存在的问题,并针对性进行加固防范,将隐患消灭在初始阶段。因此,对于混沌工程来说,故障注入是第一步,也是混沌工程技术中的核心部分。本文将结合G行混沌工程实践经验,从故障场景分类、故障注入实现、实践效果总结三方面为大家逐步揭开故障注入的神秘面纱。
针对故障注入,如果您存在以下疑问,皆可在本文中找到答案。
问题1:目前支持的场景都有哪些?
问题2:一个服务,如何成为一个被故障注入对象?
问题3:这些故障注入能力是如何实现的呢?
问题4:故障注入能够实现对业务系统源代码故障注入且无侵入吗?比如Java语言的系统,如果可以,是如何做到的呢?
问题5:中间件类型的系统可以做故障注入吗?这部分是怎么实现的呢?
问题6:除了后端系统,前端可以被纳入故障注入的范畴么?
故障注入场景分类
目前的混沌工程场景按照架构层级分类分为以下场景:
系统资源类:系统资源类故障是混沌工程故障注入技术中最基础、最常用的故障场景,该故障通常会涉及到CPU、内存、磁盘、网络、文件、文件系统等方面,其中磁盘故障主要包括磁盘负载、磁盘I/O hang,磁盘读写故障、磁盘读写慢等;网络故障主要包括丢包、乱序、延迟、网络损坏、网络占用等,该类故障在生产环境中出现概率高,因此使用率也较高。另外对于主机与容器资源的故障也划分在这个类别,主机机器的重启、断电、时钟偏移等,容器相关的container pause、移除,pod删除、pod失败等场景。
数据库、中间件类故障:数据库的稳定性对于银行业务安全运行至关重要,同时中间件比如缓存、消息队列、网关等,在银行业务中的应用也越来越广泛和深入。对于数据库、中间件,基础的故障能力例如:慢SQL、行锁、表锁、Redis大key、热key、消息积压、消息丢失等。进一步,数据库、中间件本身的高可用机制在各种故障下是否生效切实决定其稳定性,比如:磁盘I/O hang时,数据库能否完成主备切换;内存打满时,缓存平台能否正常处理;消息队列主进程挂掉时,消息的消费是否还能如常进行等。另外,极端故障场景下,数据库、中间件本身的自愈能力如何;是否会对业务造成很大的影响,这类场景都是使用该类组件时需要关注的。
应用级别故障:应用级别的故障,是指对业务系统应用程序的故障注入,目前G行应用系统主要涉及的编程语言为Java、C。对于应用系统的故障场景主要为业务处理延时、业务抛出异常、业务数据篡改等,同时对于Java中JVM虚拟机进行针对性的故障比如full-gc、JVM线程池满等故障。
前端/端故障:随着智能手机、平板电脑等设备的普及,移动端成为许多用户获取信息和使用应用程序的主要平台之一。端作为一个重要的业务场景,其稳定性除了受本身产品迭代的影响之外,还有另一个重要的因素--操作系统和手机厂商的影响。由于系统和手机厂商经常会有升级更新,APP相关人员需紧跟其脚步,每次端系统升级都可能带来一些不可预期的影响,端的稳定性越来越受到考验。端系统故障场景主要包括:闪退、白屏、页面crash等属于端稳定性的重要基线指标;前端故障场景主要包括展示乱码、跳转拦截、点击无效等。
综上所述,故障注入场景分类以及各分类下具体场景总结如下脑图:
图1 故障注入场景脑图
故障注入实现步骤
混沌工程的核心之一是故障注入,那么一个被测系统实施故障注入的步骤是?三步走:
第一步:需要明确被测系统部署在哪些机器上;
第二步:这些机器可被混沌工程平台控制;
第三步:开始真正的故障注入。
第一步可以很快确定。
第二步的本质在于混沌工程平台可以对被测机器进行任务下发。业内有两种方式,一种是建立SSH通道如AWX等,一种是安装agent。基于混沌工程的目标:可以稳定的对被测对象进行故障注入,同时观察被测对象的各项监控指标;另外考虑到SSH通道的方式实施下发脚本在安全性上有不稳定的风险,因此混沌工程选择安装agent的方式。
对G行应用物理部署情况分析,被测对象主要分为两大类别:主机和容器。其中主机包括物理机和虚拟机,安装探针agent方式如图2所示。
图2 主机安装探针示意图
对于容器来说,可以分为两类,一类是主机内的容器,一类是k8s集群管理的容器。如果是主机内的容器,通过主机探针安装即可进行实验,如上图2。而k8s集群管理的容器要稍微复杂一些。
在Kubernetes中部署应用,通常我们会选择将应用定义为 Pod、Deployment、Statefulset 等资源类型,这些都是 Kubernetes 内置的资源类型;在实际应用中,内置的资源类型无法满足复杂的需求,Operator 是解决复杂应用容器化的一种方案,通过自定义资源和自定义控制器来实现复杂应用容器化,混沌工程在容器内的实施chaos-operator正是基于 Operator 实现的。
在 Kubernetes中安装完chaos-operator后,会生成一个deployment 资源类型的operator实例、一个daemonset资源类型的agent-tool实例。
用户用来自定义故障实验的资源称为chaosCR;每次新建演练就可以通过 kubectl或者chaos-cli创建chaosCR实例资源,chaosCR资源本身包含了混沌实验定义;chaos-operator会监听chaosCR资源的生命周期,当发现有chaosCR资源实例被创建时,同时就能拿到混沌实验定义,然后解析实验定义,去真正的注入故障--调度Agent-tool;Agent-tool是daemonset资源类型,一个Node节点必定会部署一个Agent-tool Pod,从 Linux Namespace的维度,该Pod网络、PID命名空间与Node节点处在同一命名空间,因此可以做到Node/pod/container各个级别的演练。
图3 k8s集群安装故障注入探针原理示意图
第三步,具体的故障注入执行。由以上分析,无论是主机还是容器,故障注入的执行都是通过被测对象上安装的agent来完成的。Agent不仅负责与混沌平台的通信,同时需要包含具体的故障注入与对被测系统指标数据采集。本文主要聚焦故障注入的部分。
对于第一节中的不同场景,都有其独立的执行器Executor,如下图所示。
图4 混沌工程故障注入原理示意图
故障注入原理剖析
接下来介绍不同的故障注入执行器原理,进一步为大家解开混沌工程故障注入的神秘面纱。
首先是资源类型的故障,该类故障主要指对于被测机器的CPU、MEM、磁盘、网络等资源的故障注入。
表1 资源类型故障原理示意表
另外需申明,对于主机和容器的实施原理还有一个区别点。我们知道,对于 Linux 虚拟化技术而言,核心实现原理就是 Linux Namespace 的隔离能力、Linux Cgroups 的限制能力,以及基于 rootfs 的文件系统;所以容器的执行需要先找到主机上该容器的pid,然后再具体执行相关的命令来达到实验目的。而对于主机而言,无需中间步骤,直接操作即可。
3.2 数据库、中间件类故障
对于数据库、中间件来讲,相关的故障注入分为三个层次:
功能层故障:数据库、中间件本身系统机制类故障,比如Redis大key构造,数据库慢查询,等等,该类故障需要先拿到被测服务本身的信息:比如用户名、密码、地址、端口等信息,进而连接到服务中,进行服务内嵌的操作才能完成。更深一步,还可以通过开发数据库、中间件插件的方式完成能力更强大的故障注入,当然具体情况需要根据实际需求来决定。
高可用层故障:数据库、中间件等验证其自身高可用能力的故障,比如当数据库认证表主节点挂掉时,数据库会进行重新选主;Redis集群中主节点发生网络故障时,会重新选主;数据库数据主节点磁盘I/O hang时,其他从数据节点会迅速接管,等等,该类故障基本可以通过资源类型故障就可以完成。
连接层故障:应用通过调用数据库、中间件而完成业务逻辑,比如jdbc、jedis等连接,这部分故障通过下文中应用类型故障的能力,即可完成相应连接的超时、异常、数据篡改等。下面的内容中会详细介绍。
图5 数据库、中间件与系统之间关系示意图
3.3 应用类型故障
对于应用系统的故障注入,是指对业务系统可以做到接口、代码级的故障注入。所以有一个基本点需要保障:不能因为故障注入而引入新的系统问题,即不能通过预编译代码前插入“脏代码”进行注入故障。G行应用系统主要为Java与C两种语言,目前G行已完成对这两种语言下针对接口、方法级别的延迟、异常、数据篡改等能力。因此主要介绍这两种语言的故障注入能力。
Java语言类系统的故障注入能力,主要使用了字节码增强技术和Java Agent能力。
一个Java语言程序从代码到执行的过程如下图,字节码增强技术(ASM)可以直接生成字节码文件,也可以动态修改字节码文件,
图6 Java程序运行过程图
这样通过字节码增强的技术,就可以对想要注入故障的任意方法通过字节码文件的修改完成。
图7 字节码注入效果示意图
其次,字节码增强技术实现的程序是如何加载到被测程序中的,答案是Java Agent原理。Java Agent(Java 代理)是 JDK 1.5 之后引入的技术。Agent 就是JVMTI的一种实现,Agent 有两种启动方式:一是随Java进程启动而启动;二是运行时载入,通过attach API,将模块(jar包)动态地Attach到指定进程id的Java进程内。具体的实现方式有两种:
根据调研,目前业内对于C语言系统的故障注入能力没有开源方案。对于编译型语言,通常的故障注入方式可以分为debug方式、代码插装技术、网络及内核分析技术(Wireshark、ebpf)。其中与C语言类似的C++语言开发的系统业内有开源的故障注入能力,该方式就是通过debug GDB(程序调试工具)来实现的,该方式有诸多限制:首先要求被注入程序为debug模式,对于很多老的业务系统,打包debug版本成本太高;其次调试方式进行故障注入性能也会受限。而通过网络及内核分析方式的系统入侵,主要是覆盖网络层与系统层的,对于方法调用粒度的故障注入比较有限。因此选择代码插装技术来实现C语言的故障注入。
代码插装技术的原理是一种通过修改机器码的方式来实现hook的技术,对于正常执行的程序,它的函数调用流程大概是这样的:
图8 正常代码函数调用流程示意图
0x1000地址的call指令执行后跳转到0x3000地址处执行,执行完毕后再返回执行call指令的下一条指令。在hook的时候,可能会读取或者修改call指令执行之前所压入栈的内容。那么,可以将call指令替换成jmp指令,jmp到自己编写的函数,在函数里call原来的函数,函数结束后再jmp回到原先call指令的下一条指令。如图:
图9 代码插装后函数调用流程示意图
G行基于调研结果使用Frida进行了实现,主要考虑到以下几点:
对于端故障,从技术栈的角度讲,可以粗略分为后端和前端。后端技术主要涉及的是Java。该类型语言的故障如Java故障的注入前文已做介绍,只是对于端来讲需要以SDK的形式与APP程序一起打包,这里不做赘述。本文主要描述纯前端故障的基本实现原理。
目前,纯前端技术领域满足各种需求的框架可谓百花齐放,但是其基本原理还是基于js与css,因此本文主要介绍基本的故障注入方式,对于不同的框架,要实现具体的故障需要根据框架本身的特点进行具体实现。
图10 纯前端故障注入流程示意图
混沌工程实践效果
技术要为业务服务才具有价值,混沌工程的技术累积只是一个开始,真正发挥价值需要将其应用到实际的混沌实施中。
在根据实际需求建设以上相关能力的同时,G行也在混沌工程的实施方向不断探索。目前主要应用于应用系统、基础软件、红蓝对抗三个方向:
目前已取得阶段性成果:
(1)推广范围:通过异常测试与红蓝对抗等形式,目前已在全行23个重要业务系统进行混沌实施,接入400+虚拟机,容器数量4000+。形成200+实验场景,支撑每月300+次演练任务。同时演练过程具备演练与可观测能力一体化,提升各个应用场景混沌实施、演练效率的同时,助力业务系统快速、提前发现问题。
图11 目前纳管服务拓扑示意图
(2)场景沉淀:分布式数据库、分布式缓存、分布式消息、全栈云等基础领域进行专项混沌实施,并形成各自领域的专家库100+,目前正在深耕金融业务领域。
(3)风险挖掘:实施过程中发现了6类风险,并对此进行整改优化,提升系统的稳定性。
表2 混沌实施发现问题列表
总结
对被测系统的深入掌握是混沌工程的建设和实践的基础,随着混沌工程实施,又会进一步推进我们对被测系统的全面掌握,二者相得益彰,让混沌工程这一提升系统稳定性的利剑发挥出最大价值。
作者:
郭少芬
负责混沌工程的建设与应用推广,爱好旅游、美食。关注个人成长,简单可依赖。
编辑:
肖琦
22年加入光大,目前负责EverDB运维管控平台建设,热爱足球,羽毛球。