golang中如何配置 sql.DB 以获得更好的性能

有很多很好的教程讨论 Go 的sql.DB类型以及如何使用它来执行 SQL 数据库查询和语句。但它们中的大多数都掩盖了SetMaxOpenConns()、SetMaxIdleConns()和SetConnMaxLifetime()方法------您可以使用它们来配置 的行为sql.DB并改变其性能。

在这篇文章中,我想准确解释这些设置的作用,并展示它们可能产生的(积极和消极)影响。

打开和空闲连接

我将从一些背景开始。

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

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

SetMaxOpenConns 方法

默认情况下,同时打开的连接数(使用中+空闲)没有限制。SetMaxOpenConns()但你可以通过这样的方法实现你自己的限制:

go 复制代码
// Initialise a new connection pool
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
if err != nil {
    log.Fatal(err)
}

// Set the maximum number of concurrently open connections (in-use + idle)
// to 5. Setting this to less than or equal to 0 will mean there is no 
// maximum limit (which is also the default setting).
db.SetMaxOpenConns(5)

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

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

bash 复制代码
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对于此基准测试,我们可以看到允许的打开连接越多,在数据库上执行操作所需的时间就越少(1 个打开连接的 3129633 ns/op 与无限制连接的 531030 ns/op 相比,大约快 6 倍)。这是因为允许的打开连接越多,可以并发执行的数据库查询就越多。

SetMaxIdleConns 方法

默认情况下,sql.DB连接池中最多保留2个空闲连接。您可以通过SetMaxIdleConns()如下方法更改此设置:

go 复制代码
// Initialise a new connection pool
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
if err != nil {
    log.Fatal(err)
}

// Set the maximum number of concurrently idle connections to 5. Setting this
// to less than or equal to 0 will mean that no idle connections are retained.
db.SetMaxIdleConns(5)

从理论上讲,允许池中存在更多数量的空闲连接将提高性能,因为它使得需要从头开始建立新连接的可能性降低,从而有助于节省资源。

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

bash 复制代码
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 数据库还实现了最长连接生存期,或者例如您希望在负载均衡器后面轻松地交换数据库,那么这会很有用。

你像这样使用它:

go 复制代码
// Initialise a new connection pool
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
if err != nil {
    log.Fatal(err)
}

// Set the maximum lifetime of a connection to 1 hour. Setting it to 0
// means that there is no maximum lifetime and the connection is reused
// forever (which is the default behavior).
db.SetConnMaxLifetime(time.Hour)

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

这并不能保证连接将在池中存在整整一个小时;连接很可能由于某种原因变得不可用并在此之前自动关闭。

连接在创建后仍然可以使用一小时以上 - 只是在那之后无法开始重用。

这不是空闲超时。连接将在首次创建后 1 小时到期,而不是在最后一次空闲后 1 小时。

每秒自动运行一次清理操作,以从池中删除"过期"连接。

从理论上讲,连接越短,ConnMaxLifetime连接过期的频率就越高,因此,需要从头开始创建连接的频率就越高。

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

bash 复制代码
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

在这些特定的基准测试中,我们可以看到,与无限生命周期相比,100 毫秒生命周期的内存使用量增加了 3 倍多,并且每个生命周期的平均运行时间也INSERT稍长。

如果您ConnMaxLifetime在代码中进行了设置,请务必记住连接过期(并随后重新创建)的频率。例如,如果您总共有 100 个连接,连接时间ConnMaxLifetime为 1 分钟,那么您的应用程序每秒可能会终止并重新创建多达 1.67 个连接(平均)。您不希望这个频率太高,以至于最终会阻碍性能,而不是帮助它。

超出连接限制

最后,如果不提及超过数据库连接数量的硬限制会发生什么,那么本文就不完整。

作为说明,我将更改我的postgresql.conf文件,因此总共只允许 5 个连接(默认值为 100)...

bash 复制代码
max_connections = 5

然后以无限的开放连接重新运行基准测试...

bash 复制代码
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 个连接的硬限制,我的数据库驱动程序 ( pq ) 就会立即返回一条sorry, too many clients already错误消息,而不是完成INSERT.

为了防止出现此错误,我们需要将打开连接的最大总数(使用中 + 空闲)设置sql.DB为低于 5。如下所示:

go 复制代码
// Initialise a new connection pool
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
if err != nil {
    log.Fatal(err)
}

// Set the number of open connections (in-use + idle) to a maximum total of 3.
db.SetMaxOpenConns(3)

sql.DB现在,任何时刻最多只能创建 3 个连接,并且基准测试运行时应该不会出现任何错误。

但这样做有一个很大的警告:当达到打开连接限制并且所有连接都在使用中时,应用程序需要执行的任何新数据库任务都将被迫等待,直到连接空闲并标记为空闲。例如,在 Web 应用程序的上下文中,用户的 HTTP 请求可能会"挂起",甚至可能在等待数据库任务运行时超时。

为了缓解这种情况,您应该在进行数据库调用时始终传递一个context.Context具有固定、快速、超时的对象,使用启用上下文的方法,例如ExecContext(). 可以在此处的要点中看到一个示例。

总之

根据经验,您应该明确设置一个MaxOpenConns值。这应该大大低于数据库和基础设施对连接数量的硬性限制。

一般来说,MaxOpenConns和MaxIdleConns值越高,性能越好。但回报是递减的,您应该意识到,拥有太大的空闲连接池(连接未被重用并最终变坏)实际上会导致性能下降。

为了减轻上述第 2 点的风险,您可能需要设置相对较短的ConnMaxLifetime. 但您不希望这个时间太短,导致连接被频繁地不必要地终止和重新创建。

MaxIdleConns应始终小于或等于MaxOpenConns。

对于中小型 Web 应用程序,我通常使用以下设置作为起点,然后根据实际吞吐量水平的负载测试结果进行优化。

go 复制代码
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5*time.Minute)
相关推荐
cookqq7 分钟前
mongodb源码分析session异步接受asyncSourceMessage()客户端流变Message对象
数据库·sql·mongodb·nosql
呼拉拉呼拉19 分钟前
Redis故障转移
数据库·redis·缓存·高可用架构
什么都想学的阿超22 分钟前
【Redis系列 04】Redis高可用架构实战:主从复制与哨兵模式从零到生产
数据库·redis·架构
pp-周子晗(努力赶上课程进度版)42 分钟前
【MySQL】视图、用户管理、MySQL使用C\C++连接
数据库·mysql
斯特凡今天也很帅1 小时前
clickhouse常用语句汇总——持续更新中
数据库·sql·clickhouse
超级小忍2 小时前
如何配置 MySQL 允许远程连接
数据库·mysql·adb
吹牛不交税2 小时前
sqlsugar WhereIF条件的大于等于和等于查出来的坑
数据库·mysql
hshpy3 小时前
setting up Activiti BPMN Workflow Engine with Spring Boot
数据库·spring boot·后端
文牧之4 小时前
Oracle 审计参数:AUDIT_TRAIL 和 AUDIT_SYS_OPERATIONS
运维·数据库·oracle
篱笆院的狗4 小时前
如何使用 Redis 快速实现布隆过滤器?
数据库·redis·缓存