Apache Cassandra 轻量级基准测试工具
针对 Cassandra 集群运行自定义 CQL 工作负载,并测量吞吐量与响应时间

为什么又有一个基准测试程序?
- Latte 在 Apache Cassandra 的基准测试工具中性能远超其他工具。参见 benchmarks.
- Latte 旨在提供定义工作负载的最灵活方式。
性能
与 NoSQLBench、 Cassandra Stress 和 tlp-stress 不同, Latte 使用 Rust 编写,并采用来自 Scylla 的原生 Cassandra 驱动程序。 它具备完全异步的每核一线程执行引擎, 能够从单个线程每秒处理数千个请求。
Latte 具有以下独特的性能特征:
- 在多核机器上具有出色的可扩展性。
- CPU 效率比 NoSQLBench 高约 10 倍。 这意味着你可以用少量客户端测试大型集群。
- 内存占用比基于 Java 的工具低约 50 倍至 100 倍。
- 对操作系统资源的影响极小——系统调用、上下文切换和页面错误数量少。
- 无需客户端代码预热。客户端代码从第一个基准测试周期起即以最大性能运行。 即使仅运行 30 秒也能获得准确结果。
- 测试过程中不会发生 GC 暂停或 HotSpot 重新编译。你希望测量的是服务器的卡顿, 而不是基准测试工具。
卓越的性能使其成为探索性基准测试的理想工具,当你想快速尝试 不同的工作负载时。
灵活性
其他基准测试工具通常使用配置文件来指定工作负载配方。 虽然这使得定义简单的工作负载变得容易,但当你想要 编写更真实的场景,这些场景需要发出多个 查询,或者需要以不同于工具内置方式生成数据时,它很快就会变得繁琐。
Latte 没有试图将流行的配置文件格式扭曲为图灵完备的脚本语言,而是简单地 嵌入了一种真正的、功能齐全的、图灵完备的现代脚本语言。我们选择了 Rune ,因为它与 Rust 的集成无痛、一流的异步支持、令人满意的性能以及来自其 维护者的出色支持。
Rune 提供了与 Rust 类似的语法和功能,尽管它具有动态类型和简单的自动内存管理。因此, 你不仅可以发出自定义 CQL 查询,还可以编程 任何你希望的东西。这里有变量、条件语句、循环、模式匹配、函数、lambda、 用户定义的数据结构、对象、枚举、常量和宏等等。
特性
- 兼容 Apache Cassandra 3.x、4.x、DataStax Enterprise 6.x 和 ScyllaDB
- 使用强大的脚本引擎实现自定义工作负载
- 异步查询
- 预编译查询
- 可编程数据生成
- 工作负载参数化
- 带误差范围的吞吐量和响应时间精确测量
- 无协调遗漏
- 可配置的连接数和线程数
- 速率和并发限制器
- 进度条
- 美观的文本报告
- 可将报告导出为 JSON
- 两次运行的并排比较
- 针对自相关进行校正的差异统计显著性分析
局限性
Latte 仍处于早期阶段,正在积极开发中。
- 查询结果集尚未公开。
- 数据生成函数集非常小,并将在近期扩展。
- 向后兼容性可能会频繁被破坏。
安装
从 deb 包
dpkg -i latte-<version>.deb
从源
- Install Rust toolchain
- Run
cargo install latte-cli
用法
在某个位置启动一个 Cassandra 集群(可以是本地节点)。然后运行:
latte schema <workload.rn> [<node address>] # create the database schema
latte load <workload.rn> [<node address>] # populate the database with data
latte run <workload.rn> [-f <function>] [<node address>] # execute the workload and measure the performance
您可以在 workloads 文件夹中找到一些示例工作负载文件。
为了方便,您可以将工作负载文件放置在 /usr/share/latte/workloads 或 .local/share/latte/workloads 下,
这样 latte 无论当前工作目录如何都能找到它们。您还可以通过设置 LATTE_WORKLOAD_PATH 环境变量来配置自定义的工作负载位置。
Latte 在标准输出上生成文本报告,但也会将所有数据保存到工作目录中的一个 json 文件。该文件的 名称是根据运行参数和时间戳自动生成的。
您可以使用 latte show 显示之前运行的结果:
latte show <report.json>
latte show <report.json> -b <previous report.json> # to compare against baseline performance
运行 latte --help 以显示可用选项的帮助。
Workloads
Latte 的 Workloads 可通过嵌入式脚本语言 Rune 完全自定义。
Workload 脚本定义了一组 Latte 会自动调用的公共函数。一个最小可行 workload 脚本
必须至少定义一个带有两个参数的公共 async 函数 run:
ctx– 提供对 Cassandra 访问的会话上下文i– 当前唯一的 64 位整数类型循环编号,从 0 开始
以下脚本将对 system.local 表进行查询基准测试:
pub async fn run(ctx, i) {
ctx.execute("SELECT cluster_name FROM system.local LIMIT 1").await
}
ctx 上的实例函数是异步的,因此您应该对它们调用 await。
工作负载脚本可以为运行基准测试提供多个函数。
在这种情况下,您可以随意命名这些函数,然后通过 -f / --function 参数选择其中一个。
Schema creation
您可以在 schema 函数中(重新)创建基准测试所需的键空间和表。
如果存在旧模式,schema 函数还应将其删除。
通过运行 latte schema 命令执行 schema 函数。
pub async fn schema(ctx) {
ctx.execute("CREATE KEYSPACE IF NOT EXISTS test \
WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }").await?;
ctx.execute("DROP TABLE IF NOT EXISTS test.test").await?;
ctx.execute("CREATE TABLE test.test(id bigint, data varchar)").await?;
}
预编译语句
调用 ctx.execute 并非最优选择,因为它没有使用预编译语句。你可以在 prepare 函数中准备语句,
并将其注册到上下文对象上。
它们将在 加载步骤 期间,在实际填充数据库之前执行,
以及在运行步骤期间,在执行运行函数之前执行。
实现和使用 prepare 的示例:
const INSERT = "my_insert";
const SELECT = "my_select";
pub async fn prepare(ctx) {
ctx.prepare(INSERT, "INSERT INTO test.test(id, data) VALUES (?, ?)").await?;
ctx.prepare(SELECT, "SELECT * FROM test.test WHERE id = ?").await?;
}
pub async fn run(ctx, i) {
ctx.execute_prepared(SELECT, [i]).await
}
查询参数也可以按名称进行绑定和传递:
const INSERT = "my_insert";
pub async fn prepare(ctx) {
ctx.prepare(INSERT, "INSERT INTO test.test(id, data) VALUES (:id, :data)").await?;
}
pub async fn run(ctx, i) {
ctx.execute_prepared(INSERT, # { id: 5, data: "foo" }).await
}
填充数据库
当查询返回非空结果集时,读查询会更有意义。
为了能够使用 latte load 向表中加载数据,你需要在上下文对象上设置加载循环次数,
并定义 load 函数:
pub async fn prepare(ctx) {
ctx.load_cycle_count = 1000000;
}
pub async fn load(ctx, i) {
ctx.execute_prepared(INSERT, [i, "Lorem ipsum dolor sit amet"]).await
}
我们还建议定义 erase 函数,在加载之前擦除数据,以便无论数据库之前存在什么数据,您始终都能获得相同的
数据集:
pub async fn erase(ctx) {
ctx.execute("TRUNCATE TABLE test.test").await
}
生成数据
Latte 附带了一个数据生成函数库。它们可以在 latte crate 中访问。通常,这些
函数接受一个整数 i 周期编号,以便您可以生成一致的数值。数据生成函数
是纯函数,即使用相同参数多次调用它们总是会产生相同的结果。
latte::uuid(i)– 生成一个随机(类型 4)UUIDlatte::hash(i)– 生成一个非负整数哈希值latte::hash2(a, b)– 生成两个整数的非负整数哈希值latte::hash_range(i, max)– 生成范围0..max内的整数值latte::hash_select(i, vector)– 基于哈希从向量中选择一个元素latte::blob(i, len)– 生成长度为len的随机二进制数据块latte::normal(i, mean, std_dev)– 从正态分布生成一个浮点数latte::normal_vec(i, length, mean, std_dev)– 从正态分布生成一个浮点数向量latte::uniform(i, min, max)– 从均匀分布生成一个浮点数latte::uniform_vec(i, length, min, max)– 从均匀分布生成一个浮点数向量latte::text(i, length)– 生成一个随机字符串latte::vector(length, function)– 使用一个接受整数元素索引并生成元素值的函数, 生成指定长度的向量latte::join(vector, separator)– 使用分隔符连接字符串集合x.clamp(min, max)– 将整数或浮点值的范围限制在给定范围内
类型转换
Rune 使用 64 位表示整数和浮点数。
从 0.28 版本开始,Rune 数字会自动转换为正确的目标查询参数类型,
因此你不需要进行显式转换。例如,你可以将一个整数作为 Cassandra 类型 smallint 的参数。
如果数字过大,超出了目标类型允许的范围,则会发出运行时错误。
以下方法可用:
x as i64– 将任何数字转换为整数x as f64– 将任何数字转换为浮点数x.parse::<i64>()– 将字符串解析为整数x.parse::<f64>()– 将字符串解析为浮点数x.to_string()– 将浮点数或整数转换为字符串
文本资源
文本数据可以使用 fs 模块中的函数从文件或资源中加载:
fs::read_to_string(file_path)– 以字符串形式返回文件内容fs::read_lines(file_path)– 将文件行读取到字符串向量中fs::read_words(file_path)– 将文件单词(按非字母字符分割)读取到字符串向量中fs::read_resource_to_string(resource_name)– 以字符串形式返回内置资源内容fs::read_resource_lines(resource_name)– 以字符串向量形式返回内置资源行fs::read_resource_words(resource_name)– 以字符串向量形式返回内置资源单词
资源嵌入在程序二进制文件中。你可以在
源代码树的 resources 文件夹下找到它们。
为了减少内存分配的成本,最好在 prepare 函数中仅加载一次资源,
并将它们存储在上下文的 data 字段中,以便在 load 和 run 中将来使用:
pub async fn prepare(ctx) {
ctx.data.last_names = fs::read_lines("lastnames.txt")?;
// ... prepare queries
}
pub async fn run(ctx, i) {
let random_last_name = latte::hash_select(i, ctx.data.last_names);
// ... use random_last_name in queries
}
参数化工作负载
工作负载可以通过命令行调用中提供的参数进行参数化。
使用 latte::param!(param_name, default_value) 宏从命令行参数初始化脚本常量:
const ROW_COUNT = latte::param!("row_count", 1000000);
pub async fn prepare(ctx) {
ctx.load_cycle_count = ROW_COUNT;
}
然后你可以使用 -P 来设置该参数:
latte run <workload> -P row_count=200
混合工作负载
可以同时运行多个工作负载函数。
您可以使用 -f / --function 指定多个函数,并可选地为
每个函数指定权重,该权重将决定该函数应被调用的频率。
如果未指定,默认权重为 1。权重之和不必为 1。
假设工作负载定义文件包含函数 read 和 write,则以下
latte 调用将运行 80% 写入和 20% 读取的混合负载:
latte run <workload> -f read:0.2 -f write:0.8
错误处理
执行工作负载脚本期间发生的错误分为三类:
- 编译错误 – 在脚本加载时检测到的错误;例如语法错误或引用未定义的 变量。这些错误会立即发出信号,并在连接到数据库之前终止基准测试。
- 运行时错误 / 恐慌 – 例如除以零或数组越界访问。它们会立即终止基准测试 。
- 错误返回值 – 例如当查询执行返回错误结果时。这些仅在从函数实际
返回时生效(使用
?将它们沿调用链向上传播)。除 Cassandra 过载 错误外,所有错误都会立即终止 基准测试。在主运行阶段发生的过载错误(例如超时)会被计数并 在基准测试报告中报告。
其他函数
ctx.elapsed_secs()– 返回自工作负载启动以来经过的秒数,类型为浮点数