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 的网络中,问题解决。

comments powered by Disqus