作者:唐刘
在对 TiDB 进行 Chaos 实践的时候,我一直在思考如何更好的发现 TiDB 整个系统的故障。最开始,我们参考的就是 Chaos Engineering 里面的方式,观察系统的稳定状态,注入一个错误,然后看 metrics 上面有啥异常,这样等实际环境中出现类似的 metrics,我们就知道发现了什么故障。
但这套机制其实依赖于如何去注入错误,虽然现在我们已经有了很多种错误注入的方式,但总有一些实际的情况我们没有料到。所以后来我们又考虑了另外的一种方式,也就是直接对 metrics 历史进行学习,如果某一段时间 metrics 出现了不正常的波动,那么我们就能报警。但这个对我们现阶段来说难度还是有点大,只使用了几种策略,对 QPS,Latency 这些进行了学习,并不能很好的定位到具体出了什么样的问题。
所以我一直在思考如何更好的去发现系统的故障。最近,刚好看到了 OSDI 2018 一篇 Paper,Capturing and Enhancing In Situ System Observability for Failure Detection,眼睛一亮,觉得这种方式也是可以来实践的。
大家都知道,在生产环境中,故障是无处不在,随时可能发生的,譬如硬件问题,软件自身的 bug,或者运维使用了一个错误的配置这些。虽然多数时候,我们的系统都做了容错保护,但我们还是需要能尽快的发现故障,才好进行故障转移。
但现实世界并没有那么美好,很多时候,故障并不是很明显的,譬如整个进程挂掉,机器坏掉这些,它们处于一种时好时坏的状态,我们通常称为「Gray Failure」,譬如磁盘变慢了,网络时不时丢包。这些故障都非常隐蔽,很难被发现。如果单纯的依赖外部的工具,其实很难检测出来。
上面是作者举的一个 ZooKeeper 的例子,client 已经完全不能跟 Leader 进行交互了,但是 Leader 却仍然能够给 Follower 发送心跳,同时也能响应外面 Monitor 发过来的探活命令。
如果从外面的 Monitor 看来,这个 ZooKeeper 集群还是正常的,但其实它已经有故障了。而这个故障其实 client 是知道的,所以故障检测的原理很简单,从发起请求的这一端来观察,如果发现有问题,那就是有故障了。而这也是这篇论文的中心思想。
在论文里面,作者认为,任何严重的 Gray Failure 都是能够被观察到的,如果发起请求的这边遇到了错误,自然下一件事情就是将这个错误给汇报出去,这样我们就知道某个地方出现了故障。于是作者开发了 Panorama 这套系统,来对故障进行检测。
先来说说 Panorama 一些专业术语。
Panorama 整体结构如下:
Panorama 通过一些方式,譬如静态分析代码进行代码注入等,将 Observer 跟要观察的 Subject 进行绑定,Observer 会将 Subject 的一些信息记录并且汇报给本地的一个 Local Observation Store ( LOS )。本地一个决策引擎就会分析 LOS 里面的数据来判断这个组件的状态。如果多个 LOS 里面都有对某个 Subject 的 observation,那么 LOS 会相互交换,用来让中央的 verdict 更好的去判断这个 component 的状态。
而用来判断一个 component 是不是有故障也比较容易,采用的是一种大多数 bounded-look-back 算法。对于一个 subject,它可能会有很多 observations,首先我们会对这些 observations 按照 observer 进行分组,对每组单独进行分析。在每个组里面,observations 会按照时间从后往前检查,并且按照 context 进行聚合。如果一个被观察的 observation 的 status 跟记录前面相同 context 的 observation status 状态不一样,就继续 loop-back,直到遇到一个新的 status。对于一个 context,如果最后的状态是 unhealthy 或者 healthy 的状态没有达到多数,就会被认为是 unhealthy 的。
通过这种方式,我们在每组里面得到了每个 context 的状态,然后又会在多个组里面进行决策,也就是最常用的大多数原则,哪个状态最多,那么这个 context 对应的状态就是哪一个。这里我们需要额外处理下 PENDING 这个状态,如果当前状态是 HEALTHY 而之前老的状态是 PENDING,那么 PENDING 就会变成 HEALTHY,而如果一直是 PENDING 状态并超过了某个阈值,就会退化成 UNHEALTHY。
这里再来说说 Observability 的模式。对于分布式系统来说,不同 component 之间的交互并不是同步的,我们会面临如下几种情况:
如果两个组件 C1 和 C2 是同步交互,那么当 C1 给 C2 发送请求,我们就完全能在 C1 这一端知道这次请求成功还是失败了,但是对于非同步的情况,我们可能面临一个问题,就是 C1 给 C2 发了请求,但其实这个请求是放到了异步消息队列里面,但 C1 觉得是成功了,可是后面的异步队列却失败了。所以 Panorama 需要有机制能正确处理上面多种情况。
为了能更好的从 component 上面得到有用的 observations,Panorama 会用一个离线工具对代码进行静态分析,发现一些关键的地方,注入钩子,这样就能去汇报 observations 了。
通常运行时错误是非常有用的能证明有故障的证据,但是,并不是所有的错误都需要汇报,Panorama 仅仅会关系跨 component 边界产生的错误,因为这也是通过发起请求端能观察到的。Panorama 对于这种跨域的函数调用称为 observation boundaries。对于 Panorama 来说,第一件事情就是定位 observation boundaries。通常有两种 boundaries,进程间交互和线程间交互。进程间交互通常就是 socket I/O,RPC,而线程间则是在一个进程里面跨越线程的调用。这些 Panorama 都需要分析出来。
当定位了 observation boundaries 之后,下一件事情就是确定 observer 和 subject 的标识。譬如对于进程间交互的 boundaries,observer 的标识就可能是这个进程在系统里面的唯一标识,而对于 subject,我们可以用 method 名字,或者是函数的一个参数,类里面的一个字段来标识。
然后我们需要去确定 observation points,也就是观测点。通常这些点就是代码处理异常的地方,另外可能就是一些正常处理返回结果但会对外报错的地方。
上面就是一个简单分析代码得到 observation points 的例子,但这个仍然是同步的,对于 indirection 的,还需要额外处理。
对于异步请求,我们知道,通过发出去之后,会异步的处理结果,所以这里分为了两步,叫做 ob-origin 和 ob-sink。如下:
对于 ob-origin,代码分析的时候会先给这个 observation 设置成 PENDING 状态,只有对应的 ob-sink 调用并且返回了正确的结果,才会设置成 HEALTHY。因为 ob-origin 和 ob-sink 是异步的,所以代码分析的时候会加上一个特殊的字段,包含 subject 的标识和 context,这样就能让 ob-origin 和 ob-sink 对应起来。
上面大概介绍了 Panorama 的架构以及一些关键的知识点是如何实现的,简单来说,就是在一些关键代码路径上面注入 hook,然后通过 hook 对外将相关的状态给汇报出去,在外面会有其他的分析程序对拿到的数据进行分析从而判定系统是否在正常工作。它其实跟加 metrics 很像,但 metrics 只能看出哪里出现了问题,对于想更细致定位具体的某一个问题以及它的上下文环境,倒不是特别的方便。这点来说 Panorama 的价值还是挺大的。
Panorama 的代码已经开源,总的来说还是挺简单的,但我没找到核心的代码分析,注入 hook 这些,有点遗憾。但理解了大概原理,其实先强制在代码写死也未尝不可。另一个比较可行的办法就是进行在代码里面把日志添加详细,这样就不用代码注入了,而是在外面写一个程序来分析日志,其实 Panorama 代码里面提供了日志分析的功能,为 ZooKeeper 来设计的,但作者自己也说到,分析日志的效果比不上直接在代码里面进行注入。
那对我们来说,有啥可以参考的呢?首先当然是这一套故障检查的理念,既然 Panorama 已经做出来并且能发现故障量,自然我们也可以在 TiDB 里面实施。因为我们已经有在 Go 和 Rust 代码里面使用 fail 来进行错误注入的经验,所以早期手写监控代码也未尝不可,但也可以直接完善日志,提供一个程序来分析日志就成。如果你对这块感兴趣,想把 Panorama 相关的东西应用到 TiDB 中来,欢迎联系我 [email protected] 。