使用 Chaos 测试分布式系统线性一致性

背景

在之前的文章 测试分布式系统的线性一致性 以及 使用 Porcupine 进行线性一致性测试 中,我介绍了 Go 的线性一致性测试工具 Porcupine 以及一些简单使用的例子,这里我将简单介绍一下基于 Porcupine 的一款简单的分布式线性一致性测试框架:Chaos

对于分布式系统的线性一致性测试,通常我们都会使用 jepsen,TiDB 当然也支持 jepsen,那么为啥还是费力的再去捣鼓一个线性一致性测试框架呢?我觉得主要有以下几点:

  • Clojure:jepsen 使用的是 clojure,一门跑在 JVM 上面的函数式编程语言。虽然它很强大,但我并不精通。所以每次看 jepsen 的代码对我都是一种折磨,而且我们 team 里面大部分同学也完全不会。
  • OOM:jepsen 的 linearizability check 只要稍微跑长一点时间,就非常容易 OOM,所以我们的测试 case 都不会跑特别久。

我其实一直有一个用 Go 写一个线性一致性测试框架的想法,但主要困难在于如何去 linearizability check,幸运的是我找到了 porcupine,自然整个工作就能开动了,于是先捣鼓了一个简单的 chaos,如果可行就继续完善。

架构

类似于 jepsen,chaos 也将 DB service 跑在五个 node 上面,node 的命名就是 n1 到 n5,我们也能够通过名字连接到对应的 node 上,譬如我们可以通过 ssh n1 就能直接登录到 node n1。

Chaos 也有一个 controller 节点,用来控制整个集群,包括初始化要测试的 DB,创建对应的 client 跑实际的 test,启动 nemesis 来干扰系统,最后验证 history 的 linearizability 等。架构图如下:

不同于 jepsen 的地方在于,在 jepsen 里面,controller 是全部通过 ssh 发送命令到 node 节点去执行所有的操作,但 chaos 会在每一个 node 上面启动一个 agent,controller 通过 HTTP API 跟 agent 交互,来操作 node。之所以这么设计,主要就是想用 Go 直接写相关的 DB,nemesis 逻辑,而不是像 jepsen 那样每次用 Linux 的 command 来操作。

但是用 agent 唯一问题在于需要显式的在不同的 node 上面先启动 agent,使用上面比 jepsen 稍微麻烦一点,但也可以通过脚本来搞定。

因为 Go 是一门静态语言,所以如果我们需要在 chaos 里面验证自己 DB 的线性一致性,需要首先实现相关的 interface,然后注册给 chaos,这样 chaos 才能对其验证。这里,我们以 TiDB 为例来进行说明。

DB

DB interface 对应的就是我们实际要进行测试的 DB,DB interface 定义如下:

type DB interface {
    SetUp(ctx context.Context, nodes []string, node string) error
    TearDown(ctx context.Context, nodes []string, node string) error
    Start(ctx context.Context, node string) error
    Stop(ctx context.Context, node string) error
    Kill(ctx context.Context, node string) error
    IsRunning(ctx context.Context, node string) bool
    Name() string
}

我们在 SetUp 函数里面初始化整个 DB 集群,用 TearDown 来析构整个集群。Start,Stop 等函数的含义非常明了,这里不做说明。Name 就是 DB 名字,因为我们是要注册给 chaos 的,所以名字必须唯一,譬如我们的 TiDB 的 Name 就是
“tidb”。

参数 nodes 就是整个集群所在的 Node 信息,通常就是 n1 到 n5,node 就是当前 Node 节点的名字。

在 TiDB 中,我们在 SetUp 函数里面,下载 TiDB binary,解压放到固定位置,然后更新配置文件,然后启动整个集群。而 TearDown 则是直接发送 kill 命令干掉了整个集群。在 Start 函数里面,我们会在每个 Node 上面分别启动 pd-server,tikv-server 和 tidb-server。

当我们实现了 TiDB 的 DB 接口之后,我们就通过 RegisterDB 函数将 TiDB 注册到 chaos,这样我们就能在 agent 里面通过 DB name 找到 TiDB 并操作了。

Client

Client 就是 controller 这边用来跟要测试的 DB 交互的组件。Client interface 定义如下:

type Client interface {
    SetUp(ctx context.Context, nodes []string, node string) error
    TearDown(ctx context.Context, nodes []string, node string) error
    Invoke(ctx context.Context, node string, request interface{}) interface{}
    NextRequest() interface{}
}

Invoke 函数就是 Client 实际给 DB 发送命令的接口,因为我们并不知道不同 DB client 的命令参数,所以这里的 request 就是一个 interface。Invoke 执行完毕会返回一个 response,我们也不知道各个 client 实际的 response,也用 interface 来表示。

NextRequest 返回的是下一个可以被 Invoke 的 request,因为只有 client 自己知道如何去构造一个 request。

在 TiDB bank case 里面,我们会定义一个 bank client,每次 NextRequest 的时候会随机选择是查询所有账户的数据,还是选择两个账户进行转账。如果是 read,那么 response 就是查询的数据,如果是 transfer,那么 response 就是是否成功。这里需要注意,对于分布式系统来说,一个操作可能有三种结果,成功,失败和未知,所以我们在 response 这边也需要考虑处理 Unknown 的情况。具体可以参考 issue 上面的讨论。

因为我们有 5 个 Node,controller 这边每个 Node 会有一个 client 对应,所以实际我们也需要实现一个 client creator,用来生成多个 client。

type ClientCreator interface {
    Create(node string) Client
}

Linearizability check

上面我们说到了 client 的接口,我们会用 NextRequest 生成一个 request,然后去 invoke 这个 request,得到一个 response。Controller 这边会将 request 和 response 都记录到一个 history 文件里面。所以一次 operation,是有一个 request 和 一个 response 两个事件的。

为了简单,我们是将 request 和 response 直接用 JSON 编码写入到 history 里面的。当测试跑完之后,我们需要分析这个 history 文件是否是线性一致的。首先,我们就需要去解析这个 history,这里我们需要实现一个 record parser:

type RecordParser interface {
    OnRequest(data json.RawMessage) (interface{}, error)
    OnResponse(data json.RawMessage) (interface{}, error)
    OnNoopResponse() interface{}
}

当 parser 读取一行 record 之后,我们会首先判断这行 record 是 request 还是 response,然后调用对应的 RecordParser 接口,再对数据进行解码成实际的类型。

这里需要注意 OnNoopResposne 接口,上面说过,所以 Unknown 的 response,我们在 OnResponse 这个函数需要返回 nil,让 chaos 先忽略这次事件,然后在最后调用 OnNoopResposne 得到一个 Response,补全之前的 operation。

要实现 linearizability check,我们还需要实现自己的 porcupine model,然后调用函数 VerifyHistory(historyFile string, m porcupine.Model, p RecordParser) 来对生成的 history 进行验证。

在 TiDB bank 的 porcupine model 关键 step 函数定义如下:

Step: func(state interface{}, input interface{}, output interface{}) (bool, interface{}) {
    st := state.([]int64)
    inp := input.(bankRequest)
    out := output.(bankResponse)

    if inp.Op == 0 {
        // read
        ok := out.Unknown || reflect.DeepEqual(st, out.Balances)
        return ok, state
    }

    // for transfer
    if !out.Ok && !out.Unknown {
        return true, state
    }

    newSt := append([]int64{}, st...)
    newSt[inp.From] -= inp.Amount
    newSt[inp.To] += inp.Amount
    return out.Ok || out.Unknown, newSt
}

如果是 read 操作,那么就判断这次的结果跟上次状态的是否一致,或者是否是 Unknown,如果是 transfer,那么就在现有状态上面,执行一次转账操作,返回新的状态。

Nemesis

在跑测试的时候,controller 也会定期的执行一些 nemesis 操作去干扰整个系统,譬如一下子 kill 所有的 DB,或者 drop 掉相关从一些 Node 上面发过来的网络包这些。Nemesis interface 定义如下:

type Nemesis interface {
    Invoke(ctx context.Context, node string, args ...string) error
    Recover(ctx context.Context, node string, args ...string) error
    Name() string
}

因为 nemesis 也是要注册给 chaos 使用,所以 Name 必须唯一。我们使用 Invoke 对系统干扰,然后 Recover 恢复系统。当实现了自己的 nemesis 之后,也需要调用 RegisterNemesis 来进行注册,这样 agent 才能使用。

在 controller 这边,我们需要实现 NemesisGenerator:

type NemesisGenerator interface {
    Generate(nodes []string) []*NemesisOperation
    Name() string
}

Generate 会对每个 Node 生成一个 NemesisOperation 操作,NemesisOperation 里面就定义了需要执行的 nemesis,以及相关的参数,和执行时间。Controller 会将 NemesisOperation 发送给 agent,让 agent 去执行对应的 nemesis。

Agent and Controller

当我们定义好自己的 DB,Client,Nemesis 等之后,我们就需要将其整合到一起了。我们需要在 agent 里面首先注册自己的 DB 以及相关的 nemesis。在 cmd/agent/main.go 文件里面,TiDB 相关的注册代码如下:

    // register nemesis
_ "github.com/siddontang/chaos/pkg/nemesis"

// register tidb
_ "github.com/siddontang/chaos/tidb"

然后启动 node, 之后我们通过

NewController(cfg *Config, clientCreator core.ClientCreator, nemesisGenerators []core.NemesisGenerator) *Controller`

创建一个 controller,controller 需要接受一个 ClientCreator 以及一个 nemesis generator 的列表。Config 里面会指定这次测试每个 client 最多发送的 request 个数,以及整个测试执行的时间,以及要操作的 DB name 等。

启动 controller,执行测试,最后结束之后,会有一个 history 文件生成,我们就可以验证线性一致性了。

总结

Chaos 现阶段只是一个非常初级的版本,还有很多工作需要完善,譬如更好的 interface 定义,更易于使用这些。但现在至少是能 work 的,现在只有 TiDB 的转账测试,后面,我会给 TiDB 多加入几个线性一致性测试,如果大家感兴趣,也欢迎加入其他开源项目的线性一致性测试 case。

chaos: https://github.com/siddontang/chaos

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 最近看到一篇文章 http://www.anishathalye.com/2017/06/04/testing-d...
    siddontang阅读 6,637评论 1 16
  • 创业之初是自己单干还是与人合伙?这个问题是创业初期的创业者免不了面对的问题,自己单干当然可以,但创业初期总归...
    巾喜一次性毛巾阅读 292评论 1 4
  • 不知从什么时候开始,到处都是高喊着"女人一胖毁所有"的口号,声嘶力竭的激励女性瘦身减重的鸡汤文。好像只要有一副...
    zero007阅读 865评论 13 7
  • 刚进大学的时候,对不喜欢清洗身体室友抱有深刻的不满,脑海习惯性的浮现出不爱干净、懒散、宅等诸多贬义词。出现这样的情...
    天造蠢材阅读 282评论 0 0