Trino(Presto)调研
Trino(Presto)的历史#
2012年 Facebook 的工程师开发了 Presto ,作为一个查询引擎,其利用大规模并行处理技术,可以将原本需要12小时运行的SQL任务在分钟级甚至秒级完成。
当时 Facebook 的主要痛点是在 Hive 上的 map reduce 任务运行时间太长,Presto 很好的解决了这个问题。
在接下来的几年,Presto 增加了多种数据源的支持,其可以通过 SQL 查询各种数据源的数据并进行处理,即便数据源不支持 SQL (例如:ES,MongoDB)。
由于其提供统一的 SQL 查询能力,数据分析人员和现有的BI工具也可以很容易使用它来处理各种数据。
2018年 Presto 的两名作者离开 Facebook 并创建了一个更适合开源社区的项目 PrestoSQL,并于2020年更名为 Trino。
这篇文章介绍了这两个项目的相关信息:https://www.starburst.io/learn/presto-sql/prestosql-and-prestodb/
Trino on MongoDB#
我本次调研主要是用 Trino 帮助进行 MongoDB 的 Join 操作。
在对 MongoDB 中的数据进行查询时,如果涉及到两个 Collection 的 Join (使用 $lookup 操作符)并对结果进行过滤,则性能非常差。
例如:将“设备”表与“人员”表进行关联,通过人员是否是“离职”的属性进行过滤,获得所有离职人员的“设备”列表。在5万设备与2千人员关联查询时,MongoDB需要4秒钟才能得出结果,这还是在相关字段做了索引的条件下。
而如果使用 Trino 进行关联查询,只需 0.3 秒就可以得出结果。
Trino 是将所有数据源的数据都统一抽象成关系数据库的表,每张表有若干字段,每个字段都有一个确定的数据类型(例如:varchar,int,array(varchir)等)。
Trino 的 MongoDB Connector 会自动识别各个 Collection 中的数据类型,并将识别出的 schema 写在一个隐藏表中,若其识别的不正确,可以手工修改表中的数据进行纠正。(https://trino.io/docs/current/connector/mongodb.html#table-definition)
在 MongoDB 中,如果一个字段可能会是多种数据类型,则在使用 Trino 时会面临问题。
实测中发现,如果一个字段在有些文档中是字符串,有些文档中是数组,如果 Trino 中以字符串定义该字段时,数组数据会转换成一个字符串,但不是Json格式,无法再次解析。如果 Trino 中以数组定义该字段,则字符串类型的值会变成NULL。
Connector 也提供了 直接查询 MongoDB 的能力,但不支持 aggregation ,只能使用 filter 对 collection 中的数据进行过滤并返回。
性能比较:
join的查询效率#
下面 mongo 语句从“设备”表中关联“软件”表,并过滤安装有“log4j”软件的设备列表,该语句在5万设备和118万软件数据时,耗时5.9秒。
db.device.aggregate([
{
$lookup: {
from: "software",
localField: "deviceId",
foreignField: "deviceId",
pipeline: [
{$match: {$expr: {$eq: ["$name", "log4j"]}}}
],
as: "softwares",
},
},
{
$match: {$expr: {$lt: [0, {$size: "$softwares"}]}}
}
])
通过下面的 Trino 语句进行同样的查询时,只需要 0.3 秒就可以得到结果。
SELECT a.hostname, b.name, b.deviceId FROM device a, software b WHERE a.deviceId = b.deviceId AND b.name = 'log4j';
count的查询效率#
但是在对软件表进行 count 时,Trino表现的就很慢:
SELECT count(1) FROM software;
需要19.5秒返回结果,而用 mongo 来求 count 只需要 0.6 秒。
然而,如果优化 count 语句使用 count(_id) 则只需要 1.53 秒,算是可以接受的一个范围。
下表是不同查询语句的性能比较:
查询语句 | 运行时间 | Trino输出 |
---|---|---|
SELECT count(*) FROM software | 17.59 | 17.59 [1.17M rows, 0B] [66.4K rows/s, 0B/s] |
SELECT count(1) FROM software | 16.50 | 16.50 [1.17M rows, 0B] [70.7K rows/s, 0B/s] |
SELECT count(_id) FROM software | 1.53 | 1.53 [1.17M rows, 0B] [765K rows/s, 0B/s] |
db.software.aggregate([{$count:“count”}] | 0.65 | 用mongo查询 |
字段过滤的查询效率#
查询语句 | 运行时间 | 输出 |
---|---|---|
db.software.aggregate([{$match:{name: {$regex: “log4j”}}}]) | 3.64 | |
SELECT * FROM software WHERE name LIKE ‘%log4j%’ | 17.89 | 17.89 [1.17M rows, 0B] [65.3K rows/s, 0B/s] |
SELECT _id,name,vendor,version FROM software WHERE name LIKE ‘%log4j%’ | 2.72 | 2.72 [1.17M rows, 0B] [430K rows/s, 0B/s] |
db.software.aggregate([{$match:{name: {$regex: “log4j”}}},{$count:“count”}]) | 3.57 | |
SELECT count(_id) FROM software WHERE name LIKE ‘%log4j%’ | 2.09 | 2.09 [1.17M rows, 0B] [558K rows/s, 0B/s] |
在通过软件名称进行模糊查询时,有趣的是如果在 Trino 中使用 SELECE * FROM 则效率要慢得多,而如果指定字段,则效率比mongo还要更高一些。
支持将不同数据源的数据查询并 join 在一起#
下面语句从 ElasticSearch 中查询软件数据,从 MongoDB 中查询设备数据,并将这两组数据 join 到一起。
SELECT a.deviceId, a.name, b.hostname FROM es.default."software: name:log4j" a LEFT JOIN mongo.test.device b ON a.deviceId = b.deviceId
该语句在5万设备数据时,执行时间仅需 0.27 秒。
在上面的语句中,使用了 ElasticSearch Connector 的一个特性,可以直接调用 elasticsearch 的 full text query 来进行查询。
“software: name:log4j” 就表示查询 software 索引中 name 字段包含 log4j 的数据。
为什么 Trino 效率高#
Trino可以利用计算机的多核进行并行处理,下面是 Trino 运行查询时的服务器负载情况,可以看到在 Trino 运行时,各个CPU都在工作。
1 [||||||||| 25.8%] 5 [||||||| 21.8%]
2 [||||||||| 27.1%] 6 [|||||||||| 29.0%]
3 [||||||| 21.3%] 7 [||||||| 20.0%]
4 [|||||||||| 30.1%] 8 [|||||| 17.5%]
Mem[|||||||||||||||||||27.4G/31.2G] Tasks: 567, 1773 thr; 4 running
Swp[ 0K/0K] Load average: 0.61 0.44 0.48
Uptime: 81 days, 22:19:08
而下面是MongoDB在运行查询时的系统负载,只有一个CPU在进行高负荷运行,其它CPU都处于空闲的状态。
1 [|| 3.2%] 5 [|| 3.2%]
2 [|| 3.8%] 6 [|| 2.6%]
3 [|| 3.8%] 7 [||||||||||||||||||||||||100.0%]
4 [|| 1.3%] 8 [|| 3.2%]
Mem[|||||||||||||||||||24.3G/31.2G] Tasks: 475, 1720 thr; 2 running
Swp[ 0K/0K] Load average: 10.88 3.10 1.81
Uptime: 81 days, 23:46:59
从内存占用上看,Trino内存还是可以接受的,在运行了一些查询后,内存占用在2G左右
PID USER PRI NI VIRT RES SHR S CPU% MEM% TIME+ Command
25014 polkitd 20 0 18.5G 15.5G 10828 S 0.7 49.8 55h37:59 mongod --auth --bind_ip_all
10921 dev_guest 20 0 31.6G 2465M 8340 S 3.3 7.7 3h02:53 java -cp /usr/lib/trino/lib/* -s
但是 Trino 是 Java 程序,与其它服务部署在同一台服务器时需要注意限制其内存占用。由于我是用 docker 启动的,所以可以通过 -m 参数限制容器的内存即可。
坑点记录#
环境中的 ElasticSearch 8.0 是需要用 https 访问的,并且使用了自签名的证书,Trino 配置后会报证书问题。
使用 stackoverflow 中的方法将 es 的证书导入到 trino 容器的 java 默认证书中,具体方法为:
用下面命令将网站的证书保存到文件中:
echo -n | openssl s_client -connect <ServerName>:<PORT> -servername <ServerName> | openssl x509 > /tmp/<ServerName>.cert
之后用下面命令将证书导入到 jdk 的 cert-store 中:
sudo keytool -import -alias mycertificate -keystore "<JDK_HOME>/lib/security/cacerts" -file /tmp/<ServerName>.cert
在 Trino 容器中导入时发现没有权限,通过下面语句可以进入容器中的 shell 并切换到 root 用户:
docker exec -it -u 0 <container> bash
或者用用户名:
docker exec -it -u root <container> bash
解决证书问题后,Trino 在 show tables 时会报连接不上 elasticsearch 服务器,其IP地址与配置的地址不同。
由于 elasticsearch 也是使用 docker 启动的,怀疑为 es 在 docker 私有网络内的地址,使用下面命令列出 docker 的网络并查看 es 所在的网络,确认确实是 es 的私有网络地址。
docker network ls
docker network inspect <network>
在 Trino 的 docker run 命令行中加入 –network elasticsearch_default 让 Trino 容器加入到 ElasticSearch 的网络中,问题解决。