使用 unistore 对 TiDB 快速进行『回表优化』原型验证

很早之前,TiDB 流传着一个段子 - 『每天只有 24 次编译 TiKV 的机会』,虽然现在这个黑历史早就成了过去,完整编译一次 TiKV 的时间其实也就是 10 分钟,使用 debug 编译速度会更快,但实话,对于想快速开发进行原型验证的同学,有时候这个耗时还是不能接受。

如果我们有一个 Go 的程序,能模拟 TiKV,那么我们所有的快速验证都可以使用 Go 来进行,这样能大大的提升原型验证效率,幸运的是,我们早就有了这样的东东 - unistore。使用 unistore 非常的简单,直接 make 就能编译出来一个 binary,然后使用 ./bin/unistore-server --data-dir ./db 就能启动了,当然使用之前要把 PD 启动起来,对于 TiDB 则是按照使用 TiKV 的方式。有了 unistore,对我们有什么好处呢?直观的就是原型验证会非常快速了。

最近我在思考如何减少 TiDB 和 TiKV 之间读取数据的回表操作,假设现在我们有一张表,结构如下:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  `name` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `k` (`k`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

INSERT INTO t VALUES (3, 3, "c");

我们现在插入了一条数据,然后使用 SELECT * FROM t WHERE k = 3 来查询,对于 TiDB 来说,底层的逻辑如下:

  1. TiDB 首先使用 unique index 来获得 k = 3 这条数据实际的 primary key,也就是 id = 3
  2. TiDB 使用 primary key id = 3 拿到实际的数据

上面的步骤,我们俗称回表操作,但这个回表操作有一个很严重的问题,就是要有两次网络交互。但实际,在上面这个 case 里面,表的 index 和实际的 row 数据都是在一个 TiKV region 里面,也就是说,我们在通过 k = 3 拿到对应的 primary key 之后,直接可以进行 id = 3 的数据读取,然后将实际的 row 给返回。(虽然我们马上要支持的 clustered index 能缓解不少,但只要是通过其他 index 来查询,仍然会遇到网络回表问题)

优化前后的效果大概如下

理论上上面这个优化一定是能提速的,剩下的当然是快速的原型验证,所以这里选择 unistore,因为上面的两步,对应的协议都是 KvGet,首先我们先改下 proto,如下:

message GetResponse {
    // A region error indicates that the request was sent to the wrong TiKV node
    // (or other, similar errors).
    errorpb.Error region_error = 1;
    // A value could not be retrieved due to the state of the database for the requested key.
    KeyError error = 2;
    // A successful result.
    bytes value = 3;
    // True if the key does not exist in the database.
    bool not_found = 4;
    bytes lookup_value = 5;
}

我们添加了一个 lookup_value 字段,用来存储回表操作读取的值。然后在 unistore 的 KvGet 里面,做如下改动:

    val = safeCopy(val)

    var lookupValue []byte

    {
        tableID, indexID, _, _ := tablecodec.DecodeKeyHead(req.Key)
        if indexID > 0 && len(val) > 0 {
            var iv kv.Handle
            iv, err = tablecodec.DecodeHandleInUniqueIndexValue(val, false)
            if err == nil {
                key := tablecodec.EncodeRowKeyWithHandle(tableID, iv)
                lookupValue, _ = reader.Get(key, req.GetVersion())
            }
        }
    }

    return &kvrpcpb.GetResponse{
        Value: val,
        LookupValue: lookupValue,
    }, nil
}

上面的逻辑是先尝试解开 key,如果这个 key 是一个 index,那么我们就尝试按照 unique index 的方式解码,这个其实就能得到实际的 primary key 了,然后通过这个 primary key 直接读取数据,放到 lookup value 里面一起返回。

然后在 TiDB 这一层,因为外面其实使用的 KV interface 来跟 unistore 交互,为了不改动接口,我们使用 context 的方式,将 lookup value 给传递到请求处理那边,然后 get response 之后将这个值给设置上去,类似如下:

var lookupValue *[]byte
if v := bo.ctx.Value("lookup_value"); v != nil {
    lookupValue = v.(*[]byte)
}

req := tikvrpc.NewReplicaReadRequest(tikvrpc.CmdGet,
    &pb.GetRequest{
        Key:     k,
        Version: s.version.Ver,
    }, s.replicaRead, &s.replicaReadSeed, pb.Context{
        Priority:     s.priority,
        NotFillCache: s.notFillCache,
        TaskId:       s.taskID,
    })
for {
    loc, err := s.store.regionCache.LocateKey(bo, k)
    if err != nil {
        return nil, errors.Trace(err)
    }
    resp, _, _, err := cli.SendReqCtx(bo, req, loc.Region, readTimeoutShort, kv.TiKV, "")
    ...
    cmdGetResp := resp.Resp.(*pb.GetResponse)
    val := cmdGetResp.GetValue()
    ...

    if lookupValue != nil {
        // 这里设置了 lookup value
        *lookupValue = cmdGetResp.LookupValue
    }

我们在 PointGet 的 Next 函数里面,做如下改动:

var lookupValue []byte
// 这里我们要传一个引用进去,这样 get 里面才能设置值
ctx1 := context.WithValue(ctx, "lookup_value", &lookupValue)

e.handleVal, err = e.get(ctx1, e.idxKey)
if err != nil {
    if !kv.ErrNotExist.Equal(err) {
        return err
    }
}

然后如果有 lookup value,我们就不在进行回表操作了:

var val []byte
if len(lookupValue) == 0 {
    var err error 
    key := tablecodec.EncodeRowKeyWithHandle(tblID, e.handle)
    val, err = e.getAndLock(ctx, key)
    if err != nil {
        return err
    }       
} else {
    val = lookupValue
}

做了如下更新之后,我们基于 sysbench 框架来测试,使用的 sysbench PointGet,当然,我把测试代码改成了 SELECT * FROM t WHERE k = 3,结果如下:

// 优化后
[ 10s ] thds: 32 tps: 39734.48 qps: 39734.48 (r/w/o: 39734.48/0.00/0.00) lat (ms,95%): 1.30 err/s: 0.00 reconn/s: 0.00
[ 20s ] thds: 32 tps: 37079.88 qps: 37079.88 (r/w/o: 37079.88/0.00/0.00) lat (ms,95%): 1.34 err/s: 0.00 reconn/s: 0.00


// 优化前
[ 10s ] thds: 32 tps: 30474.39 qps: 30474.39 (r/w/o: 30474.39/0.00/0.00) lat (ms,95%): 1.61 err/s: 0.00 reconn/s: 0.00
[ 20s ] thds: 32 tps: 26815.95 qps: 26815.95 (r/w/o: 26815.95/0.00/0.00) lat (ms,95%): 1.89 err/s: 0.00 reconn/s: 0.00

可以看到,不通过网络回表,对于使用 unique index 来进行点查的情况性能至少能提升 40% 以上,这个收益还是蛮可观的,而且如果网络有延迟,这个收益会更大。

上面只是简单的做了一个原型验证,那么,为啥我们要做这个事情呢?主要对于一些场景是真的有用,譬如 TPC-C,或者银行的核心交易场景,数据会有明显的分区特性,而一个分区的实际数据大小又不会太大,所以多数时候,我们都可以通过调度让分区的数据尽量聚集到一起,这样我们的回表读取都是可以不用走网络了。

当然实际要做的工作还有很多,强烈建议感兴趣的同学参与进来,直观的有:

  • 为 batch get,scan 甚至 coprocessor 都提供本地回表功能,这个工作量就已经很大了。
  • 如果不在同一个 region,在 TiKV 上面跨 region 读取,要考虑 region leader 问题。
  • 现在 TiDB 是基于 region 进行调度的,一个分区可能有不同的 table,也就是会有不同的 region,我们后面也需要让 PD 能支持按照分区进行调度。

上面只是使用 unistore 的一个简单例子,从规划原型,写代码,跑通流程,改 sysbench 脚本进行落地验证,我总共花了不到 1 个小时吧,可以看到效率还是非常惊人的。所以,你如果喜欢 TiDB 但又不想被 Rust 虐待,欢迎尝试下 unistore。另外,我们很多新奇的 idea 也会在 unistore 提前验证,所以你如果想更加深度的参与到 TiDB 开发中,请联系我们。

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

推荐阅读更多精彩内容