YCSB 是一个非常出名的性能测试框架, 我们可以非常方便的用它来对系统进行多维度的性能测试, 本来我也准备使用它来对我们系统进行性能测试的, 但在调研了一番之后, 我决定直接用 Go 来完全移植一个. 过年的时候就一直在干这件事情, 于是就有了 https://github.com/pingcap/go-ycsb .
为什么需要 YCSB?
先来说说为什么我们需要 YCSB, 对于一个系统来说, 用户在试用之前, 通常都会问你的性能怎样?, 但其实这句话是非常不好回答的. 所以通常业界都会用一些基准的性能测试工具来衡量. 另一方面, 一个系统, 性能并不是只有一个维度, 譬如 sharding 的系统可能随机 read 一个 key 非常快, 但如果是顺序 scan 一批 key 性能就可能嗝屁了. 再就是性能其实也跟数据的分布有关系, 譬如有些数据就是热点, 需要频繁操作, 而大部分数据其实是冷数据. 刚好 YCSB 都能很好的支持这些特性.
再来说说为什么需要 Go 的 YCSB, 其实无非就是两个原因:
我不会 Java. 虽然我个人对语言没啥偏爱, 譬如我就一直搞不懂为啥很多写 C++ 的人不喜欢 Rust, 但我个人对 Java 却实在提不起兴趣, 所以到了现在, 看到 Java 代码我就头大, 自然不会想着自己去写 Java 相关的代码.
现在 TiKV 只有 Go 的 API, 虽然我们 TiSpark https://github.com/pingcap/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{})
}
具体可以参考现有的一些例子 https://github.com/pingcap/go-ycsb/tree/master/db , 然后注册给 YCSB, 譬如 basic DB 就直接在 import 里面注册:
_ "github.com/pingcap/go-ycsb/db/basic"
小结
现在, 在我们项目的 issue 里面, 已经有看到有些用户将 go-ycsb 用来测试我们的系统, 而后面, 我们会用 go-ycsb 多维度的对整个系统进行测试, 作为我们后面优化的一个性能基准指标. 但是, 现在 go-ycsb 还是有很多需要完善的, 譬如在统计信息上面, 现在就比较粗糙, 而且还不能支持 export 到外面, 让外面生成图表等.
如果你对性能测试工具感兴趣, 欢迎联系我, 一起来完善, 我的邮箱 mailto:tl@pingcap.com .
来源: http://www.jianshu.com/p/73d22befe47d