原文:How We Built Postgres Compatibility in Rust: pgwire and DataFusion
背景:Greptime
中文名是格睿科技,其实应该反过介绍,因为这是一家中国公司,自研数据库开发,用于解决可观测性特点等等,官网
本文的作者正是这家公司的CTO
让我们回到本文的重点,他的这篇博客
引入:两种模式
Pg很火,rust也很火,那么如何结合二者?作者认为主要有两种方式:
- 自底向上的,主要用
pgrx库以插件的形式把rust生态引入PostgreSQL,典型案例比如ParadaDB - 自顶向下的,通过模拟协议和接口实现另类的“Postgres”,作者的Greptime DB就属于此类
接口:Postgres Protocol
狭义上,Postgres Protocol指的是基于TCP协议的和pg server端通信用的应用层协议,这也是事实上连接到数据库时在TCP层发生的事
总体而言,协议主要包含以下五部分:
- Startup: 在连接建立时,握手交换信息和各种身份认证机制
- Simple Query: 基于文本的查询和回应
- Extended Query: 扩展查询,即预编译,可以把语句和参数分开传
- Copy: 导入/导出大量数据
- Cancel: 处理终止信号
实际上这五类是作者总结概括之后的,实际上会分的更加琐碎
为了实现比较完整的pg兼容性,狭义上的这些协议只是第一层,还需要支持广义的协议,包括:
- Query Language: SQL方言和关键函数
- Data Type System: 建立一个到pg独有的类型的映射
- pg_catalog Metadata: 对元数据信息系统的支持
架构:优缺点
相比于自己设计一套协议,复用pg的有很多优点:
- 已经证明的可靠性
- 解锁无尽的工具: 不同语言的驱动、管理工具、BI(Business Intelligence) Tools等等直接用PostgreSQL的已经非常完善的生态即可
- 允许把
Federated Queries作为一个FDW的数据源: FDW(Foreign Data Wrapper),外部数据包装器,所以这个特性简单的说就是可以把GreptimeDB挂载到Pg上,甚至可以对两个数据库的表做JOIN操作
当然也有缺点:
- Row-Oriented: Pg是关系型数据库,其协议设计时也是面向行传输的,而GreptimeDB却是按列处理的,这就需要一次转换
- 取消查询机制: 查询取消机制和pg的进程模型绑定,所以很麻烦且有安全风险
- 对于拓展查询的类型猜测: 拓展查询需要数据库在不实际执行时对表达式里的参数做类型推断,SQLite和DuckDB就没有提供这种API,因而做不到这种层面的兼容
实现:pgwire
GreptimeDB 用了作者写的pgwire库和DataFusion生态完成了实现
传输层协议支持
如果把pg的应用层协议类比http,那么pgwire就像hyper甚至axum。用户可以为不同程度的pg兼容性选择性的使用
以simple query为例,这个子协议甚至不假设用户的输入是SQL。pgwire提供了两个水平的api:
- Low-level API: SimpleQueryHandler::on_query: 重点关注消息级别的处理和语义。
- High-level API: SimpleQueryHandler::do_query: 重点关注数据级别的处理和语义。
#[async_trait]
impl SimpleQueryHandler for EchoHandler {
async fn do_query<C>(&self, _client: &mut C, query: &str) ->
PgWireResult<Vec<Response>>
where
C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
C::Error: Debug,
PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>
{
let query = query.to_string();
let f1 = FieldInfo::new("input".into(), None, None, Type::VARCHAR,
FieldFormat::Text);
let schema = Arc::new(vec![f1]);
let data = vec![Some(query)];
let mut encoder = DataRowEncoder::new(schema.clone());
let data_row_stream = stream::iter(data).map(move |r| {
encoder.encode_field(&r)?;
Ok(encoder.take_row())
});
Ok(vec![Response::Query(QueryResponse::new(
schema,
data_row_stream,
))])
}
}
忽略掉泛型以及相应的若干特征,函数体内做的逻辑还是比较清晰的:
创建一列,字段名input,类型字符串,用Arc包装,把用户的查询用Option包一下,vec!处理一下发给encoder得到字节流,这样有表头有数据,就可以发给客户端了
运行参考:
❯ psql -h 127.0.0.1 -p 5432 -U postgres
psql (18.1, server 16.6-pgwire-0.38.2)
Type "help" for help.
postgres=# hello world;
input
--------------
hello world;
(1 row)
嗯,不是SQL也可以
pg_catalog支持
更深的兼容需要元数据支持,这就是需要pg_catalog的地方了
pg_catalog需要一系列系统表和视图,而这需要建立在查询引擎之上
GreptimeDB使用DataFusion查询引擎,于是作者维护了一个datafusion-postgres项目作为pgwire DataFusion和 Arrow数据格式的适配器
然而作者表示这个工作实在是太复杂了,他们只专注于了五张主要的表(pg_database pg_namespace pg_tables pg_class pg_attributes),把其余的留给了社区
结语
作者把pg的兼容做成了可复用的库,这意味这不仅GreptimeDB可以用,其他数据基础设施也可以用它构建一个"Postgres-like"生态