TiDB 中的混沌测试实践

阅读数:5037 2018 年 7 月 24 日 18:11

什么是混沌

在分布式计算领域,我们无法预测集群将会发生什么,一切皆有可能。在里约热内卢飘舞的蝴蝶可能会改变芝加哥的气候,甚至摧毁位于开罗的数据中心。网络时间协议(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 集群的状态和行为,并收集集群的度量指标,用以确定一个稳定的系统应该是什么样的。
  2. 列出一些失败性的假设以及我们的预期。以 TiDB 为例:如果我们从集群中分离出一个 TiKV(TiDB 的分布式键值存储层)节点,那么 QPS(每秒查询次数)应该会下降,但很快会恢复到另一个稳定状态。
  3. 从列表中选择一个假设。
  4. 通过注入故障和分析结果对所选的假设进行实验。如果结果与我们的假设不同,则可能(或必然)出现错误或遗漏。
  5. 从列表中选择另一个假设进行实验,并重复及自动化这一过程。

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

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

TiDB 如何进行故障注入

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

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

一些顶级的故障注入工具

内核故障注入

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

  1. 重新构建内核,启用 Fault Injection Framework
  2. 使用内核故障注入:
复制代码
echo 1 > /sys/block/vdb/vdb1/make-it-fail
mount debugfs /debug -t debugfs
cd /debug/fail_make_request
echo 10 > interval # interval
echo 100 > probability # 100% probability
echo -1 > 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 > cache-filter
echo 1 > /sys/kernel/slab/ext4_inode_cache/failslab
echo N > ignore-gfp-wait
echo -1 > times
echo 100 > 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&sektion=9&apropos=0&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中的混沌测试实践

故障注入平台 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 二进制文件 ;
  2. 请管理员分配一些机器进行测试 ;
  3. 部署 TiDB 二进制文件并运行它们 ;
  4. 运行测试用例 ;
  5. 运行 Nemeses 来注入故障 ;
  6. 在完成所有测试后,清理所有资源并释放机器。

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

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

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

TiDB中的混沌测试实践

K8s 上的 Shrodinger 架构

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

TiDB中的混沌测试实践

Schrodinger 主页

如何使用 Schrodinger?

使用 Schrodinger 可分为五步:

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

TiDB中的混沌测试实践

创建一个 TiDB 集群

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

TiDB中的混沌测试实践

创建一个 TiDB 测试用例

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

TiDB中的混沌测试实践

创建一个测试场景

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

TiDB中的混沌测试实践

创建一个测试任务

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

TiDB中的混沌测试实践

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/

感谢张婵对本文的审校。

评论

发布