如何配置 sql.DB 的 SetMaxOpenConns SetMaxIdleConns 和 SetConnMaxLifetime

Go

有很多不错的教程讨论了 Go 的 sql.DB 类型以及如何使用它执行 SQL 数据库查询和与语句. 但是它们大多数都掩盖了 SetMaxOpenConns(), SetMaxIdleConns() 以及 SetConnMaxLifetime() 方法 - 它们可用于配置 sql.DB 的行为并调整其性能.

在这篇文章中我想确切地解释下这些设置的作用, 并演示它们可能产生的影响 (正面和负面).

打开空闲链接

我将从一些背景开始。

sql.DB对象是许多数据库连接的池,其中包含'使用中'和'空闲'两种连接状态。当您使用连接来执行数据库任务(例如执行SQL语句或查询行)时,该连接会被标记为正在使用中。任务完成后,连接将被标记为空闲。

当您指示sql.DB执行数据库任务时,它将首先检查池中是否有可用的空闲连接。如果有一个可用,那么Go将重用此现有连接并将其标记为在任务执行期间处于使用状态。如果需要时池中没有空闲连接,则Go将创建一个新的附加连接。

SetMaxOpenConns 方法

默认情况下,同时打开的连接数(使用中+空闲)没有限制。但是您可以通过SetMaxOpenConns 方法实现对连接数的限制,如下所示:

//初始化一个新的连接池
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
if err != nil {
    log.Fatal(err)
}

//设置同时打开的连接数(使用中+空闲)
//设为5。将此值设置为小于或等于0表示没有限制
//最大限制(这也是默认设置)。
db.SetMaxOpenConns(5)

在此示例代码中,连接池现在最大限制为5个并发打开的连接。如果5个连接全部都已标记为使用中,并且需要另一个新连接,则应用程序将被迫等待,直到5个连接的其中一个被释放并变为空闲状态。

为了说明更改MaxOpenConns产生的影响,我进行了基准测试,将最大连接数分别设置为1,2,5,10和无限制。基准测试在PostgreSQL数据库上执行并行的INSERT语句,您可以在此要点中找到代码。结果如下:

BenchmarkMaxOpenConns1-8                 500       3129633 ns/op         478 B/op         10 allocs/op
BenchmarkMaxOpenConns2-8                1000       2181641 ns/op         470 B/op         10 allocs/op
BenchmarkMaxOpenConns5-8                2000        859654 ns/op         493 B/op         10 allocs/op
BenchmarkMaxOpenConns10-8               2000        545394 ns/op         510 B/op         10 allocs/op
BenchmarkMaxOpenConnsUnlimited-8        2000        531030 ns/op         479 B/op          9 allocs/op
PASS

编辑:为了清楚起见,此基准测试的目的不是模拟应用程序的`真实''行为。只是为了帮助说明sql.DB的幕后行为以及更改MaxOpenConns`对该行为的影响。

对于此基准测试, 我们可以看到允许打开的链接越多, 在数据库上执行 INSERT 所需的时间就越少 (3129633 ns/op 带有 1 个打开的链接, 而 531030 ns/op 则不受限制 - 大约快了 6 倍). 这是因为允许的链接越开放, 可以同时执行更多的数据库操作.

SetMaxIdleConns 方法

默认情况下 sql.DB 会在链接池中最多保留 2 个空闲链接. 可以通过 SetMaxIdleConns() 方法更改此方法, 如下所示:

// 初始化一个新的链接池
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
if err != nil {
    log.Fatal(err)
}

// 将最大并发空闲链接数设置为 5.
// 小于或等于 0 表示不保留任何空闲链接.
db.SetMaxIdleConns(5)

理论上来说, 在链接池中允许更多数量的空闲链接将提高性能, 因为它使从头开始建立新链接的可能性降低了, 因此有助于节省资源.

我们来看下相同的基准测试, 最大空闲链接数设置为 none, 1, 2 , 5 和 10 (并且打开的链接数不受限制):

BenchmarkMaxIdleConnsNone-8          300       4567245 ns/op       58174 B/op        625 allocs/op
BenchmarkMaxIdleConns1-8            2000        568765 ns/op        2596 B/op         32 allocs/op
BenchmarkMaxIdleConns2-8            2000        529359 ns/op         596 B/op         11 allocs/op
BenchmarkMaxIdleConns5-8            2000        506207 ns/op         451 B/op          9 allocs/op
BenchmarkMaxIdleConns10-8           2000        501639 ns/op         450 B/op          9 allocs/op
PASS

MaxIdleConns 设置为 none 时, 必须为每个 INSERT 从头开始创建一个新链接, 并且从基准测试中我们可以看到平均运行时间和内存使用率都相对较高.

仅保留 1 个空闲链接并重用它就对这个特定的基准测试产生了巨大的影响 - 它使平均运行时间减少了大约 8 倍, 并将内存使用量减少了大约 20 倍. 继续增加空闲链接池的大小可以使性能更好, 尽管改进的效果并不明显.

所以是否应该维护一个较大的空闲链接池嘛? 答案是 这得看应用程序.

重要得是要意识到保持空闲链接得存在是有代价得 - 它占用了内存, 否则可用于你得应用程序和数据库.

如果链接闲置时间过长, 也有可能变得无法使用. 例如, MySQL 得 wait_timeout 设置将自动关闭所有没有被使用得链接在 8 小时后 (默认情况下).

当发现这种情况时 sql.DB 会妥善处理它. 放弃之前错误得链接将自动重试两次, 这时 Go 将从链接池中删除该链接并创建一个新得链接. 因此将 MaxIdleConns 设置的太高实际上可能会导致链接变得不可用, 并且并空闲链接池较小 (使用较少得链接但频繁得使用) 消耗得资源个更多. 所以实际上如果你可能很快再次使用该链接, 则只需要保持链接空闲.

需要指出得最后一件事是 MaxIdleConns 应该始终小于或等于 MaxOpenConns. Go 会强制执行此操作, 并在必要时自动减去 MaxIdleConns.

SetConnMaxLifetime 方法

现在让我们看一下 SetConnMaxLifetime() 方法, 该方法设置了可重用链接得最大时间长度. 如果你得 SQL 数据还实现了最大链接得生命周期, 或者如果你希望方便得再负载均衡器之后轻松得交换数据库得话这将非常有用.

您可以这样使用它:

//初始化一个新的连接池
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
if err != nil {
    log.Fatal(err)
}

// 设置最大生存时间为1小时
// 设置为0,表示没有最大生存期,并且连接会被重用
// forever (这是默认配置).
db.SetConnMaxLifetime(time.Hour)

在此示例中,我们所有的连接将在首次创建后一个小时'过期',并且过期后无法重用。但请注意:

  • 这不能保证连接将在连接池中存在一个小时;连接很有可能由于某种原因而变得无法使用,并在此之前被自动关闭。
  • 建立连接后仍可以使用超过一小时-在那之后它就无法 启动 重用
  • 这不是空闲超时。连接将在第一次创建后1个小时到期,而不是在上一次空闲后1个小时到期。
  • 每秒自动执行一次清除操作,从连接池中删除“过期”的连接。

理论上, ConnMaxLifetime越短,连接过期的次数就会越频繁—因此—需要从头开始创建连接的次数也就越多。

为了说明这一点,我在ConnMaxLifetime设置为100ms,200ms,500ms,1000ms和无限制(永久重用)的情况下运行了基准测试,默认设置是无限制开放连接和2个空闲连接。 这些时间段显然比大多数应用程序中使用的时间段短很多,但它们可以很好地说明行为。

BenchmarkConnMaxLifetime100-8               2000        637902 ns/op        2770 B/op         34 allocs/op
BenchmarkConnMaxLifetime200-8               2000        576053 ns/op        1612 B/op         21 allocs/op
BenchmarkConnMaxLifetime500-8               2000        558297 ns/op         913 B/op         14 allocs/op
BenchmarkConnMaxLifetime1000-8              2000        543601 ns/op         740 B/op         12 allocs/op
BenchmarkConnMaxLifetimeUnlimited-8         3000        532789 ns/op         412 B/op          9 allocs/op
PASS

在这次特定的基准测试中,我们可以看到100ms生存时间的在内存占用上是无限制生存时间下的3倍多,并且每个INSERT的平均运行时间也稍长一些。

如果你确实再代码中设置了 ConnMaxLifetime, 请务必牢记链接过期得频率 (随后会重新创建). 例如如果总共有 100 个链接并且 ConnMaxLifetime 为 1 分钟, 则你的应用程序有可能每秒断开并重新创建多达 1.67 个链接 (平均值). 你不会希望如此高频率最终导致性能问题.

超出链接限制

最后, 如果不超过数据库链接数量得硬限制会发生什么情况呢.

作为说明, 我将更改我的 postgresql.conf 文件仅允许总共 5 个链接 (默认为 100)...

max_connections = 5

然后使用无限得开发链接重新允许基准测试...

BenchmarkMaxOpenConnsUnlimited-8    --- FAIL: BenchmarkMaxOpenConnsUnlimited-8
    main_test.go:14: pq: sorry, too many clients already
    main_test.go:14: pq: sorry, too many clients already
    main_test.go:14: pq: sorry, too many clients already
FAIL

一旦达到 5 个链接得硬限制, 我的数据库驱动程序 pg 将立即返回 sorry, too many clients already 错误消息并代替 INSERT 过程得完成.

为了防止出现此类错误, 我们需要将 sql.DB 中得 链接总数 (使用中 + 空闲) 最大设置为 5 以下.

// 初始化一个新得链接池
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
if err != nil {
    log.Fatal(err)
}

// 设置可用链接数最大为 3 (使用中 + 空闲).
db.SetMaxOpenConns(3)

现在在任何时候最多只能由 sql.DB 创建 3 个链接, 并且基准测试应该可以正常运行.

但这会带来一个很大得问题: 当达到可用得链接限制并且所有链接都在使用中时, 你的应用程序需要执行得任何数据库任务都将被迫等待, 直到链接变得空闲并标记为空闲. 例如在 Web 应用程序得上下文中, 用户得 HTTP 请求似乎被 '卡住', 并且在等待数据库任务运行时甚至可能超时.

为了缓解这种问题, 你应该使用 ExecContext() 启用 context.Context 对象. 这里有个 代码示例.

综上所述

  1. 根据经验, 应显式设置一个 MaxOpenConns 的值. 这应该低于数据库和基础结构所施加的对链接数的任何硬限制.
  2. 通常较高的 MaxOpenConnsMaxIdleConns 值会有更好的性能. 但收益却在下降, 应该意识到空闲的链接池过大实际上会导致性能下降 (链接没有被重用最终变为坏链).
  3. 为了缓解上述第 2 点的问题, 可能需要设置相对较短的 ConnMaxLifetime. 但是你不会希望太短, 导致不必要的链接被终止和不必要的重建.
  4. MaxIdleConns 应该始终小于或等于 MaxOpenConns.

对于中小型 Web 应该程序, 我通常使用以下设置开始, 然后根据负载测试的结果来调整和优化 (具有真实吞吐量水平的测试).

db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5*time.Minute)
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://www.alexedwards.net/blog/configu...

译文地址:https://learnku.com/go/t/49809

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!