YCSB 是一个非常出名的性能测试框架,我们可以非常方便的用它来对系统进行多维度的性能测试,本来我也准备使用它来对我们系统进行性能测试的,但在调研了一番之后,我决定直接用 Go 来完全移植一个。过年的时候就一直在干这件事情,于是就有了 go-ycsb。
为什么需要 YCSB?
先来说说为什么我们需要 YCSB,对于一个系统来说,用户在试用之前,通常都会问『你的性能怎样?』,但其实这句话是非常不好回答的。所以通常业界都会用一些基准的性能测试工具来衡量。另一方面,一个系统,性能并不是只有一个维度,譬如 sharding 的系统可能随机 read 一个 key 非常快,但如果是顺序 scan 一批 key 性能就可能嗝屁了。再就是性能其实也跟数据的分布有关系,譬如有些数据就是热点,需要频繁操作,而大部分数据其实是冷数据。刚好 YCSB 都能很好的支持这些特性。
再来说说为什么需要 Go 的 YCSB,其实无非就是两个原因:
- 我不会 Java。虽然我个人对语言没啥偏爱,譬如我就一直搞不懂为啥很多写 C++ 的人不喜欢 Rust,但我个人对 Java 却实在提不起兴趣,所以到了现在,看到 Java 代码我就头大,自然不会想着自己去写 Java 相关的代码。
- 现在 TiKV 只有 Go 的 API,虽然我们 TiSpark 带了一个 Java TiKV client,但不支持写。为了能让 YCSB 直接测试 TiKV,我现在必须使用 Go,但我又不知道如何 Java 调用 Go 的代码,所以还不如用 Go 重写 YCSB 来的简单,反正不复杂。
Benchmark Tiers
YCSB 主要测试两层 – 性能和可扩展性。
对于性能来说,主要关注的是 Latency,当然,Latency 和 Throughput 是需要取舍的,在固定的硬件条件下,当我们逐渐增加请求的时候,因为 disk,CPU,network 等竞争,请求的 latency 是在增加的。所以我们需要知道的是需要多少机器才能满足用户 lantency 和 throughput 的需求。当然,需要机器越少,证明我们系统优化的越好。这里,YCSB 采用的是非常常见的 Wisconsin Sizeup 方法,固定硬件,增加测试并发压力,直到系统出现瓶颈过载。
而对于可扩展性来说,一个是按比例增加,将硬件,数据量和负载等比增加,正常情况下面 latency 是保持恒定的。另一个就是弹性加速,我们测试 N 个服务,然后在测试 N + 1 个服务,正常情况下面 latency 是要降低的。
Workload
这里来说说 YCSB 的 Workload,YCSB 提供了一个 Core workload,并且默认提供了很多的 workloads。每个 Workload 提供了一批混合读写的操作,数据量的大小,请求的分布等,所以用户可以依据不同的 workload 多维度的对系统进行测试。
操作主要包括:
- Insert:插入一条新的记录
- Update:更新一条记录的某一个或者所有 fields
- Read:读取一条记录的某一个或者所有 fields
- Scan:随机从一个 key 开始顺序扫描随机条记录
Distribution
在测试的时候,我们还需要根据不同的业务场景来模拟测试,这个就是通过 Distribution 来完成的。YCSB 提供了默认的几种 distribution:
- Uniform:随机选择一个记录
- Zipfian:根据 Zipfian 分布来选择记录。一些记录会比较热,而大部分记录会比较冷。
- Latest:比较类似 Zipfian,但最近的新插入记录是在整个分布的开头。
- Multinomial:根据概率指定,譬如,我们可以指定 0.95 的的 read 操作和 0.05 的 update 操作,然后 0 给 scan 和 insert,这样就是一个 read heavy workload。
Zipfian 和 Latest 的区别在于使用 Latest,新插入的记录会变成最热的记录,而对于 Zipfian 来说,所有的记录仍然保持原来的冷热度。Latest 就比较适用于热点新闻,而对于 Zipfian,可能就比较适用于明星,对于他们的 profile,即使是几年前加入的,也会非常热。
至于 Distribution 到底是怎么实现的,可以详细参考 go-ycsb 的相关 generator 实现,后面如果有时间,也会详细的分析。
如何使用
对于 YCSB 来说,是非常容易使用的,我们只需要选择好自己的 workload,先使用 load 导入数据,然后用 run 就能跑起来了。YCSB 提供常用的几种 workload:
Workload | Operations | Record selection | Application example |
---|---|---|---|
A — Update heavy | Read: 50%, Update: 50% | Zipfian | 在用户的 session 里面 存储和访问最近的操作 |
B — Read heavy | Read: 95%, Update: 5% | Zipfian | 图片标记,打标记是 update,但多数时候是 read |
C — Read only | Read: 100% | Zipfian | 用户 profile cache |
D — Read latest | Read: 95%, Insert: 5% | Latest | 用户最近的状态更新 |
E — Short range | Scan: 95%, Insert: 5% | Zipfian / Uniform | 不同主题帖子浏览 |
这里以 Workload A 为例,我们使用 MySQL,导入相关的数据:
./bin/go-ycsb load mysql -P workload/workloada -p mysql.host=127.0.0.1 -p mysql.port=3306 -p mysql.db=test
上面,我们使用 mysql
这个 Database,指定了一个 Workload A,然后传入了 MySQL 相关的参数,先用 load
导入了 1000 行数据。
然后我们开始执行:
./bin/go-ycsb run mysql -P workload/workloada -p mysql.host=127.0.0.1 -p mysql.port=3306 -p mysql.db=test
如何测试自己的 Database
如果要在 go-ycsb 测试自己的 Database,也非常容易。只要实现 DB 和 DBCreator 的 interface 就可以了。首先,我们要实现自己的 Database,接口定义如下
type DB interface {
Close() error
InitThread(ctx context.Context, threadID int, threadCount int) context.Context
CleanupThread(ctx context.Context)
Read(ctx context.Context, table string, key string, fields []string) (map[string][]byte, error)
Scan(ctx context.Context, table string, startKey string, count int, fields []string) ([]map[string][]byte, error)
Update(ctx context.Context, table string, key string, values map[string][]byte) error
Insert(ctx context.Context, table string, key string, values map[string][]byte) error
Delete(ctx context.Context, table string, key string) error
}
对于 Read,Update, Insert,Scan,Delete 等函数,非常直观,这里不过多解释。这里需要关注 InitThread 和 CleanupThread,对于 YCSB 来说,DB 是多线程安全的,我们会启动多个 thread (Go 里面就是 goroutine)同时对该 DB 进行操作,但有些时候,我们需要每个 thread 上面都有该 DB 的 local thread 变量,所以在 thread 开始的时候,我们会调用 InitThread,而结束的时候会 CleanupThread。
以 basic 为例,
type contextKey string
const stateKey = contextKey("basicDB")
type basicState struct {
r *rand.Rand
buf *bytes.Buffer
}
func (db *basicDB) InitThread(ctx context.Context, _ int, _ int) context.Context {
state := new(basicState)
state.r = rand.New(rand.NewSource(time.Now().UnixNano()))
state.buf = new(bytes.Buffer)
return context.WithValue(ctx, stateKey, state)
}
func (db *basicDB) Read(ctx context.Context, table string, key string, fields []string) (map[string][]byte, error) {
state := ctx.Value(stateKey).(*basicState)
...
}
它对于每个 thread,创建了一个 basicState,并且挂在到 context 上面,这样,后面我们就可以通过 context Value 得到这个 basicState 了。为什么要做这个事情了,当初设计的时候主要是为了避免全局使用 Go 的 rand 函数,我们这边已经无数次碰到全局使用 rand 造成的性能问题了,毕竟里面有 Mutex,所以最好就是每个 thread 单独的 rand。
然后我们要实现一个 DBCreator,接口定义如下:
type DBCreator interface {
Create(p *properties.Properties) (DB, error)
}
这个接口很简单,就是根据当前的配置,创建对应的 DB,然后我们需要将自己的 Creator 注册给 YCSB,并制定一个唯一的名字,譬如对于 basic 这个 Database,我们在 init 函数里面直接全局注册:
func init() {
ycsb.RegisterDBCreator("basic", basicDBCreator{})
}
具体可以参考现有的一些例子,然后注册给 YCSB,譬如 basic DB 就直接在 import 里面注册:
_ "github.com/pingcap/go-ycsb/db/basic"
小结
现在,在我们项目的 issue 里面,已经有看到有些用户将 go-ycsb 用来测试我们的系统,而后面,我们会用 go-ycsb 多维度的对整个系统进行测试,作为我们后面优化的一个性能基准指标。但是,现在 go-ycsb 还是有很多需要完善的,譬如在统计信息上面,现在就比较粗糙,而且还不能支持 export 到外面,让外面生成图表等。
如果你对性能测试工具感兴趣,欢迎联系我,一起来完善,我的邮箱 tl@pingcap.com。