TiDB中的混沌测试实践

什么是混沌

\\

在分布式计算领域,我们无法预测集群将会发生什么,一切皆有可能。在里约热内卢飘舞的蝴蝶可能会改变芝加哥的气候,甚至摧毁位于开罗的数据中心。网络时间协议(NTP)可能出现不同步,CPU可能会无缘无故地爆表,或更糟糕的是,勤劳的DBA可能会在半夜意外地删除数据。

\\

TiDB是一款开源的分布式NewSQL混合事务/分析处理(HTAP)数据库,其中保存了客户最重要的数据资产。对于我们的系统来说,容错是最基本和最重要的需求之一。但如何保证分布式数据库的容错?在本文中,我将介绍混沌工程的故障注入工具和技术,以及我们在TiDB中的混沌实践。

\\

为什么我们需要混沌

\\

自从2011年Netflix开发了Chaos Monkey以来,这款软件变得越来越流行。如果想要构建一个分布式系统,就让Chaos Monkey来“糟蹋”这个集群,这样有助于构建出一个更具容错性、弹性和可靠性的系统。

\\

通常情况下,我们会尽可能多地编写单元测试,确保能够覆盖所有的代码逻辑,也会进行足够多的集成测试,确保我们的系统可以与其他组件一起工作,还会执行性能测试,用以改进处理数百万次请求的性能。

\\

然而,这些对于分布式系统来说还远远不够。无论我们做了多少单元测试、集成测试或性能测试,仍然无法保证我们的系统能够应对生产环境的各种不可预测性。我们可能会遇到磁盘故障、机器断电、网络隔离,而这些只是冰山一角。为了让分布式系统(如TiDB)更加健壮,我们需要一种方法来模拟不可预知的故障,并测试我们对这些故障的反应。这就是为什么我们需要Chaos Monkey。

\\

制造“混沌”

\\

Netflix不仅发明了混沌,而且还引入了“混沌工程”的概念,这是一种用于揭示系统缺陷的系统性方法。混沌工程有它自己的核心原则,市面上还有一本关于混沌工程的书:《Building Confidence in System Behavior through Experiments》。

\\

在TiDB中,我们应用混沌工程来观察系统的状态、做出假设、进行实验,并用真实结果验证这些假设。除了遵循混沌原则,我们也会加入自己的想法。这是我们的五步混沌法:

\\
  1. 使用Prometheus作为监控工具来观察TiDB集群的状态和行为,并收集集群的度量指标,用以确定一个稳定的系统应该是什么样的。\\t
  2. 列出一些失败性的假设以及我们的预期。以TiDB为例:如果我们从集群中分离出一个TiKV(TiDB的分布式键值存储层)节点,那么QPS(每秒查询次数)应该会下降,但很快会恢复到另一个稳定状态。\\t
  3. 从列表中选择一个假设。\\t
  4. 通过注入故障和分析结果对所选的假设进行实验。如果结果与我们的假设不同,则可能(或必然)出现错误或遗漏。\\t
  5. 从列表中选择另一个假设进行实验,并重复及自动化这一过程。\

在生产环境中运行实验是混沌工程的原则之一。在为我们的用户部署TiDB之前,我们必须确保它已经经过了严格的测试。不过,我们不能在客户的生产环境中运行这些实验,因为他们把最关键的数据放在TiDB中,表示他们对TiDB的信任,所以我们不能破坏这种来自不易的信任。 我们能做的是建立我们自己的“战场”——一个内部生产环境。

\\

目前,我们使用Jira进行内部问题跟踪和项目管理,同时使用了TiDB作为数据存储,可以说,我们是在自食其力。我们可以在Jira上运行混沌实验。当我们的员工在日常工作中使用Jira时,在没有事先发出任何警告的情况下,向Jira系统注入各种故障,用以模拟一系列级联“事故”来识别可能的系统漏洞。我们称这种做法为“军事演习”,并且经常在我们的日常运维中做这样的事情。在下面的章节中,我将介绍我们如何进行故障注入并自动化这一过程。

\\

TiDB如何进行故障注入

\\

故障注入是一种通过引入故障来测试代码路径(尤其是处理错误的代码路径)以便改进测试覆盖率的技术。它被认为是开发健壮软件系统的重要组成部分。有多种方法可用于进行故障注入,在TiDB中,故障以下列方式注入:

\\
  1. 使用kill -9强制终止进程,或者使用kill来优雅地终止进程,然后重新启动它。\\t
  2. 使用SIGSTOP挂起进程,或使用SIGCONT恢复进程。\\t
  3. 使用renice来调整进程的优先级,或在进程的线程上使用setpriority。\\t
  4. 让CPU超载。\\t
  5. 使用iptables或tc丢弃或拒绝网络数据包,或让网络数据包延迟。\\t
  6. 使用tc重新排列网络数据包,并使用代理重新排列gRPC请求。\\t
  7. 使用iperf获取所有网络吞吐量。\\t
  8. 使用libfuse挂载文件系统并执行IO故障注入。\\t
  9. 链接libfiu以便进行IO故障注入。\\t
  10. 使用rm -rf强制删除所有数据。\\t
  11. 使用echo 0 \u0026gt; file来毁坏文件。\\t
  12. 通过拷贝一个巨大的文件来制造NoSpace问题。\

一些顶级的故障注入工具

\\

内核故障注入

\\

Linux内核中包含了一个流行的故障注入工具Fault Injection Framework,开发人员可以用它执行简单的故障注入来测试设备驱动程序。为了进行更精确的故障注入,例如在用户读取文件时返回错误,或者调用malloc失败,我们使用以下故障注入过程:

\\
  1. 重新构建内核,启用Fault Injection Framework\\t
  2. 使用内核故障注入:\
\echo 1 \u0026gt; /sys/block/vdb/vdb1/make-it-fail\mount debugfs /debug -t debugfs\cd /debug/fail_make_request\echo 10 \u0026gt; interval # interval\echo 100 \u0026gt; probability # 100% probability\echo -1 \u0026gt; times # how many times: -1 means no limit
\\
  1. 在访问该文件时,可能会出现以下错误:\
\

Buffer I/O error on device vdb1, logical block 32538624

\\

lost page write due to I/O error on vdb1

\
\\
  1. 我们可以按照如下方式注入malloc故障:\
\echo 1 \u0026gt; cache-filter\echo 1 \u0026gt; /sys/kernel/slab/ext4_inode_cache/failslab\echo N \u0026gt; ignore-gfp-wait\echo -1 \u0026gt; times\echo 100 \u0026gt; probability
\\
\

cp linux-3.10.1.tar.xz linux-3.10.1.tar.xz.6

\\

cp: cannot create regular file ‘linux-3.10.1.tar.xz.6’: Cannot allocate memory

\
\\

Linux内核的Fault Injection Framework功能很强大,不过我们需要重新构建内核才能启用它,因为有些用户不会在生产环境中开启这一选项。

\\

SystemTap

\\

注入故障的另一种方法是使用SystemTap(https://sourceware.org/systemtap/),这是一种可用于诊断性能或功能问题的工具。我们使用SystemTap来探测内核函数并进行准确的故障注入。例如,我们可以通过执行以下操作来延迟read/write return中的IO操作:

\\
\probe vfs.read.return {\    if (target() != pid()) next\    udelay(300)\}\\probe vfs.write.return {\    if (target() != pid()) next\    udelay(300)\}
\\

我们也可以改变IO返回值。下面我们为read注入一个EINTR,为write注入一个ENOSPC:

\\
\probe vfs.read.return {\    if (target() != pid()) next\    // Interrupted by a signal\    $return = -4\}\\probe vfs.write.return {\    if (target() != pid()) next\    // No space\    $return = -28\}
\\

Fail

\\

有时候,我们想在特定的地方进行故障注入,例如:

\\
\fn save_snapshot() {\    save_data();\    save_meta();\}
\\

我们希望在保存快照数据之后以及保存元数据之前看到系统发生混乱。这个时候应该怎么做?我们可以使用一种称为fail(https://www.freebsd.org/cgi/man.cgi?query=fail\u0026amp;sektion=9\u0026amp;apropos=0\u0026amp;manpath=FreeBSD%2B10.0-RELEASE)的机制。我们可以使用fail将故障注入到任意的地方。在Go语言中,我们可以使用gofail(https://github.com/coreos/gofail),而在Rust中,我们可以使用fail-rs(https://github.com/pingcap/fail-rs)。

\\

对于上面的例子,现在我们可以这样做:

\\
\fn save_snapshot() {\    save_data();\    fail_point!(\"snapshot\");\    save_meta();\}
\\

在这个例子中,我们注入一个叫作“snapshot”的故障点,然后触发它来抛出一个像FAILPOINTS=snapshot=panic(msg) cargo run这样的异常。

\\

故障注入平台

\\

我们已经介绍了一些故障注入方法,除此之外,还有一些平台集成了这些方法。借助这些平台,我们可以进行独立或并行的故障注入。这些平台中最受欢迎的是Namazu(https://github.com/osrg/namazu),一种用于测试分布式系统的可编程模糊调度器。

\\

TiDB中的混沌测试实践_第1张图片

\\

故障注入平台Namazu

\\

我们可以在Namazu容器中运行系统。在容器中,Namazu将通过sched_setattr、带有熔断机制的文件系统和带有netfilter的网络来调度进程。不过,我们发现启用Namazu的文件系统调度程序会导致CentOS 7操作系统崩溃,所以我们只在Ubuntu上运行Namazu。

\\

另一个平台是Jepsen(https://github.com/jepsen-io/jepsen),主要用于验证分布式数据库的线性一致性。Jepsen使用Nemeses来扰乱系统、记录客户端操作,并通过操作历史来验证线性一致性。

\\

我们开发了一个Clojure库来测试TiDB,详情请参阅(https://github.com/pingcap/jepsen/tree/master/tidb)。 Jepsen已被集成到持续集成(CI)工具中,因此TiDB代码库中的每个更新都会自动触发CI来执行Jepsen测试。

\\

自动化混沌:Schrodinger

\\

到目前为止,我们提到的工具或平台都可用于将故障注入到系统中。但为了测试TiDB,我们需要自动化这些测试来提高效率和覆盖率,于是我们开发了Schrodinger。

\\

2015年,在我们刚开始开发TiDB时,每次提交一个功能,都会执行以下操作:

\\
  1. 构建TiDB二进制文件;\\t
  2. 请管理员分配一些机器进行测试;\\t
  3. 部署TiDB二进制文件并运行它们;\\t
  4. 运行测试用例;\\t
  5. 运行Nemeses来注入故障;\\t
  6. 在完成所有测试后,清理所有资源并释放机器。\

所有这些任务都涉及乏味的手动操作。随着TiDB代码库规模的增长,我们需要同时运行许多测试,而手动方式不具备伸缩性。

\\

为了解决这个问题,我们开发了自动执行混沌工程的测试平台Schrodinger。我们只需要配置Schrodinger执行特定的测试任务就可以了,剩下的事情交给平台。

\\

Schrodinger基于Kubernetes(K8s)构建,所以我们不再依赖于物理机器。 K8s隐藏了机器级别的细节,并帮我们将正确的作业安排到合适的机器上。

\\

TiDB中的混沌测试实践_第2张图片

\\

K8s上的Shrodinger架构

\\

下面是Schrodinger的主页屏幕截图,显示了正在运行的测试的概览。我们可以看到两个测试失败,一个测试仍在运行。如果测试失败,会向我们的Slack频道发送告警,并通知开发人员解决问题。

\\

TiDB中的混沌测试实践_第3张图片

\\

Schrodinger主页

\\

如何使用Schrodinger?

\\

使用Schrodinger可分为五步:

\\
  1. 使用Create Cluster Template创建一个TiDB群集。在下面的截图中,我们部署了一个带有3个Placement Driver(PD)服务器、5个TiKV服务器和3个TiDB服务器的TiDB集群。(PD是TiDB集群的管理组件,负责元数据存储、调度、负载均衡,以及分配事务标识。)\

TiDB中的混沌测试实践_第4张图片

\\

创建一个TiDB集群

\\
  1. 使用Create Case Template为集群创建一个测试用例。我们可以使用预先构建的测试用例,如下图所示的bank测试用例,或者让Schrodinger从Git源创建一个新用例。\

TiDB中的混沌测试实践_第5张图片

\\

创建一个TiDB测试用例

\\
  1. 创建一个场景来链接我们在上一步中配置的集群,并将测试用例添加到该集群。\

TiDB中的混沌测试实践_第6张图片

\\

创建一个测试场景

\\
  1. 创建一个任务,告诉Schrodinger TiDB集群的详细版本,并附加一个Slack频道用来接受告警。例如,在下面的截图中,我们让Schrodinger从最新的master分支构建整个群集。\

TiDB中的混沌测试实践_第7张图片

\\

创建一个测试任务

\\
  1. 在创建任务后,Schrodinger开始自动运行所有测试用例。\

TiDB中的混沌测试实践_第8张图片

\\

Shrondinger自动化

\\

Schrodinger现在可以同时在7个不同的群集上运行测试,24/7不间断。我们的团队从人工测试中解放出来,只需要配置测试环境和任务就可以了。

\\

在未来,我们将继续优化这个过程,让Chaos Monkey变得更聪明。我们不想再通过手动的方式配置测试环境和任务,而是让Schrodinger“学习”集群,并找出自动注入故障的方法。Netflix已经在这方面进行了研究,并发表了一篇相关论文:“互联网规模的自动故障测试研究”(https://people.ucsc.edu/~palvaro/socc16.pdf)。我们基于这项研究开始自己的研发工作,而且很快就会向外部分享我们的进展!

\\

TiDB中的TLA+

\\

除了故障注入和混沌工程实践外,我们还使用TLA+,这是一门用于设计、建模、文档化和验证并发系统的语言,旨在验证分布式事务和相关算法的正确性。 TLA+由Leslie Lampor开发,我们已经用它来证明我们的两阶段事务算法(详见https://github.com/pingcap/tla-plus)。我们计划在未来使用TLA+来验证更多的算法。

\\

最后的想法

\\

从我们开始构建TiDB那一刻起,就决定使用混沌测试。混沌是检测分布式系统不确定性、建立系统弹性信心的一种非常好的方式。我们坚信,是否能够恰当且缜密地应用混沌工程将决定分布式系统的成败。

\\

英文原文:https://pingcap.com/blog/chaos-practice-in-tidb/

\\

感谢张婵对本文的审校。

你可能感兴趣的:(TiDB中的混沌测试实践)