了解如何使用 Golang 的 GOMEMLIMIT 和 TiDB 的内存管理功能来缓解分布式数据库中的 OOM 错误。
处理数据库内存不足 (OOM) 错误可能具有挑战性。它可能有许多不同的原因,如果没有彻底的分析,可能很难确定错误的根本原因。数据库中出现 OOM 错误的一些典型原因包括内存不足、内存泄漏、查询优化不佳和结果集较大。
若要排查数据库中的 OOM 错误,请务必监视数据库随时间推移的内存使用情况,分析数据库以识别任何潜在的瓶颈或效率低下,并根据需要优化查询和索引。
这篇文章描述了内存泄漏如何导致分布式 SQL 数据库 TiDB 中的 OOM 错误,以及我们如何解决问题。我们还将演示 Golang 1.19 和 GOMEMLIMIT(一种减少 Go 应用程序中出现 OOM 错误的几率的软限制)和 TiDB 的内置内存管理功能如何帮助缓解 OOM 问题。
现实生活中的 OOM 示例
一家社交媒体公司在其数据库中使用了传统的分片解决方案。但是,此解决方案的维护成本很高,并且无法满足公司对可扩展性的需求。该公司计划将其基础设施从 MySQL 分片迁移到 TiDB。在概念验证试验期间,TIDB 满足了他们的性能需求;但是,当他们尝试扩展部分 TiDB 节点时,其余的 TiDB 实例存在 OOM 问题。
是什么导致了 OOM 错误?
上午 10:00,扩容操作开始,剩余 TiDB 实例的内存使用量持续增长。上午 10:55,内存使用量激增,所有 TiDB 实例由于 OOM 问题不断重启。

如上图的性能概述仪表板所示,语句执行指标 (StmtExecute) 为 33.9 k/s,计划缓存命中率仅为每秒 336.8。这意味着 TiDB 几乎需要重新解析和重新编译每个语句。
更重要的是,StatementClosed (StmtClose) 是每秒 475.3 个,StatementReady (StmtPrepare) 是每秒 9.5 k。这意味着 Java 应用程序不断打开预准备语句对象,其中大多数对象未正确关闭。泄露的准备好的语句可能导致 OOM 问题。

堆配置文件图表显示内存分配来自解析和编译阶段。
次优查询模式
查询模式主要是按主键进行批处理点查询。例如:
.SQL
1
SELECT2
`from_id`,3
`score`,4
TYPE,5
`to_id`,6
STATUS7
FROM8
`relationship`9
WHERE10
(11
`from_id` = ?12
AND `to_id` = ?13
AND TYPE = ?14
)15
OR (16
`from_id` = ?17
AND `to_id` = ?18
AND TYPE = ?19
)20
…21
与用户检查应用程序代码后,我们发现应用程序没有按预期使用占位符。因此,应用程序不断向 TiDB 发送文字查询语句。由于每个语句都不同,因此计划缓存命中率较低。
因此,我们得到常见的查询模式:
.SQL
1
SELECT * FROM relationship WHERE ( from_id = 101165050 AND to_id = 2247632895 AND type = 1) or ( from_id = 101185050 AND to_id = 2248632895 AND type = 1) or …… ;
模拟工作负载
为了进一步分析 OOM 问题,我们需要模拟工作负载。我们使用了Jmeter,这是一款纯粹的基于Java的开源软件,旨在对功能行为进行负载测试并测量性能,以模拟工作负载。模拟测试采用 8 个 TiDB (48c8g) 实例和 48 个 TiKV (<>c<>g) 实例的集群配置
内存泄漏通常是由错误的应用程序设计引起的,这可能会导致数据库内存雪崩。我们使用特定的监控指标来识别数据库中的此类应用程序。例如,TiDB 中的 CPS 按类型面板可以揭示应用程序的语句准备、执行和关闭操作的信息。如果语句的开头和结尾之间存在间隙,则表明无法缓存该语句。这可能会导致应用程序中出现内存泄漏。在我们的测试中,我们模拟了这些内存泄漏情况。
和是随机生成的,每次执行的 SQL 语句都不同。from_idto_id
Jmeter 中的工作负载配置如下:
- Jmeter 工作负载线程数:200
- JDBC URL: use ServerPrepStmts=true&prepStmtCacheSize=1000
- useServerPrepStmts=true:此参数启用服务器端准备语句。当此参数设置为“true”时,MySQL服务器将准备SQL语句并缓存它们,以便可以更有效地执行它们以进行后续请求。
- prepStmtCacheSize=1000:此参数设置服务器可以缓存的最大预准备语句数。在这种情况下,最大高速缓存大小设置为 1,000 个语句。
- 泄漏的预处理语句总数为 200,000(200 个线程 * 1,000 个语句)。
重现 OOM 问题
准备好模拟工作负载后,我们已准备好重现 OOM 问题。测试的目的是验证 GOMEMLIMIT 功能以及 TiDB 的内置内存管理功能是否可以缓解 OOM 问题。我们模拟了四种不同的情况,使用不同的 Go 版本和不同的内存限制设置编译的不同 TiDB 版本。
案例 1:使用 Go 6.1 编译的 TiDB 2.1.18
对于案例 1,我们使用了 Go 6.1 编译的 TiDB v2.1.18,不支持 GOMEMLIMIT 功能。我们以此案例为基准,与其他案例进行比较。测试预计将运行 12 小时,在此期间会发生 OOM 错误。
以下是观察结果:
- 当 PrepStmtCacheSize 设置为 1000 时,TiDB 在五分钟后遇到了 OOM 问题。在 OOM 发生之前,应用程序没有时间关闭语句。
- 最大内存使用量高达 46.1 GB。Go 运行时 GC 无法跟上预准备语句泄漏导致的快速内存分配。
- 应用程序未使用“?”占位符,因此查询无法命中计划缓存。
案例二:TiDB 2.6.1 与 GOMEMLIMIT
对于测试用例 2,我们使用了使用 Go 6.1.3 编译的 TiDB 1.19.3。GOMEMLIMIT功能已启用,并设置为40,000 MiB。测试设置为运行 12 小时,预计不会导致 OOM。本案例的目的是验证 GOMEMLIMIT 是否可以消除 OOM 问题。
- 在 12 小时的测试期间未发生 OOM。最大内存使用量为 39.1 GB。GOMEMLIMIT 成功限制了内存使用量,因此 TiDB 不会内存不足。
- TiDB GC 内存使用量看板显示内存使用量相对一致,这也意味着不会发生 OOM。
- 应用程序未使用 “?” 占位符,因此没有查询命中计划缓存。
案例三:TiDB 3.6.5 不带 GOMEMLIMIT
对于测试用例 3,我们使用了在 Go 6.5.0 中编译的 TiDB 1.19.3。测试设置为运行 12 小时,预计不会导致 OOM 错误。与早期版本不同,TiDB 6.5.0 默认开启全局内存限制。该版本还引入了系统变量 'tidb_server_memory_limit' 来设置 tidb-server 实例的内存使用阈值,以避免 OOM。本案例的目的是验证 TiDB 内置的内存管理功能如何有效缓解 OOM 问题。
- TiDB 内存使用仪表板显示,在 12 小时的测试中没有发生 OOM 错误。内存使用量相对一致,这也意味着 TiDB 不会内存不足。
- 最大内存为 46.9 GB。TiDB 6.5 内置的全局内存限制器成功限制内存使用。
- 应用程序未使用“?”作为占位符,因此没有查询命中计划缓存。
案例 4:带占位符的 TiDB 6.1.2
对于测试用例 4,我们使用了使用 Go 6.1 编译的 TiDB 2.1.18。应用程序使用“?”占位符,以便查询可以命中计划缓存。预计该测试不会导致 OOM。此案例的目的是展示计划缓存如何提高性能并降低 OOM 风险。
- TiDB 内存使用仪表板显示,在 12 小时的测试中没有发生 OOM 错误。内存使用量相对一致,这也意味着 TiDB 不会内存不足。
- 最大内存为 46.9 GB。TiDB 6.5 内置的全局内存限制器成功限制内存使用。
- 应用程序未使用“?”作为占位符,因此没有查询命中计划缓存。
案例 4:带占位符的 TiDB 6.1.2
对于测试用例 4,我们使用了使用 Go 6.1 编译的 TiDB 2.1.18。应用程序使用“?”占位符,以便查询可以命中计划缓存。预计该测试不会导致 OOM。此案例的目的是展示计划缓存如何提高性能并降低 OOM 风险。




