在分布式计算领域,我们无法预测集群将会发生什么,一切皆有可能。在里约热内卢飘舞的蝴蝶可能会改变芝加哥的气候,甚至摧毁位于开罗的数据中心。网络时间协议(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中,我们应用混沌工程来观察系统的状态、做出假设、进行实验,并用真实结果验证这些假设。除了遵循混沌原则,我们也会加入自己的想法。这是我们的五步混沌法:
\\在生产环境中运行实验是混沌工程的原则之一。在为我们的用户部署TiDB之前,我们必须确保它已经经过了严格的测试。不过,我们不能在客户的生产环境中运行这些实验,因为他们把最关键的数据放在TiDB中,表示他们对TiDB的信任,所以我们不能破坏这种来自不易的信任。 我们能做的是建立我们自己的“战场”——一个内部生产环境。
\\目前,我们使用Jira进行内部问题跟踪和项目管理,同时使用了TiDB作为数据存储,可以说,我们是在自食其力。我们可以在Jira上运行混沌实验。当我们的员工在日常工作中使用Jira时,在没有事先发出任何警告的情况下,向Jira系统注入各种故障,用以模拟一系列级联“事故”来识别可能的系统漏洞。我们称这种做法为“军事演习”,并且经常在我们的日常运维中做这样的事情。在下面的章节中,我将介绍我们如何进行故障注入并自动化这一过程。
\\故障注入是一种通过引入故障来测试代码路径(尤其是处理错误的代码路径)以便改进测试覆盖率的技术。它被认为是开发健壮软件系统的重要组成部分。有多种方法可用于进行故障注入,在TiDB中,故障以下列方式注入:
\\Linux内核中包含了一个流行的故障注入工具Fault Injection Framework,开发人员可以用它执行简单的故障注入来测试设备驱动程序。为了进行更精确的故障注入,例如在用户读取文件时返回错误,或者调用malloc失败,我们使用以下故障注入过程:
\\\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
\\
\\\Buffer I/O error on device vdb1, logical block 32538624
\\lost page write due to I/O error on vdb1
\
\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(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\}
\\
有时候,我们想在特定的地方进行故障注入,例如:
\\\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),一种用于测试分布式系统的可编程模糊调度器。
\\ \\故障注入平台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测试。
\\到目前为止,我们提到的工具或平台都可用于将故障注入到系统中。但为了测试TiDB,我们需要自动化这些测试来提高效率和覆盖率,于是我们开发了Schrodinger。
\\2015年,在我们刚开始开发TiDB时,每次提交一个功能,都会执行以下操作:
\\所有这些任务都涉及乏味的手动操作。随着TiDB代码库规模的增长,我们需要同时运行许多测试,而手动方式不具备伸缩性。
\\为了解决这个问题,我们开发了自动执行混沌工程的测试平台Schrodinger。我们只需要配置Schrodinger执行特定的测试任务就可以了,剩下的事情交给平台。
\\Schrodinger基于Kubernetes(K8s)构建,所以我们不再依赖于物理机器。 K8s隐藏了机器级别的细节,并帮我们将正确的作业安排到合适的机器上。
\\ \\K8s上的Shrodinger架构
\\下面是Schrodinger的主页屏幕截图,显示了正在运行的测试的概览。我们可以看到两个测试失败,一个测试仍在运行。如果测试失败,会向我们的Slack频道发送告警,并通知开发人员解决问题。
\\ \\Schrodinger主页
\\使用Schrodinger可分为五步:
\\创建一个TiDB集群
\\创建一个TiDB测试用例
\\创建一个测试场景
\\创建一个测试任务
\\Shrondinger自动化
\\Schrodinger现在可以同时在7个不同的群集上运行测试,24/7不间断。我们的团队从人工测试中解放出来,只需要配置测试环境和任务就可以了。
\\在未来,我们将继续优化这个过程,让Chaos Monkey变得更聪明。我们不想再通过手动的方式配置测试环境和任务,而是让Schrodinger“学习”集群,并找出自动注入故障的方法。Netflix已经在这方面进行了研究,并发表了一篇相关论文:“互联网规模的自动故障测试研究”(https://people.ucsc.edu/~palvaro/socc16.pdf)。我们基于这项研究开始自己的研发工作,而且很快就会向外部分享我们的进展!
\\除了故障注入和混沌工程实践外,我们还使用TLA+,这是一门用于设计、建模、文档化和验证并发系统的语言,旨在验证分布式事务和相关算法的正确性。 TLA+由Leslie Lampor开发,我们已经用它来证明我们的两阶段事务算法(详见https://github.com/pingcap/tla-plus)。我们计划在未来使用TLA+来验证更多的算法。
\\从我们开始构建TiDB那一刻起,就决定使用混沌测试。混沌是检测分布式系统不确定性、建立系统弹性信心的一种非常好的方式。我们坚信,是否能够恰当且缜密地应用混沌工程将决定分布式系统的成败。
\\英文原文:https://pingcap.com/blog/chaos-practice-in-tidb/
\\感谢张婵对本文的审校。