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)
相关推荐
Elastic 中国社区官方博客3 分钟前
Elasticsearch 开放推理 API 增加了对 IBM watsonx.ai Slate 嵌入模型的支持
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
企鹅侠客7 分钟前
ETCD调优
数据库·etcd
Json_1817901448014 分钟前
电商拍立淘按图搜索API接口系列,文档说明参考
前端·数据库
煎饼小狗25 分钟前
Redis五大基本类型——Zset有序集合命令详解(命令用法详解+思维导图详解)
数据库·redis·缓存
永乐春秋42 分钟前
WEB-通用漏洞&SQL注入&CTF&二次&堆叠&DNS带外
数据库·sql
打鱼又晒网1 小时前
【MySQL】数据库精细化讲解:内置函数知识穿透与深度学习解析
数据库·mysql
大白要努力!1 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
tatasix2 小时前
MySQL UPDATE语句执行链路解析
数据库·mysql
ifanatic2 小时前
[面试]-golang基础面试题总结
面试·职场和发展·golang
南城花随雪。2 小时前
硬盘(HDD)与固态硬盘(SSD)详细解读
数据库