1. 从一次凌晨三点的告警说起:Presto 报错为什么让人头疼
Presto 这个分布式 SQL 查询引擎,用起来是真香,查 TB 级数据跟切菜一样快。但一旦报错,尤其是那种模棱两可的报错信息,排查起来能让人把头发薅光。我印象最深的一次,是凌晨三点被告警叫醒,一个跑了半年都没问题的报表任务突然挂了,报错信息只有一行Query failed: Internal error。没有堆栈,没有具体节点,就这一句话。当时整个人是懵的,因为 Presto 的报错体系不像传统数据库那样有明确的错误码和文档索引,很多错误是运行时动态拼出来的,同一个现象可能对应好几种根因。
这篇文章想聊的,就是 Presto 在实际使用中最常撞见的那几类报错,以及我这些年攒下来的排查思路和应对手法。不管你是刚接触 Presto 的新手,还是已经跑了一段时间但遇到报错还是靠重启大法的人,下面这些内容应该都能帮你省下不少深夜加班的时间。我会尽量把每个报错的触发条件、底层原因、排查路径和修复方案讲透,而不是只丢一句“调大内存”就完事。
先说一个基本认知:Presto 的报错大致可以分成四层——语法解析层、查询规划层、执行调度层和资源与网络层。不同层级的报错,排查手段完全不一样。搞混了层级,就像去医院看病挂错了科室,折腾半天也找不到病因。下面我按这个分层逻辑,把常见报错一个个拆开讲。
2. 语法与解析阶段的报错:那些一眼就能看出问题的错误
2.1SYNTAX_ERROR:最容易被忽视的细节
SYNTAX_ERROR是 Presto 报错里最“善良”的一类,因为它通常会告诉你出错的位置和期望的语法结构。比如:
line 1:15: mismatched input 'from'. Expecting: ',', <EOF>这个报错的意思是,解析器在第 1 行第 15 个字符处遇到了from,但它期望的是逗号或者语句结束。常见原因有几个:一是 SQL 里用了 Presto 不支持的语法,比如某些 MySQL 特有的函数写法;二是引号或括号不匹配,导致解析器提前进入了错误状态;三是关键字拼写错误,比如把SELECT写成了SELEC。
我遇到最多的情况是函数名拼写错误。Presto 的函数名和 Hive、MySQL 有差异,比如字符串拼接,Presto 用concat()或者||,但有人习惯性写concat_ws()的参数顺序搞反了,或者用了group_concat()这种 Presto 根本不存在的函数。这类错误排查起来其实很快,关键是仔细读报错信息里的位置提示,然后对照 Presto 官方文档的函数列表确认。
提示:Presto 的语法解析器对大小写不敏感,但函数名和列名在特定连接器下可能敏感。如果报错位置看起来没问题,检查一下是不是用了保留字作为列名而没有加双引号。
2.2COLUMN_NOT_FOUND:元数据不同步的典型症状
这个报错的全称通常是:
Column 'xxx' cannot be resolved或者:
Table 'xxx' does not exist表面上看是列名或表名写错了,但实际排查中,元数据缓存不一致才是更常见的根因。Presto 的元数据是从 Hive Metastore 或者其它 catalog 服务拉取的,并且会在 coordinator 节点做缓存。如果底层表结构发生了变更(比如加了列、改了列名、删了分区),而 Presto 的缓存还没刷新,就会报这个错。
我处理过的一个案例是:某张 Hive 表在凌晨做了ALTER TABLE ADD COLUMNS,然后 Presto 查询立刻报列不存在。重启 coordinator 之后问题消失,但这不是长久之计。正确的做法是调用 Presto 的元数据刷新接口:
CALL system.flush_metadata_cache(schema_name => 'your_schema', table_name => 'your_table')或者在连接器层面配置更短的元数据缓存过期时间。对于 Hive 连接器,可以调整hive.metastore-cache-ttl和hive.metastore-refresh-interval这两个参数。前者控制缓存存活时间,后者控制后台刷新频率。生产环境建议把hive.metastore-cache-ttl设成 30 秒到 1 分钟,太短会增加 Metastore 压力,太长会导致元数据变更后查询失败。
2.3TYPE_MISMATCH:隐式转换的坑
Presto 是强类型引擎,不像 MySQL 那样会自动做很多隐式类型转换。比如你写:
SELECT * FROM orders WHERE order_id = '12345'如果order_id是BIGINT类型,Presto 会直接报TYPE_MISMATCH,而不是像 MySQL 那样默默把字符串转成数字。这个设计其实更严谨,但习惯了 MySQL 的人刚开始会很不适应。
解决办法有两种:一是显式转换,用CAST('12345' AS BIGINT);二是确保比较的两边类型一致。我建议在 ETL 脚本里统一做类型规范化,不要依赖引擎的隐式转换。另外,JOIN操作时如果两边的关联键类型不一致,也会报这个错,而且报错信息可能不会直接指出是哪两个列,需要自己根据查询计划去定位。
3. 查询规划与优化阶段的报错:逻辑没问题但引擎不买账
3.1EXCEEDED_MAXIMUM_AGGREGATION_STATES:聚合状态爆炸
这个报错在跑大聚合查询时特别常见:
Query failed: Exceeded maximum aggregation states: 10000000Presto 在做GROUP BY或者DISTINCT时,会在内存里维护一个聚合状态表。当 group 的基数(cardinality)太大,超过max-aggregation-states的限制时,就会抛这个错。默认值通常是 1000 万,对于高基数的维度聚合,很容易撞到天花板。
这里有个误区:很多人第一反应是调大这个参数。但调大只是把问题延后,因为聚合状态是存在内存里的,基数越大内存消耗越猛,最后可能直接 OOM。正确的思路是从查询本身入手。比如:
- 检查
GROUP BY的维度是不是真的需要那么多。有时候业务方要的“明细”其实可以先用近似算法(如approx_distinct)替代精确去重。 - 如果确实需要高基数聚合,考虑把查询拆成两步:先做局部聚合,再对中间结果做全局聚合。
- 对于
DISTINCT操作,可以尝试用GROUP BY重写,因为 Presto 对GROUP BY的优化通常更好。
我个人的经验是,当max-aggregation-states超过 5000 万时,查询的稳定性会明显下降,即使不报错,执行时间也会变得不可预测。所以与其调参数,不如重新审视查询逻辑。
3.2COMPILER_ERROR:代码生成失败的排查
Presto 在生成执行计划时,会把查询编译成 Java 字节码。如果查询特别复杂,比如嵌套了十几层子查询、大量的CASE WHEN、或者超长的IN列表,就可能触发:
Compiler failed: Generated code is too large这个报错的根因是 JVM 对单个方法的字节码大小有限制(64KB)。Presto 生成的代码如果超过这个限制,编译就会失败。排查方法是简化查询结构:
- 把超长的
IN列表拆成临时表或者用JOIN替代。 - 把复杂的
CASE WHEN逻辑抽成子查询,分步计算。 - 减少不必要的列扫描,
SELECT *在宽表上会生成大量代码。
有一个比较隐蔽的触发场景是分区裁剪失效。如果查询没有正确利用分区字段过滤,Presto 会扫描所有分区,生成的代码里会包含大量分区判断逻辑,间接导致代码膨胀。所以遇到COMPILER_ERROR时,先检查EXPLAIN输出里的分区裁剪情况。
3.3NOT_SUPPORTED:连接器能力边界
Presto 通过连接器访问不同的数据源,但每个连接器的能力是不一样的。比如 Hive 连接器支持INSERT,但某些 NoSQL 连接器可能只支持SELECT。当你执行了连接器不支持的语法时,就会报:
NOT_SUPPORTED: Unsupported operation这类报错的关键是确认连接器的能力矩阵。比如:
| 操作 | Hive 连接器 | MySQL 连接器 | Kafka 连接器 |
|---|---|---|---|
| SELECT | 支持 | 支持 | 支持 |
| INSERT | 支持 | 支持 | 不支持 |
| DELETE | 有限支持 | 支持 | 不支持 |
| CREATE TABLE | 支持 | 支持 | 不支持 |
遇到NOT_SUPPORTED时,不要急着怀疑查询写错了,先查一下当前连接器的文档,确认这个操作是否在支持范围内。如果确实不支持,考虑把数据导出到支持该操作的存储,或者换一种实现方式。
4. 执行与调度阶段的报错:最让人抓狂的一类
4.1Query exceeded per-node memory limit:内存超限的完整排查链路
这个报错可以说是 Presto 用户遇到最多的错误之一:
Query exceeded per-node memory limit of 30GB或者:
Query exceeded distributed user memory limit of 100GBPresto 的内存管理分好几个层级:per-node memory limit是单个 worker 节点的内存上限,distributed user memory limit是整个集群的用户内存上限。报错信息里会明确告诉你是哪一层超了。
排查这个问题的第一步,是看查询计划里哪个算子消耗内存最多。用EXPLAIN (TYPE DISTRIBUTED)可以看到每个 stage 的分布情况,但更实用的是在 Presto 的 Web UI 里看查询的实时内存曲线。通常内存飙升发生在几个场景:
- 大表 JOIN 大表:如果 JOIN 的 build 侧表太大,会全部加载到内存里。解决办法是调整 JOIN 顺序,把小的表放在 build 侧,或者用
/*+ BROADCAST */提示强制广播小表。 - 窗口函数没有分区限制:
OVER (PARTITION BY ...)如果分区基数太大,每个分区都会在内存里维护状态。可以尝试增加分区字段,或者用ROWS BETWEEN限制窗口范围。 - ORDER BY 全局排序:
ORDER BY会把所有数据汇聚到一个节点做排序,数据量大时必然 OOM。如果只是要取 Top N,用LIMIT配合ORDER BY可以让 Presto 做局部排序再合并。
我处理过一个典型案例:一个JOIN查询,左表 10 亿行,右表 500 万行。用户把大表放在了 build 侧,导致每个 worker 都要加载 10 亿行数据。调整 JOIN 顺序后,内存消耗直接降了 90%。所以遇到内存报错,先看 JOIN 顺序,再看聚合基数,最后才考虑调参数。
4.2Task exceeded max wait time:任务排队超时
这个报错通常出现在集群资源紧张的时候:
Task exceeded max wait time of 5m意思是任务在队列里等了超过 5 分钟还没被调度执行。根因是集群的并发查询数超过了处理能力,或者某个查询占用了大量资源导致其他查询排队。
排查方向有两个:一是看集群的整体负载,如果 CPU 和内存长期跑满,说明需要扩容或者优化现有查询;二是看是否有“大查询”霸占资源。Presto 有资源组(resource group)机制,可以把不同优先级的查询分到不同的组里,限制每组能使用的资源比例。比如:
{ "rootGroups": [ { "name": "etl", "softMemoryLimit": "60%", "maxQueued": 10, "hardConcurrencyLimit": 5 }, { "name": "adhoc", "softMemoryLimit": "30%", "maxQueued": 20, "hardConcurrencyLimit": 10 } ] }这样 ETL 任务和即席查询互不干扰,避免互相挤占资源。配置资源组的时候要注意,softMemoryLimit是软限制,超过之后新查询会排队,但已经在跑的查询不会被 kill。hardConcurrencyLimit是硬并发限制,超过直接拒绝。
4.3Node failed/Worker node is unhealthy:节点故障的处理
分布式系统里节点故障是常态,Presto 的 coordinator 会定期检查 worker 的健康状态。如果某个 worker 心跳超时或者响应异常,就会报:
Node failed: worker-xxx或者:
Worker node is unhealthy: worker-xxx这类报错的处理分两种情况:如果是偶发的单节点故障,Presto 会自动把任务重新调度到其他节点,查询可能只是变慢而不会失败。但如果多个节点同时故障,或者故障节点上正好有不可恢复的状态,查询就会直接失败。
排查节点故障时,我通常会按这个顺序检查:
- 节点进程是否存活:
ps -ef | grep presto看进程在不在。 - 节点日志有没有 OOM 或 GC 异常:
tail -f /var/log/presto/server.log看最近的错误。 - 网络是否通畅:从 coordinator 节点
telnet worker-ip 8080测试端口连通性。 - 磁盘是否写满:Presto 的临时数据目录如果满了,节点会进入不健康状态。
预防节点故障的关键是监控和告警。JVM 的 GC 时间、堆内存使用率、磁盘使用率、网络延迟这几个指标必须纳入监控。特别是 Full GC 时间,如果单次超过 10 秒,节点基本上就处于不可用状态了。
5. 资源与网络层的报错:那些和查询逻辑无关的坑
5.1Too many open files:文件描述符耗尽
这个报错看起来和 Presto 没关系,但实际很常见:
java.io.IOException: Too many open filesPresto 在运行时会打开大量文件:数据文件、临时文件、网络连接、日志文件等。如果操作系统的文件描述符限制太低,就会报这个错。Linux 默认的ulimit -n通常是 1024,对于 Presto 来说远远不够。
解决办法是调整系统的文件描述符限制。在/etc/security/limits.conf里加上:
presto soft nofile 65536 presto hard nofile 65536然后重启 Presto 进程。注意,ulimit的修改只对新的登录会话生效,已经运行的进程需要重启才能应用新限制。另外,Presto 的 JVM 参数里也可以设置-XX:-MaxFDLimit来让 JVM 自己管理文件描述符上限,但更推荐在操作系统层面解决。
5.2Connection refused/Connection timed out:网络问题的排查
Presto 是分布式架构,coordinator 和 worker 之间、worker 和 worker 之间都有大量的网络通信。网络问题导致的报错通常长这样:
Connection refused: worker-xxx:8080或者:
Connection timed out: worker-xxx:8080Connection refused通常是目标节点的进程没起来,或者端口没监听。Connection timed out则更可能是防火墙规则、网络分区或者路由问题。
排查网络问题时,我习惯用这个链路:
- 从 coordinator ping worker 的 IP:确认基础网络可达。
- telnet worker-ip 8080:确认 Presto 的 HTTP 端口可访问。
- 检查 worker 的
node.properties里的node.internal-address:这个地址必须是 coordinator 能访问到的地址,不能是localhost或127.0.0.1。 - 检查防火墙规则:确保 Presto 使用的端口段(默认 8080 和随机分配的数据传输端口)没有被拦截。
有一个容易被忽略的点是主机名解析。Presto 的节点之间通过主机名通信,如果 DNS 解析不稳定或者/etc/hosts配置不一致,就会导致间歇性的连接失败。生产环境建议在/etc/hosts里显式配置所有节点的 IP 和主机名映射,不要依赖 DNS。
5.3No space left on device:磁盘写满的应急处理
Presto 在执行大查询时,会把中间结果写到磁盘的临时目录。如果磁盘空间不足,就会报:
No space left on device这个报错的处理分两步:应急和根治。
应急处理是清理临时目录。Presto 的临时目录默认在/tmp/presto或者由node.data-dir指定。可以直接删除里面的旧文件,但要注意不要删正在使用的文件。更安全的做法是重启对应的 worker 节点,Presto 启动时会自动清理临时目录。
根治方案是配置多磁盘目录。Presto 支持配置多个临时数据目录,把 IO 分散到不同的磁盘上:
node.data-dir=/data1/presto,/data2/presto,/data3/presto同时,要设置合理的临时文件清理策略。Presto 有task.max-local-exchange-buffer-size和exchange.client-threads等参数可以控制中间结果的落盘行为。如果查询本身不需要落盘(比如纯内存聚合),可以通过optimizer.optimize-hash-generation等参数优化执行计划,减少磁盘 IO。
6. 几个真实场景的排查实录
6.1 一个“随机失败”的查询:元数据缓存惹的祸
之前遇到过一个查询,同样的 SQL 跑十次,大概有两次会失败,报错是Table does not exist。但表明明存在,而且大部分时候都能查到。这种“随机失败”最让人头疼,因为无法稳定复现。
排查过程是这样的:先看失败查询的 coordinator 日志,发现报错发生在查询规划阶段,说明是元数据获取的问题。然后检查 Hive Metastore 的日志,发现 Metastore 本身没有异常。最后把注意力放到 Presto 的元数据缓存上,发现 coordinator 配置的hive.metastore-cache-ttl是 10 分钟,而底层表的分区信息每隔几分钟就会更新一次。当缓存过期和分区更新同时发生时,就会出现短暂的元数据不一致。
解决办法是把hive.metastore-cache-ttl降到 1 分钟,同时开启hive.metastore-refresh-interval为 30 秒。调整之后,随机失败的概率从 20% 降到了几乎为零。这个案例告诉我们,元数据缓存不是越长越好,要根据底层数据的变更频率来权衡。
6.2 内存报错背后的真凶:数据倾斜
有一次一个GROUP BY查询频繁报内存超限,但 group 的基数并不大,只有几万个。按常理不应该消耗那么多内存。用EXPLAIN看了执行计划,发现 Presto 选择了 hash 聚合,而且所有数据都集中到了少数几个 worker 上。
进一步分析发现,GROUP BY的某个字段存在严重的数据倾斜——80% 的记录都集中在 3 个值上。Presto 的 hash 分区是按 group key 做的,倾斜的 key 会导致对应的 worker 处理的数据量远超其他节点,内存自然就爆了。
解决办法有两个:一是对倾斜字段做加盐处理,比如GROUP BY concat(key, '_', cast(rand() * 10 as int)),先做局部聚合再合并;二是开启 Presto 的倾斜优化参数optimizer.optimize-top-n-row-number和optimizer.optimize-skewed-join。对于聚合倾斜,加盐是更通用的方案,虽然会多一次聚合,但能彻底解决单点压力问题。
6.3 查询突然变慢但不报错:GC 的隐形影响
还有一种情况比报错更隐蔽:查询不报错,但执行时间从几分钟变成了几十分钟。排查发现查询计划没变、数据量没变、集群负载也正常。最后看 worker 的 GC 日志,发现 Full GC 频率从每小时一次变成了每几分钟一次。
根因是某个 worker 节点的堆内存碎片化严重,虽然总内存够用,但大对象分配失败触发了频繁 Full GC。Presto 的查询执行需要大量内存分配,GC 停顿会直接拖慢查询速度。
解决办法是调整 JVM 的 GC 策略。对于 Presto 这种内存密集型应用,推荐使用 G1 GC:
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45同时,定期重启 worker 节点(比如每周一次)可以缓解内存碎片问题。如果条件允许,升级到更大内存的机器也能显著降低 GC 频率。
7. 我总结的 Presto 报错速查思路
遇到 Presto 报错时,我一般按这个顺序走:
第一步,看报错层级。是语法错误、规划错误、执行错误还是资源错误?语法错误直接改 SQL;规划错误看EXPLAIN输出;执行错误看 Web UI 的内存和任务分布;资源错误看集群监控。
第二步,看报错信息里的具体数字。Presto 的报错通常会带上限制值和实际值,比如exceeded memory limit of 30GB,这个 30GB 就是当前配置的上限。知道上限之后,要么降低查询的资源消耗,要么调整配置。
第三步,看日志的上下文。单行报错信息往往不够,coordinator 日志里报错前后的几行通常包含更多线索,比如是哪个 stage、哪个 task、哪个 worker 出的问题。
第四步,用EXPLAIN验证执行计划。很多执行阶段的报错,根因在规划阶段。EXPLAIN (TYPE DISTRIBUTED)可以看到数据在各个 stage 之间的分布,帮助判断是否有数据倾斜或者不合理的 JOIN 顺序。
第五步,小数据量复现。如果问题无法定位,把查询涉及的表换成小样本数据,看是否能复现。能复现就逐步加数据量,找到触发问题的临界点;不能复现就说明是数据量或数据分布的问题。
最后分享一个我踩过的坑:有一次报错信息是Query failed: Internal error,没有任何细节。查了半天没头绪,最后发现是 coordinator 的/tmp目录满了,导致查询状态无法写入。所以遇到“无信息报错”时,先检查一下 coordinator 和 worker 的基础环境——磁盘、内存、文件描述符,这些看似无关的因素往往是真凶。