上一篇我们沿着订单日报的执行阶段讨论了“SQL 一直运行中到底该去哪里查”。其中有一站很容易被忽略访问 Metastore。这次接着往下走聊一个实际遇到的案例。最近线上 Hive Metastore 发生过一次 OOM。排查时发现存储元数据的 MySQL 与 Hive 节点不在同一个 VPC访问数据库的链路存在额外耗时。再看 HS2 的连接监控高峰时几个节点合起来有数百个连接。于是一个很自然的判断出现了MySQL 变慢请求堵在 Metastore最后把内存撑满了。这个推断有机制和配置依据。但要把它写成确定根因还需要证明请求确实积压了而且积压的资源确实造成了这次 OOM。本文基于Hive 3.1.2、远程部署的 Metastore 与 MySQL。OOM、网络排查结论和配置来自这个实际案例下面的时延算例与两张配图用于解释机制不是故障现场测量。当前尚未通过故障时线程栈、GC 日志和堆转储完成根因确认。主机、账号等信息已省略。一、订单还没开始算为什么先要访问 MySQL沿用前面系列的订单表。我们想汇总某一天的已支付订单HS2 在组织执行前需要知道表有哪些字段、日期分区在哪里、文件是什么格式。优化过程中还可能需要统计信息。这些问题要找 Metastore也就是 HMS。HMS 再根据需要从后端 MySQL 取得元数据。订单 SQL → HiveServer2 中的编译与执行逻辑 → Metastore 的元数据接口 → 元数据存储层与 JDBC 连接池 → MySQLMySQL 在这里存的是库、表、分区等登记信息订单明细仍在 HDFS 等数据存储中。一次订单查询也不只可能访问一次 HMS查表、查分区、取统计信息以及部分写入收尾都可能触发不同调用。第一篇讲过服务分工。这里再补一条排障经验元数据这一段还没走完时后面的计算资源再充足SQL 也可能继续等待。二、监控里几百个连接应该怎样理解这次提供的连接图中一个选中时刻的三个 HS2 节点分别有 85、381、382 个总连接合计 848 个。这些数值是负载参考。截图时间与那次 OOM 并不处于同一个故障窗口也不是已经确认的全天最高值。更重要的是连接数有好几层。看到的数量它实际表示什么客户端到 HS2 的连接数有多少连接保持打开其中可能有空闲连接正在执行或编译的 SQL 数当前有多少查询在相应阶段工作HS2 等客户端到 HMS 的连接、请求元数据服务这一侧的访问情况HMS 到 MySQL 的数据库连接数JDBC 连接池能够提供的数据库连接这几组数字没有固定的一比一关系。一个连接可能暂时不查元数据一条 SQL 也可能先后发出多次元数据请求。因此848 个 HS2 连接不能直接换算成 848 条 MySQL SQL。图里的“总连接”和“HA 连接”也要先弄清统计口径不能当作两批独立连接相加。三、每次多等几毫秒怎么就开始堵了可以把 HMS 的数据库连接池想成一组能反复使用的电话。工作线程要找 MySQL 查资料先借一部电话用完后再归还。如果一通电话一直没有结束后面的人就借不到这部电话。工作人员增加了电话另一头的处理速度却没有因此提高。回到真实系统连接通常会被复用并不是每条 SQL 都重新建立 TCP 连接。但数据库请求、结果传输以及相应路径中的事务交互仍然需要经过网络。假设一次元数据操作有 20 次串行数据库往返每次额外增加 5 ms仅这些往返就可能多花约 100 ms。这是帮助理解的假设真实次数取决于接口、分区规模、缓存和执行路径。接着看第二个简单算例。假设一段稳定负载中每秒有 100 个 HMS 请求平均响应时间 50 ms平均未完成请求约为 100 × 0.05 5 个 平均响应时间 500 ms平均未完成请求约为 100 × 0.5 50 个这就是排队分析中常用的关系平均未完成请求数 ≈ 请求速率 × 平均响应时间。统计时必须使用同一层请求、同一时间口径并满足稳定运行等条件不能拿 HS2 连接数直接代入。请求到达速度没变每个请求停留的时间变长同时留在系统里的请求就会增多。如果已经持续超出处理能力系统还可能进入排队、拒绝、超时或重试不能再用这两个简单数字描述全部过程。不同 VPC 本身也不是故障判定条件。真正要测的是HMS 所在节点到 MySQL 的实际耗时、抖动和丢包情况。连通方式、可用区、路由和数据库负载都可能影响结果。四、线程只是等着为什么还会占内存“没在干活”不等于“没有占资源”。一个 HMS 请求等待数据库时相关调用栈仍然存在请求参数、处理上下文、已经取得的元数据对象或缓冲区也可能仍被引用。具体保留了什么要看它停在处理的哪一步。只要对象仍被活跃调用引用垃圾回收器就不能因为请求很慢而把它收走。垃圾回收简称 GC回收的是已经不再需要的对象不负责替业务取消慢请求。在 Hive 3.1.2 的常见 ObjectStore 路径中HMS 还会通过ThreadLocal保存线程关联的存储对象。ThreadLocal可以理解为“这个线程自己的随身资料”它同样需要适时清理。HMS 实现如果新请求继续进入或新连接带来更多工作线程这些资源可能同时保留更久。大量对象占着堆GC 回收效果又不好处理速度还可能进一步下降。不过图中的 OOM 是有条件的结果。并发受控、请求占用较小、超时能及时生效时表现也可能只是变慢、失败或拒绝接入。还有一个容易写错的细节这一版本 HMS 使用的 ThriftTThreadPoolServer默认执行器采用SynchronousQueue没有一个能无限存放请求的工作队列。工作线程围绕客户端连接处理请求同一连接也可以承载后续调用。因此不能把这里笼统写成“无限队列把内存塞满了”。积压可能表现为工作线程等待 JDBC、连接资源占用以及上游的排队与重试。Thrift 0.9.3 实现五、这份配置为什么值得警惕现场确认下面这份hive-site.xml也由独立的 Metastore 进程加载。这里只摘出与本次分析有关的值它们是案例配置不是推荐配置。参数案例值它控制的事情hive.metastore.server.max.threads100000HMS 工作线程上限datanucleus.connectionPool.maxPoolSize10单个数据库连接池的最大连接数hive.server2.thrift.max.worker.threads3000HS2 Thrift 服务工作线程上限hive.server2.async.exec.threads3000HS2 后台操作线程池大小hive.server2.async.exec.wait.queue.size3000HS2 后台操作等待队列容量hive.driver.parallel.compilationtrue允许多个会话并行编译hive.metastore.client.socket.timeout600sHMS 客户端 socket 超时hive.hmshandler.retry.attempts10HMS 处理器针对符合条件异常的重试次数配置hive.hmshandler.retry.interval2000ms上述重试间隔hive.metastore.limit.partition.request-1相关接口不设置分区数量上限这组配置允许较多上游工作进入而数据库访问仍受连接池和实际调用耗时约束。MySQL 一旦变慢等待压力就可能留在中间。10 万是允许的上限不是已经创建了 10 万线程。10 也是单个连接池的容量不是整个集群只有 10 个 MySQL 连接。多个 HMS 实例及不同存储路径会影响总连接数不能简单算一个“线程与连接比例”就决定扩容。并行编译尤其值得结合负载检查。编译阶段可能访问表、分区和统计信息多个会话一起编译就可能一起请求 HMS。另一方面HS2 的后台等待队列属于 HS2 进程不能直接说它就是 HMS 堆里的对象。HS2 后台线程池实现超时为什么不能只看一个 600 秒这份配置使用 HikariCP没有显式设置hikari.connectionTimeout。按该版本实现默认获取连接等待为 30 秒。这里的“获取连接超时”相当于借电话最多等多久。它管不到已经借到电话以后对方迟迟没有答复的情况。HikariCP 参数说明JDBC URL 中也没有显式设置 MySQL 的connectTimeout、socketTimeout。如果没有其他覆盖配置这两项驱动默认值为 0表示不设置相应超时前者针对连接建立后者针对网络 socket 操作。MySQL 官方说明所以HMS 客户端等了 600 秒后报错不意味着服务端正在进行的 JDBC 调用一定在同一时刻停止也不意味着所有资源马上释放。服务端的检查机制、数据库驱动超时和客户端超时需要分开理解。重试也一样。单纯变慢不会自动触发这里所有重试只有符合重试条件的异常才会进入相应分支。发生持续故障时多轮尝试却可能让一次调用拖得更久。RetryingHMSHandler六、另一个排查方向一次拿回太多分区还要把“大量小请求一起等”和“少量请求特别大”分开。案例中的hive.metastore.limit.partition.request-1意味着受该参数约束的接口没有设置分区数量限制。若某个调用需要大量完整分区对象即使并发不高也可能造成明显的堆压力。现场同时开启了 Direct SQLhive.metastore.direct.sql.batch.size0。0 表示自动选择在该版本的 MySQL 路径中会选择不拆批。其他位置配置的batch.retrieve.max300不能当作所有接口、所有路径的统一分区上限。Direct SQL 实现即使调整了批大小也要区分两件事一次数据库查询取多少以及最终要在内存里组装多少对象。有些实现分批查询后仍会汇总成完整结果返回分批不等于流式处理。这给 OOM 排查留下了三种需要区分的情况同时等待的请求很多每个请求都保留了一些对象。请求数量不多但某次分区或统计信息读取非常大。大请求遇到延迟同时存活的时间变长两种影响叠加。这些情况可能都表现为Java heap space但调整方向并不完全相同。七、怎样把合理怀疑变成根因结论先找准发生 OOM 的进程和完整错误。HS2、HMS、YARN 容器各有自己的内存边界不能混在一起处理。报错或现象优先检查Java heap space哪些对象占据 Java 堆为什么仍被引用GC overhead limit exceededGC 后仍然存活的对象是否太多堆容量是否不足unable to create new native thread实际线程数、本地内存、线程与进程限制系统或容器 OOMKilled进程总内存及系统、容器限制不能只看-XmxJava 堆 OOM 也不等于已经证明内存泄漏。可能是当时同时存活的对象超过了配置容量。Oracle OOM 说明接下来在同一故障时间窗口里把三段证据接起来。第一段数据库调用确实先变慢了。比较 HMS 侧完整调用耗时、数据库侧执行情况和网络表现。不要只测一次 ping也不要因为 MySQL 慢日志没有异常就排除网络等待。第二段HMS 确实出现了等待积压。观察连接池是否持续满载、获取连接等待是否增加连续几次线程快照是否有大量线程停留在 JDBC 读取或连接池获取位置。只有一个偶然等待的线程不足以说明整个服务堵塞。第三段积压确实对应到资源耗尽。堆转储要看主要占用对象及其引用关系线程类问题则看实际线程增长和进程资源限制。GC 日志帮助判断内存压力何时开始。顺序也很重要。如果先发生长时间 GC随后 HMS 响应变慢那么“慢”可能是内存问题的结果。不能把因果方向倒过来。如果调整网络路径后在相近请求量、请求类型和 JVM 配置下数据库调用耗时、连接池等待和内存压力一起下降会进一步支持网络延迟这条解释。仅仅“重启后恢复”还不够因为重启同时清掉了很多运行状态。八、处理时先让系统有边界对于这个案例我会把实际网络延迟作为优先验证和改善的方向同时重新评估服务能够承受多少并发。HMS 的 10 万线程上限值得优先检查但具体改成多少要结合正常峰值连接、堆大小、线程内存和压测结果。限得太低会拒绝新连接放得过大又可能让资源耗尽先于接入限制发生。数据库连接池也不是越大越好。适度增加连接可能缓解某些等待但如果 MySQL 已经受限继续扩池可能增加竞争。判断依据应该是连接池等待、连接占用时长、数据库余量以及所有实例的总连接预算。HS2 侧可以从调度错峰、查询并发和编译压力入手超时与重试要围绕正常耗时和故障恢复目标一起设计大分区调用则需要合适的数量约束和查询范围。这些调整应该分批验证。正常请求也需要足够的处理时间过短的超时可能制造失败与重复尝试分区限制则可能拒绝原本允许的大查询。这次案例目前能给出的判断是网络延迟与现有配置具备形成资源积压的条件是值得优先验证的故障路径最终是哪类对象或线程资源导致 OOM还要由故障证据回答。回到那条订单 SQL下次看到它迟迟不动时可以再多追一步谁在等谁这一段等待占着什么系统有没有给等待留出合理的边界。本篇关键源码与资料HiveMetaStore.javaThrift 服务、线程配置与线程关联的存储对象。ObjectStore.java持久化访问与 PersistenceManager 生命周期。MetastoreConf.java参数定义默认值与案例配置要分开看。HikariCPDataSourceProvider.java连接池大小与获取连接超时的设置。DatabaseProduct.javaDirect SQL 自动分批的数据库类型判断。上一篇 · 返回专栏目录