
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:MaxGCPauseMillis200 -XX:InitiatingHeapOccupancyPercent45同时定期重启 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 的基础环境——磁盘、内存、文件描述符这些看似无关的因素往往是真凶。