通过一个ScyllaDB的例子,了解准备好的语句、分页和重试是如何提高使用ScyllaDB Rust驱动的应用程序的性能。 在ScyllaDB,我们一直在努力开发和改进scylla-rust-driver。这是一个开源的ScyllaDB(和Apache Cassandra)的Rust驱动,用纯Rust编写,使用Tokio的完全异步API。你可以阅读更多关于它的基准测试结果以及我们的开发人员如何解决性能退步的问题。在不同的基准测试中,Rust驱动被证明比其他驱动更有性能,这让我们有了将其作为其他驱动的统一核心的想法。这篇博文是基于ScyllaDB大学的Rust课程。
在这篇文章中,我将介绍该课程的基本内容。你将了解到 预备语句、分页和重试,并看到一个使用ScyllaDB Rust驱动的例子。最终的目标是展示一些小的变化如何能显著提高应用程序的性能。
在Docker中启动ScyllaDB从git上下载这个例子。
git clone https://github.com/scylladb/scylla-code-samples.gitcd scylla-code-samples/Rust_Scylla_Driver/chat/为了快速启动和运行ScyllaDB,使用官方Docker镜像。
docker run \ -p 9042:9042/tcp \
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)
);现在,看看应用程序的主代码。
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);}
该应用程序连接到数据库,从控制台读取一些行,并将它们存储在表log.messages中。然后它从表中读取这些行并打印出来。
到目前为止,这与你在《Rust入门》一课中看到的很相似。使用这个应用程序,你将看到一些微小的变化是如何提高应用程序的性能的。
预备语句
在while循环的每一次迭代中,我们要向log.messages表插入新的数据。这样做的效率很低,因为每次调用session.query都会把整个查询字符串发送到数据库,然后再进行解析。人们可以使用session提前准备一个查询,以避免不必要的数据库边的计算.prepare方法。对这个方法的调用将返回一个PreparedStatement对象,该对象可以在以后使用session.execute()来执行所需的查询。
什么是准备好的语句?
一个准备好的语句是由ScyllaDB解析的查询,然后保存起来供以后使用。使用准备好的语句的一个有价值的好处是,你可以继续重复使用同一个查询,同时修改查询中的变量以匹配参数,如姓名、地址和位置。
当被要求准备一个CQL语句时,客户端库将发送一个CQL语句到ScyllaDB。然后,ScyllaDB将通过MD5散列为该CQL语句创建一个独特的认证标识。然后,ScyllaDB使用这个哈希值来检查它的查询缓存,看看它是否已经看到它。如果是这样,它将返回一个对该缓存的CQL语句的引用。如果ScyllaDB在其缓存中没有那个唯一的查询哈希值,那么它将继续解析该查询并将解析后的输出插入其缓存中。
然后,客户端将能够发送和执行一个请求,指定语句ID(它被封装在PreparedStatement对象中)并提供(绑定的)变量,正如你接下来要看到的。
在应用程序中使用预制语句
查看上面的示例代码,并修改它以使用准备好的语句。第一步是在while循环之前创建一个准备好的语句(在session.prepare的帮助下)。接下来,你需要在while循环中用session.execute替换session.query。
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方法的调用,并且发送了一个未准备的select查询。因为这个查询只执行一次,所以不值得准备。然而,如果我们怀疑结果会很大,使用分页可能会更好。
什么是分页?
分页是一种在可管理的块中返回大量数据的方法。在没有分页的情况下,协调者节点会准备一个持有所有数据的单一结果实体,并将其返回。在大型结果的情况下,这可能会对性能产生重大影响,因为它可能会占用大量的内存,在客户端和ScyllaDB服务器侧。
为了避免这种情况,可以使用分页,所以结果是以有限大小的块来传输,一次一个块。在传输完每一块后,数据库会停止,等待客户端请求下一块。这样反复进行,直到整个结果集被传输。
客户端可以根据它能包含的行数来限制页面的大小。如果一个页面在达到客户端提供的行数限制之前就达到了大小限制,那么它就被称为短页或短读。
在我们的应用程序中添加分页
正如你现在可能已经猜到的,Session::query不使用分页。它一次就把整个结果取到内存中。一个替代的Session方法在外壳下使用分页--Session::query_iter(Session::execute_iter是另一个替代方法,可用于准备好的语句)。Session::query_iter方法将一个查询和一个值列表作为参数,并在结果Rows上返回一个异步迭代器(流)。这是它的使用方法。
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); }
在query_iter调用之后,驱动启动了一个后台任务来获取后续的行。调用者任务(调用query_iter的任务)通过使用一个类似迭代器的流接口来消耗新获取的行。调用者和后台任务同时运行,所以其中一个任务可以在另一个任务消耗行的时候获取新行。
通过在应用程序中添加分页,你可以减少内存的使用并提高应用程序的性能。
重试
在一个查询失败后,驱动程序可能会根据重试策略和查询本身决定重试。重试策略可以为整个会话或单个查询进行配置。
可用的重试策略
驱动程序提供了两种策略供用户选择。
突破性重试策略。从不重试,直接向用户返回所有错误。
默认重试策略。默认使用,如果有很大的成功机会,可能会重试。
通过实现RetryPolicy和RetrySesssion,可以提供一个自定义的重试策略。
使用重试策略
享受重试策略的好处的关键是提供更多关于查询空闲性的信息。如果一个查询可以被多次应用而不改变最初应用的结果,那么它就是空闲的。如果一个失败的查询不是同位素的,驱动程序将不会重试。将查询标记为idempotent预计将由用户完成,因为驱动程序不会解析查询字符串。
将应用程序的select语句标记为idempotent的语句。
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)>();
通过这种改变,你可以在选择语句执行错误的情况下使用重试(由默认重试策略提供)。
继续学习
要运行该应用程序并看到结果,请查看ScyllaDB大学的完整课程。另外,Rust Driver docs页面包含一个快速入门指南,供想使用ScyllaDB驱动的人使用。
原文标题:Rust and ScyllaDB NoSQL: 3 Ways to Improve Performance
原文作者:Guy Shtub
原文地址:https://dzone.com/articles/rust-and-scylladb-3-ways-to-improve-performance




