持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第15天,点击查看活动详情
通过 ScyllaDB 示例了解预处理语句、分页和重试如何改善Scylladb Rust驱动的应用程序的性能。
在 ScyllaDB中,一直在努力开发和改进scylla-rust-driver。它是 Rust 的开源 ScyllaDB(和 Apache Cassandra)驱动程序,使用Tokio,用纯 Rust 编写,具有完全异步的 API 。
在不同的基准测试中,Rust 驱动程序被证明比其他驱动程序具有更高的性能,这给了我们将其也用作其他驱动程序的统一核心的想法。
在这篇文章中,你将了解预处理语句、分页和重试,并查看使用 ScyllaDB Rust 驱动程序的示例。最终目标是演示一些微小的更改如何提高应用程序的性能。
在 Docker 中启动 ScyllaDB
从 git 下载示例:
git clone https://github.com/scylladb/scylla-code-samples.git
cd scylla-code-samples/Rust_Scylla_Driver/chat/
要快速启动并运行 ScyllaDB,请使用官方 Docker 映像:
docker run \
-p 9042:9042/tcp \
--name some-scylla \
--hostname rust-scylla \
-d scylladb/scylla:4.5.0 \
--smp 1 --memory=750M --overprovisioned 1
示例应用程序
在此示例中,将创建一个控制台应用程序,该应用程序从标准输入读取消息并将它们放入 ScyllaDB 中的表中。首先,创建键空间和表:
docker exec -it some-scylla cqlsh
CREATE KEYSPACE IF NOT EXISTS log WITH REPLICATION = {
'class': 'SimpleStrategy',
'replication_factor': 1
};
CREATE TABLE IF NOT EXISTS log.messages (
id bigint,
message text,
PRIMARY KEY (id)
);
现在,看一下应用程序的主要代码(rust):
use chrono::Utc;
use scylla::{Session, SessionBuilder};
use tokio::io::{stdin, AsyncBufReadExt, BufReader};
let session: Session = SessionBuilder::new()
.known_node("127.0.0.1:9042".to_string())
.build()
.await?;
let mut lines_from_stdin = BufReader::new(stdin()).lines();
while let Some(line) = lines_from_stdin.next_line().await? {
let id: i64 = Utc::now().timestamp_millis();
session.query(
"INSERT INTO log.messages (id, message) VALUES (?, ?)",
(id, line),
).await?;
}
let rows = session
.query("SELECT id, message FROM log.messages", &[])
.await?
.rows_typed::<(i64, String)>()?;
for row in rows {
let (id, message) = row?;
println!("{}: {}", id, message);
}
应用程序连接到数据库,从控制台读取一些行,并将它们存储在 table 中log.messages。然后它从表中读取这些行并打印它们。
应用程序连接到数据库,从控制台读取一些行,并将它们存储在 table 中log.messages。然后它从表中读取这些行并打印它们。
到目前为止,这与Rust入门课程中看到的非常相似。接下来,将看到一些小的更改如何提高应用程序的性能。
预处理语句
在 while 循环的每次迭代中,我们都希望将新数据插入到log.messages表中。这样做是低效的,因为每次调用session.query都会将整个查询字符串发送到数据库,然后数据库对其进行解析。可以使用会话提前准备查询,以避免不必要的数据库端calculations.prepare方法。调用此方法将返回一个PreparedStatement对象,稍后可以使用该对象session.execute()来执行所需的查询。
什么是预处理语句?
是由 ScyllaDB 解析然后保存以供以后使用的查询。一个重要好处是,你可以继续重用相同的查询,同时修改查询中的变量以匹配名称、地址和位置等参数。
当被要求准备 CQL 语句时,客户端库将向 ScyllaDB 发送 CQL 语句。然后,ScyllaDB 将通过 MD5 散列该 CQL 语句为该语句创建一个唯一指纹。然后 ScyllaDB 使用这个哈希来检查它的查询缓存,看看它是否已经看到它。如果是这样,它将返回对该缓存 CQL 语句的引用。如果 ScyllaDB 在其缓存中没有该唯一查询哈希,它将继续解析查询并将解析后的输出插入其缓存。
然后,客户端将能够发送和执行指定语句 id(封装在PreparedStatement对象中)并提供(绑定)变量的请求,如下所示。
在应用程序中使用准备好的语句
查看上面的示例代码并修改它以使用预处理语句。第一步是session.prepare在 while 循环之前创建一个预处理语句。接下来,您需要在 while 循环内 替换session.query为。session.execute
let insert_message = session
.prepare("INSERT INTO log.messages (id, message) VALUES (?, ?)")
.await?;
let mut lines_from_stdin = BufReader::new(stdin()).lines();
while let Some(line) = lines_from_stdin.next_line().await? {
let id: i64 = Utc::now().timestamp_millis();
session.execute(&insert_message, (id, line)).await?;
}
在这两个步骤之后,应用程序将预处理语句 insert_message,而不是发送原始查询。显著提高了性能。
分页
查看应用程序的最后几行:
let rows = session
.query("SELECT id, message FROM log.messages", &[])
.await?
.rows_typed::<(i64, String)>()?;
for row in rows {
let (id, message) = row?;
println!("{}: {}", id, message);
}
调用该Session::query方法,并发送未准备好的选择查询。由于此查询只执行一次,因此不值得准备。但是,如果我们考虑到结果会很大,使用分页可能会更好。
什么是分页?
是一种以可管理的块返回大量数据的方法。在没有分页的情况下,协调节点准备一个保存所有数据的结果实体并将其返回。如果结果很大,这可能会对性能产生重大影响,因为它可能会在客户端和 ScyllaDB 服务器端占用大量内存。
为避免这种情况,请使用分页,因此结果以有限大小的块的形式传输,一次一个块。传输完每个块后,数据库停止并等待客户端请求下一个块。重复此过程,直到传输整个结果集。
客户端可以根据它可以包含的行数来限制页面的大小。如果页面在达到客户端提供的行限制之前达到大小限制,则称为短页面或短读取。
向我们的应用程序添加分页
let mut row_stream = session
.query_iter("SELECT id, message FROM log.messages", &[])
.await?
.into_typed::<(i64, String)>();
while let Some(row) = row_stream.next().await {
let (id, message) = row?;
println!("{}: {}", id, message);
}
重试
查询失败后,驱动程序可能会根据重试策略和查询本身决定重试。可以为整个 Session 或单个查询配置重试策略。
可用的重试策略
驱动程序提供两种策略可供选择:
- Fallthrough Retry Policy:从不重试并将所有错误直接返回给用户
- 默认重试策略:默认使用,如果成功率很高,可能会重试
可以通过实现 RetryPolicy 和 RetrySesssion 来提供自定义重试策略。
使用重试策略
享受重试策略带来的好处的关键是提供更多关于查询幂等性的信息。如果可以多次应用查询而不改变初始应用的结果,则该查询是幂等的。如果它不是幂等的,驱动程序将不会重试失败的查询。将查询标记为幂等预计将由用户完成,因为驱动程序不解析查询字符串。
将应用的 select 语句标记为幂等语句:
let mut select_query = Query::new("SELECT id, message FROM log.messages");
select_query.set_is_idempotent(true);
let mut row_stream = session
.query_iter(select_query, &[])
.await?
.into_typed::<(i64, String)>();
通过进行此更改,您可以在 select 语句执行错误的情况下使用重试(由默认重试策略提供)。