V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
Aidenboss
V2EX  ›  Go 编程语言

分享个自己写的项目: SDB

  •  
  •   Aidenboss · 2021-12-10 00:29:23 +08:00 · 3303 次点击
    这是一个创建于 1114 天前的主题,其中的信息可能已经有所发展或是发生改变。

    SDB :纯 golang 开发、数据结构丰富、持久化的 NoSQL 数据库


    为什么需要 SDB ?

    试想以下业务场景:

    • 计数服务:对内容的点赞、播放等数据进行统计
    • 评论服务:发布评论后,查看某个内容的评论列表
    • 推荐服务:每个用户有一个包含内容和权重推荐列表

    以上几个业务场景,都可以通过 MySQL + Redis 的方式实现。 这里的问题是:MySQL 更多的是充当持久化的能力,Redis 充当的是在线服务的读写能力。

    那么只使用 Redis 行不行? 答案是否定的,因为 Redis 无法保证数据不丢失。

    那有没有一种存储能够支持高级的数据结构,并能够将数据进行持久化的呢?

    答案是:非常少的。有些数据库要么是支持的数据结构不够丰富,要么是接入成本太高,要么是不可控。

    为了解决上述问题,SDB 产生了。


    SDB 简单介绍


    快速使用

    服务端使用

    sh ./scripts/quick_start.sh
    

    默认使用 pebble 存储引擎。启动后,端口会监听 9000 端口

    客户端使用

    package main
    
    import (
    	"github.com/yemingfeng/sdb/pkg/pb"
    	"golang.org/x/net/context"
    	"google.golang.org/grpc"
    	"log"
    )
    
    func main() {
    	conn, err := grpc.Dial(":9000", grpc.WithInsecure())
    	if err != nil {
    		log.Printf("faild to connect: %+v", err)
    	}
    	defer conn.Close()
    
    	// 连接服务器
    	c := pb.NewSDBClient(conn)
    	setResponse, err := c.Set(context.Background(),
    		&pb.SetRequest{Key: []byte("hello"), Value: []byte("world")})
    	log.Printf("setResponse: %+v, err: %+v", setResponse, err)
    	getResponse, err := c.Get(context.Background(),
    		&pb.GetRequest{Key: []byte("hello")})
    	log.Printf("getResponse: %+v, err: %+v", getResponse, err)
    }
    

    更多客户端例子


    配置大全

    参数名 含义 默认值
    store.engine 存储引擎,可选 pebble 、level 、badger pebble
    store.path 存储目录 ./db
    server.grpc_port grpc 监听的端口 9000
    server.http_port http 监控的端口,供 prometheus 使用 8081
    server.rate 每秒 qps 的限制 30000
    server.slow_query_threshold 慢查询记录的阈值,单位为 ms 100

    性能测试

    测试脚本:benchmark

    测试机器:MacBook Pro (13-inch, 2016, Four Thunderbolt 3 Ports)

    处理器:2.9GHz 双核 Core i5

    内存:8GB

    测试结果:peek QPS > 12k ,avg QPS > 7k ,set avg time < 70ms ,get avg time < 0.2ms


    监控

    安装 docker 版本 grafana 、prometheus (可跳过)

    配置 grafana

    • 打开 grafana: http://localhost:3000 (注意替换 ip 地址)
    • 新建 prometheus datasources: http://host.docker.internal:9090 (如果使用 docker 安装则为这个地址。如果 host.docker.internal 无法访问,就直接替换 prometheus.yml 文件的 host.docker.internal 为自己的 ip 地址就行)
    • scripts/dashboard.json 文件导入 grafana dashboard

    最终效果可参考:性能测试的 grafana 图


    SDB 背后的思考

    SDB 存储引擎选型

    SDB 项目最核心的问题是数据存储方案的问题。

    首先,我们不可能手写一个存储引擎。这个工作量太大,而且不可靠。 我们得在开源项目中找到适合 SDB 定位的存储方案。

    SDB 需要能够提供高性能读写能力的存储引擎。 单机存储引擎方案常用的有:B+ 树、LSM 树、B 树等。

    还有一个前置背景,golang 在云原生的表现非常不错,而且性能堪比 C 语言,开发效率也高,所以 SDB 首选使用纯 golang 进行开发。

    那么现在的问题变成了:找到一款纯 golang 版本开发的存储引擎,这是比较有难度的。收集了一系列资料后,找到了以下开源方案:

    综合来看,golangdb 、badger 、pebble 这三款存储引擎都是很不错的。

    为了兼容这三款存储引擎,SDB 提供了抽象的接口 ,进而适配这三个存储引擎。


    SDB 数据结构设计

    SDB 已经通过上面三款存储引擎解决了数据存储的问题了。 但如何在 KV 的存储引擎上支持丰富的数据结构呢?

    以 pebble 为例子,首先 pebble 提供了以下的接口能力:

    • set(k, v)
    • get(k)
    • del(k)
    • batch
    • iterator

    接下来,我以支持 List 数据结构为例子,剖析下 SDB 是如何通过 pebble 存储引擎支持 List 的。

    List 数据结构提供了以下接口:LPush 、LPop 、LExist 、LRange 、LCount 。

    如果一个 List 的 key 为:[hello],该 List 的列表元素有:[aaa, ccc, bbb],那么该 List 的每个元素在 pebble 的存储为:

    pebble key pebble value
    l/hello/{unique_ordering_key1} aaa
    l/hello/{unique_ordering_key2} ccc
    l/hello/{unique_ordering_key3} bbb

    List 元素的 pebble key 生成策略:

    • 数据结构前缀:List 都以 l 字符为前缀,Set 是以 s 为前缀...
    • List key 部分:List 的 key 为 hello
    • unique_ordering_key:生成方式是通过雪花算法实现的,雪花算法保证局部自增
    • pebble value 部分:List 元素真正的内容,如 aaa 、ccc 、bbb

    为什么这么就能保证 List 的插入顺序呢?

    这是因为 pebble 是 LSM 的实现,内部使用 key 的字典序排序。为了保证插入顺序,SDB 在 pebble key 中增加了 unique_ordering_key 作为排序的依据,从而保证了插入顺序。

    有了 pebble key 的生成策略,一切都变得简单起来了。我们看看 LPush 、LPop 、LRange 的核心逻辑:

    LPush
    func LPush(key []byte, values [][]byte) (bool, error) {
    	batchAction := store.NewBatchAction()
    	defer batchAction.Close()
    
    	for _, value := range values {
    		batchAction.Set(generateListKey(key, util.GetOrderingKey()), value)
    	}
    
    	return batchAction.Commit()
    }
    
    LPop

    在写入到 pebble 的时候,key 的生成是通过 unique_ordering_key 的方案。 无法直接在 pebble 中找到 List 的元素在 pebble key 。在删除一个元素的时候,需要遍历 List 的所有元素,找到 value = 待删除的元素,然后进行删除。核心逻辑如下:

    func LPop(key []byte, values [][]byte) (bool, error) {
    	batchAction := store.NewBatchAction()
    	defer batchAction.Close()
    
    	store.Iterate(&store.IteratorOption{Prefix: generateListPrefixKey(key)},
    		func(key []byte, value []byte) {
    			for i := range values {
    				if bytes.Equal(values[i], value) {
    					batchAction.Del(key)
    				}
    			}
    		})
    
    	return batchAction.Commit()
    }
    
    LRange

    和删除逻辑类似,通过 iterator 接口进行遍历。 这里对反向迭代做了额外的支持 允许 Offset 传入 -1 ,代表从后进行迭代。

    func LRange(key []byte, offset int32, limit int32) ([][]byte, error) {
    	index := int32(0)
    	res := make([][]byte, limit)
    	store.Iterate(&store.IteratorOption{
    		Prefix: generateListPrefixKey(key), Offset: int(offset), Limit: int(limit)},
    		func(key []byte, value []byte) {
    			res[index] = value
    			index++
    		})
    	return res[0:index], nil
    }
    

    以上就实现了对 List 的数据结构的支持。

    其他的数据结构大体逻辑类似,其中 sorted_set 更加复杂些。可以自行查看。

    SDB 通讯协议方案

    解决完了存储和数据结构的问题后,SDB 面临了 [最后一公里] 的问题是通讯协议的选择。

    SDB 的定位是支持多语言的,所以需要选择支持多语言的通讯框架。

    grpc 是一个非常不错的选择,只需要使用 SDB proto 文件,就能通过 protoc 命令行工具自动生成各种语言的客户端,解决了需要开发不同客户端的问题。

    SDB 集群方案

    SDB 的集群方案其实是在规划中的,之前也考虑了 TiKV 集群方案和 Redis 集群方案。

    但目前 SDB 把注意力放在持久化、数据结构上。增加更多的数据结构,并将易用性做到极致。之后再实现集群方案。


    规划

    • 支持更多的存储引擎
      • LSM
      • B+ Tree
    • 支持对现有数据结构更多的操作
    • 支持更丰富的数据结构
      • geo hash
      • 倒排索引
      • 向量检索
      • 广告定向
    • 搭建 admin web ui

    感谢

    感谢开源的力量,这里就不一一列举了,请大家移步 go.mod

    19 条回复    2021-12-28 17:27:09 +08:00
    huyujievip
        1
    huyujievip  
       2021-12-10 00:44:08 +08:00 via iPhone
    第二个 star ,之前正好在看使用 redis 数据结构实现类似业务场景的专栏
    Aidenboss
        2
    Aidenboss  
    OP
       2021-12-10 01:06:33 +08:00
    @huyujievip 感谢支持 ~
    dcoder
        3
    dcoder  
       2021-12-10 02:04:06 +08:00
    会有 cluster 模式么?
    codespots
        4
    codespots  
       2021-12-10 02:49:28 +08:00
    貌似看过这篇文章,提个小建议,换个端口吧,9000 端口是 php-fpm 的端口
    wzw
        5
    wzw  
       2021-12-10 08:26:04 +08:00 via iPhone
    我一年前选了 pika
    Aidenboss
        6
    Aidenboss  
    OP
       2021-12-10 10:00:13 +08:00
    @dcoder 计划今年出一个集群的方案,到时候邀请大家一起 review review~
    Aidenboss
        7
    Aidenboss  
    OP
       2021-12-10 10:00:25 +08:00
    @codespots 好的,今晚就去改
    Aidenboss
        8
    Aidenboss  
    OP
       2021-12-10 10:00:37 +08:00
    @wzw 好选择。。。
    qq1340691923
        9
    qq1340691923  
       2021-12-10 10:45:05 +08:00
    用 golang 写数据库,请问怎么解决 stw 问题的
    Joker123456789
        10
    Joker123456789  
       2021-12-10 11:09:00 +08:00
    其实 redis 也有持久化能力的,之所以不只用 redis ,是因为 redis 的定位就是 内存数据库,他的设计初衷就是作为一个缓存而存在,并不是作为数据库的。

    而你这个项目,从使用的角度来看,跟 redis 没啥区别,我建议你 后面可以在 查询方面 下点功夫,将查询能力丰富起来,这样就可以去打 redis 了

    感觉你给自己挖了一个大坑,一上来就给这个项目定位了 redis+关系型数据库的 优点结合体,只用这一个就解决问题。

    但是关系型数据库的作用,你一开始就想错了,他并不是 redis 的补充,反而 redis 是关系型数据的补充,它弥补的是关系型数据库查询慢,并发低 的问题。

    关系型数据最大的优点就是,他一开始就是为了持久的储存数据而 开发的,并且功能丰富,操作灵活(得益于 sql ),起码就目前而言,关系型数据库是 储存数据的不二之选。


    所以,我再次建议:

    你就干脆把他当做 redis 的竞品,而不是 redis + 关系型数据的优势结合,想办法做的比 redis 更好用, 尤其是丰富查询能力。

    因为关系型数据库的优势,不可能被替代的。如果有,那肯定不是 key-value 。
    Aidenboss
        11
    Aidenboss  
    OP
       2021-12-10 12:03:26 +08:00
    @Joker123456789 说的在理。其实 SDB 的定位不是 redis + 关系型数据库优点的结合体。而是在开头讲述的那些业务问题,才是 SDB 的立身之本。

    我这边的想法也是:将易用性打造的足够好。提供更丰富的数据结构;提供更丰富的查询能力;提供 admin web ui 等等。

    总之就是:SDB 的定位不是取代,而是解决业务问题。
    Aidenboss
        12
    Aidenboss  
    OP
       2021-12-10 12:03:40 +08:00
    @qq1340691923 可以举例说明下吗?
    yrj
        13
    yrj  
       2021-12-10 12:48:20 +08:00 via iPad
    使用 pebble 作为默认,是因为他比 badger 和 leveldb 有更好的性能吗?因为现在在用 badger ,如果性能更好,我也尝试一下。
    我觉得楼上说的有道理,数据存储我还是更信任传统的 db 数据库。这种文件 kv 存储。我都是用来存储缓存之类的临时文件,所以加一个过期时间功能会更好。过期时间如果有续期模式就更好了。
    Redis 的优势就是内存操作性能强大,所以必须计数操作等不太耗费内存的,我还是会选择 Redis ,比文件存储性能更好。况且现在内存也便宜。
    所以我觉得楼主应该避其锋芒,不和 Redis 比性能,不和传统 db 比核心数据存储。主打大字段的缓存存储,围绕此丰富功能。
    Aidenboss
        14
    Aidenboss  
    OP
       2021-12-10 12:59:33 +08:00
    @yrj 先回答第一个问题:从我自己的测试结果和网友的测试结果来看,pebble 的性能更好些: https://blog.csdn.net/huxinglixing/article/details/116156322 ,这是网友的测试结果。
    我也用了 grafana 的监控,看起来确实如此。

    [主打大字段的缓存存储,围绕此丰富功能。] 我想想,感谢感谢 ~
    bruce0hh
        15
    bruce0hh  
       2021-12-10 15:27:30 +08:00
    支持,学习下~
    wzw
        16
    wzw  
       2021-12-25 21:57:13 +08:00
    @Aidenboss #8 360 的 pika 不好吗?
    wzw
        17
    wzw  
       2021-12-25 22:00:48 +08:00
    @yrj #13 badger 用起来怎么样? 我 1 年前选的时候, badger 和 pika 选了 pika, 但是 pika 不是纯 go 的
    Aidenboss
        18
    Aidenboss  
    OP
       2021-12-26 00:13:16 +08:00
    @wzw 其实看了一下它的 commit 就知道,今年的 commit 次数不超过 20 次。
    yrj
        19
    yrj  
       2021-12-28 17:27:09 +08:00 via iPad
    @wzw 目前轻度使用,没什么问题
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2532 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 10:57 · PVG 18:57 · LAX 02:57 · JFK 05:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.