这是 ClickHouse 的官方 Rust 客户端,最初由 Paul Loyd 开发。客户端源代码可在 GitHub 仓库 中获取。
概述
- 使用
serde对行进行编码和解码。 - 支持
serde属性:skip_serializing、skip_deserializing、rename。 - 通过 HTTP 传输使用
RowBinary格式。- 计划切换为通过 TCP 使用
Native格式。
- 计划切换为通过 TCP 使用
- 支持 TLS (通过
native-tls和rustls-tls功能特性) 。 - 支持压缩和解压缩 (LZ4) 。
- 提供用于查询或插入数据、执行 DDL 语句以及进行客户端批处理的 API。
- 为单元测试提供便捷的 mock。
安装
要使用该 crate,请将以下内容添加到 Cargo.toml:
[dependencies]
clickhouse = "0.12.2"
[dev-dependencies]
clickhouse = { version = "0.12.2", features = ["test-util"] }另请参阅:crates.io 页面.
Cargo 特性
lz4(默认启用) — 启用Compression::Lz4和Compression::Lz4Hc(_)Variant。启用后,除WATCH外,所有查询默认都使用Compression::Lz4。native-tls— 通过hyper-tls支持HTTPSschema 的 URL,并链接 OpenSSL。rustls-tls— 通过hyper-rustls支持HTTPSschema 的 URL,且不链接 OpenSSL。inserter— 启用client.inserter()。test-util— 添加 mock。参见示例。仅在dev-dependencies中使用。watch— 启用client.watch功能。详见对应章节。uuid— 添加serde::uuid,以便配合 uuid crate 使用。time— 添加serde::time,以便配合 time crate 使用。
ClickHouse 版本兼容性
该客户端兼容 ClickHouse 长期支持版及更高版本,也兼容 ClickHouse Cloud。
早于 v22.6 的 ClickHouse server 在某些极少数情况下会错误处理 RowBinary。
你可以使用 v0.11+ 并启用 wa-37420 功能来解决此问题。注意:不要将此功能用于较新的 ClickHouse 版本。
示例
我们希望通过客户端代码仓库中的 示例 覆盖客户端使用的各种场景。概览可参见 示例 README。
如果示例或下文档中有任何不清楚或缺失的内容,欢迎随时联系我们。
用法
创建客户端实例
use clickhouse::Client;
let client = Client::default()
// should include both protocol and port
.with_url("http://localhost:8123")
.with_user("name")
.with_password("123")
.with_database("test");HTTPS 或 ClickHouse Cloud 连接
HTTPS 可与 rustls-tls 或 native-tls cargo feature 搭配使用。
然后,像平常一样创建 client。在此示例中,使用环境变量存储连接信息:
fn read_env_var(key: &str) -> String {
env::var(key).unwrap_or_else(|_| panic!("{key} env variable should be set"))
}
let client = Client::default()
.with_url(read_env_var("CLICKHOUSE_URL"))
.with_user(read_env_var("CLICKHOUSE_USER"))
.with_password(read_env_var("CLICKHOUSE_PASSWORD"));另请参阅:
- 客户端仓库中的 ClickHouse Cloud HTTPS 示例。这也同样适用于本地部署环境中的 HTTPS 连接。
查询行
use serde::Deserialize;
use clickhouse::Row;
use clickhouse::sql::Identifier;
#[derive(Row, Deserialize)]
struct MyRow<'a> {
no: u32,
name: &'a str,
}
let table_name = "some";
let mut cursor = client
.query("SELECT ?fields FROM ? WHERE no BETWEEN ? AND ?")
.bind(Identifier(table_name))
.bind(500)
.bind(504)
.fetch::<MyRow<'_>>()?;
while let Some(row) = cursor.next().await? { .. }- 占位符
?fields会被替换为no, name(Row的字段) 。 - 占位符
?会被替换为后续bind()调用中传入的值。 - 可以使用便捷的
fetch_one::<Row>()和fetch_all::<Row>()方法,分别获取第一行或全部行。 - 可以使用
sql::Identifier来绑定表名。
注意:由于整个响应是以流式方式返回的,游标即使在已经返回了一些行之后,也仍然可能报错。如果你的用例中遇到这种情况,可以尝试使用 query(...).with_option("wait_end_of_query", "1") 在服务端启用响应缓冲。更多详情。buffer_size 选项也可能有帮助。
插入数据行
use serde::Serialize;
use clickhouse::Row;
#[derive(Row, Serialize)]
struct MyRow {
no: u32,
name: String,
}
let mut insert = client.insert("some")?;
insert.write(&MyRow { no: 0, name: "foo".into() }).await?;
insert.write(&MyRow { no: 1, name: "bar".into() }).await?;
insert.end().await?;- 如果未调用
end(),INSERT会被中止。 - 行会以 stream 的形式逐步发送,以分散网络负载。
- 只有当所有行都位于同一分区,且行数小于
max_insert_block_size时,ClickHouse 才会以原子方式插入批次。
异步插入 (服务端批处理)
你可以使用 ClickHouse 异步插入 来避免在客户端对传入数据进行批处理。只需将 async_insert 选项传给 insert 方法 (甚至可以直接设置在 Client 实例上,这样会影响所有 insert 调用) 即可。
let client = Client::default()
.with_url("http://localhost:8123")
.with_option("async_insert", "1")
.with_option("wait_for_async_insert", "0");另请参阅:
- async insert 示例 (位于客户端仓库中) 。
Inserter 功能 (客户端批处理)
需要启用 inserter Cargo 功能。
let mut inserter = client.inserter("some")?
.with_timeouts(Some(Duration::from_secs(5)), Some(Duration::from_secs(20)))
.with_max_bytes(50_000_000)
.with_max_rows(750_000)
.with_period(Some(Duration::from_secs(15)));
inserter.write(&MyRow { no: 0, name: "foo".into() })?;
inserter.write(&MyRow { no: 1, name: "bar".into() })?;
let stats = inserter.commit().await?;
if stats.rows > 0 {
println!(
"{} bytes, {} rows, {} transactions have been inserted",
stats.bytes, stats.rows, stats.transactions,
);
}
// don't forget to finalize the inserter during the application shutdown
// and commit the remaining rows. `.end()` will provide stats as well.
inserter.end().await?;- 如果达到任一阈值 (
max_bytes、max_rows、period) ,Inserter会在commit()中结束当前进行中的插入。 - 可以使用
with_period_bias为结束活动INSERT之间的时间间隔引入偏移,从而避免并行 inserter 带来的负载尖峰。 Inserter::time_left()可用于判断当前周期何时结束。如果你的 stream 很少产生条目,请再次调用Inserter::commit()以检查限制条件。- 时间阈值基于 quanta crate 实现,以提升
inserter的性能。如果启用了test-util,则不会使用它 (因此,在自定义测试中可通过tokio::time::advance()控制时间) 。 - 两次
commit()调用之间的所有行都会插入到同一条INSERT语句中。
执行 DDL 语句
对于单节点部署,像这样执行 DDL 语句即可:
client.query("DROP TABLE IF EXISTS some").execute().await?;不过,对于带有负载均衡器的集群部署或 ClickHouse Cloud,建议使用 wait_end_of_query 选项,等待 DDL 在所有副本上全部生效。可以这样操作:
client
.query("DROP TABLE IF EXISTS some")
.with_option("wait_end_of_query", "1")
.execute()
.await?;ClickHouse 设置
你可以使用 with_option 方法应用各种 ClickHouse 设置。例如:
let numbers = client
.query("SELECT number FROM system.numbers")
// This setting will be applied to this particular query only;
// it will override the global client setting.
.with_option("limit", "3")
.fetch_all::<u64>()
.await?;除 query 外,这种方式同样适用于 insert 和 inserter 方法;此外,也可以在 Client 实例上调用同一方法,为所有查询设置全局参数。
Query ID
使用 .with_option 可以设置 query_id 选项,以便在 ClickHouse 查询日志中识别查询。
let numbers = client
.query("SELECT number FROM system.numbers LIMIT 1")
.with_option("query_id", "some-query-id")
.fetch_all::<u64>()
.await?;除了 query 之外,它与 insert 和 inserter 方法的用法类似。
另请参阅:客户端仓库中的 query_id 示例。
会话 ID
与 query_id 类似,您也可以设置 session_id,让这些语句在同一会话中执行。session_id 既可以在客户端级别进行全局设置,也可以针对每次 query、insert 或 inserter 调用单独设置。
let client = Client::default()
.with_url("http://localhost:8123")
.with_option("session_id", "my-session");另请参见:客户端仓库中的 session_id 示例。
自定义 HTTP 请求头
如果你使用代理进行身份验证,或者需要传递自定义请求头,可以按如下方式操作:
let client = Client::default()
.with_url("http://localhost:8123")
.with_header("X-My-Header", "hello");另请参见:客户端仓库中的自定义 HTTP 请求头示例。
自定义 HTTP 客户端
这有助于调整底层 HTTP 连接池的设置。
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client as HyperClient;
use hyper_util::rt::TokioExecutor;
let connector = HttpConnector::new(); // or HttpsConnectorBuilder
let hyper_client = HyperClient::builder(TokioExecutor::new())
// For how long keep a particular idle socket alive on the client side (in milliseconds).
// It is supposed to be a fair bit less that the ClickHouse server KeepAlive timeout,
// which was by default 3 seconds for pre-23.11 versions, and 10 seconds after that.
.pool_idle_timeout(Duration::from_millis(2_500))
// Sets the maximum idle Keep-Alive connections allowed in the pool.
.pool_max_idle_per_host(4)
.build(connector);
let client = Client::with_http_client(hyper_client).with_url("http://localhost:8123");另请参阅客户端仓库中的自定义 HTTP 客户端示例。
数据类型
(U)Int(8|16|32|64|128)可与对应的(u|i)(8|16|32|64|128)类型或基于它们的 newtype 相互映射。Int256和UInt256目前不支持直接映射,但可以使用变通方案。Float(32|64)可与对应的f(32|64)或基于它们的 newtype 相互映射。Decimal(32|64|128)可与对应的i(32|64|128)或基于它们的 newtype 相互映射。使用fixnum或其他有符号定点数实现会更方便。Boolean可与bool或基于它的 newtype 相互映射。String可与任意字符串或字节类型相互映射,例如&str、&[u8]、String、Vec<u8>或SmartString。也支持 newtype。若要存储字节,建议使用serde_bytes,因为效率更高。
#[derive(Row, Debug, Serialize, Deserialize)]
struct MyRow<'a> {
str: &'a str,
string: String,
#[serde(with = "serde_bytes")]
bytes: Vec<u8>,
#[serde(with = "serde_bytes")]
byte_slice: &'a [u8],
}- 支持将
FixedString(N)作为字节数组使用,例如[u8; N]。
#[derive(Row, Debug, Serialize, Deserialize)]
struct MyRow {
fixed_str: [u8; 16], // FixedString(16)
}- 可通过
serde_repr支持Enum(8|16)。
use serde_repr::{Deserialize_repr, Serialize_repr};
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
level: Level,
}
#[derive(Debug, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
enum Level {
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
}UUID通过serde::uuid与uuid::Uuid相互映射。需要启用uuidfeature。
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
#[serde(with = "clickhouse::serde::uuid")]
uuid: uuid::Uuid,
}IPv6可与std::net::Ipv6Addr相互映射。IPv4可通过serde::ipv4与std::net::Ipv4Addr相互映射。
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
#[serde(with = "clickhouse::serde::ipv4")]
ipv4: std::net::Ipv4Addr,
}Date可映射为/从u16或其外层包装的 newtype,并表示自1970-01-01起经过的天数。此外,还支持time::Date,可通过使用serde::time::date实现,但这需要启用timefeature。
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
days: u16,
#[serde(with = "clickhouse::serde::time::date")]
date: Date,
}Date32可映射为/自i32或其外层的newtype,表示自1970-01-01起经过的天数。此外,还支持通过serde::time::date32使用time::Date,这需要启用timefeature。
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
days: i32,
#[serde(with = "clickhouse::serde::time::date32")]
date: Date,
}DateTime可映射为/自u32或其外层封装的 newtype,表示自 UNIX 纪元以来经过的秒数。此外,还支持time::OffsetDateTime,可通过serde::time::datetime使用,但这需要启用timefeature。
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
ts: u32,
#[serde(with = "clickhouse::serde::time::datetime")]
dt: OffsetDateTime,
}DateTime64(_)可与i32或其外层包装的newtype相互映射,表示自 Unix epoch 起经过的时间。此外,还支持通过serde::time::datetime64::*使用time::OffsetDateTime,这需要启用timefeature。
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
ts: i64, // elapsed s/us/ms/ns depending on `DateTime64(X)`
#[serde(with = "clickhouse::serde::time::datetime64::secs")]
dt64s: OffsetDateTime, // `DateTime64(0)`
#[serde(with = "clickhouse::serde::time::datetime64::millis")]
dt64ms: OffsetDateTime, // `DateTime64(3)`
#[serde(with = "clickhouse::serde::time::datetime64::micros")]
dt64us: OffsetDateTime, // `DateTime64(6)`
#[serde(with = "clickhouse::serde::time::datetime64::nanos")]
dt64ns: OffsetDateTime, // `DateTime64(9)`
}Tuple(A, B, ...)可映射为/从(A, B, ...),或映射为/从基于它的newtype。Array(_)可映射为/从任意 slice,例如Vec<_>、&[_]。也支持自定义新类型。Map(K, V)的行为类似于Array((K, V))。LowCardinality(_)可无缝支持。Nullable(_)可映射为/从Option<_>。对于clickhouse::serde::*helpers,请添加::option。
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
#[serde(with = "clickhouse::serde::ipv4::option")]
ipv4_opt: Option<Ipv4Addr>,
}- 可通过提供多个重命名后的数组来支持
Nested类型。
// CREATE TABLE test(items Nested(name String, count UInt32))
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
#[serde(rename = "items.name")]
items_name: Vec<String>,
#[serde(rename = "items.count")]
items_count: Vec<u32>,
}- 支持
Geo类型。Point的行为类似于Tuple(f64, f64),其余类型都只是由点组成的切片。
type Point = (f64, f64);
type Ring = Vec<Point>;
type Polygon = Vec<Ring>;
type MultiPolygon = Vec<Polygon>;
type LineString = Vec<Point>;
type MultiLineString = Vec<LineString>;
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
point: Point,
ring: Ring,
polygon: Polygon,
multi_polygon: MultiPolygon,
line_string: LineString,
multi_line_string: MultiLineString,
}Variant、Dynamic、 (新的)JSON数据类型暂不支持。
模拟
该 crate 提供了一些工具,用于模拟 CH 服务器,以及测试 DDL、SELECT、INSERT 和 WATCH 查询。可通过 test-util feature 启用此功能。仅应将其用作开发依赖项。
参见该示例。
故障排查
CANNOT_READ_ALL_DATA
CANNOT_READ_ALL_DATA 错误最常见的原因是,应用程序端的行定义与 ClickHouse 中的定义不一致。
考虑以下表:
CREATE OR REPLACE TABLE event_log (id UInt32)
ENGINE = MergeTree
ORDER BY timestamp然后,如果应用端定义的 EventLog 类型不匹配,例如:
#[derive(Debug, Serialize, Deserialize, Row)]
struct EventLog {
id: String, // <- should be u32 instead!
}插入数据时,可能会出现以下错误:
Error: BadResponse("Code: 33. DB::Exception: Cannot read all data. Bytes read: 5. Bytes expected: 23.: (at row 1)\n: While executing BinaryRowInputFormat. (CANNOT_READ_ALL_DATA)")在此示例中,可通过正确定义 EventLog struct 来修复此问题:
#[derive(Debug, Serialize, Deserialize, Row)]
struct EventLog {
id: u32
}已知限制
- 目前尚不支持
Variant、Dynamic和 (新的)JSON数据类型。 - 目前尚不支持服务器端参数绑定;跟踪进展请参见此 issue。
联系我们
如果你有任何问题或需要帮助,欢迎通过 Community Slack 或 GitHub issues 与我们联系。