配置 sql.DB 得到更好的性能

配置 sql.DB 得到更好的性能

原文: Configuring sql.DB for Better Performance

打开和空闲链接

首先说一点背景知识。web

sql.db对象是包含多个open和idle数据库链接的链接池。当使用链接执行数据库任务(如执行SQL语句或查询数据)时,该链接被标记为open(打开)。任务完成后,链接将变为idle(空闲)。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的影响,我运行了一个基准测试,将最大开放链接设置为一、二、五、10和无限制。基准测试在PostgreSQL数据库上执行并行的insert语句,您能够在这个gist中找到代码。结果以下:svg

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对该行为的影响。post

对于这个基准,咱们能够看到容许的开放链接越多,在数据库上执行插入操做所花费的时间就越少(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)

理论上,在池中容许更多的空闲链接将提升性能,由于这样能够减小从头开始创建新链接的可能性,从而有助于节省资源。.net

让咱们来看看相同的基准,最大空闲链接设置为无、一、二、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时,必须为每一个插入操做建立新的链接,从基准中咱们能够看到平均运行时间和内存分配相对较高。

只容许保留和重用一个空闲链接,在咱们这个特定的基准测试中有很大的不一样——它将平均运行时间减小了8倍左右,并将内存分配减小了20倍左右。继续增长空闲链接池的大小会使性能更好,尽管这些改进不那么明显。

那么咱们应该维护一个大的空闲链接池吗?答案是它取决于应用程序。

重要的是要认识到保持空闲链接的存活是要付出代价的——它会占用内存,不然这些内存能够同时用于应用程序和数据库。

也有一种可能,若是一个链接空闲过久,那么它也可能会变得不可用。例如,MySQL的wait_timeout设置将自动关闭8小时内未使用的任何链接(默认状况下)。

当发生这种状况时,sql.DB会优雅地处理它。在放弃以前,将自动重试两次坏链接,以后Go将从池中删除坏链接并建立新链接。所以,将MaxIdleConns设置得过高实际上可能会致使链接变得不可用,而且使用的资源比使用较小的空闲链接池(使用的链接更少,使用频率更高)的状况下要多。因此只有你极可能立刻再次使用浙西链接,你才会保持这些链接空闲。

最后要指出的一点是,MaxIdleConns应该始终小于或等于MaxOpenConns。Go会检查并在必要时自动减小MaxIdleConns StackOverflow上的一个解释很好地描述了缘由:

设置比MaxOpenConns更多的空闲链接数是没有意义的,由于你最多也就能拿到全部打开的链接,剩余的空闲链接依然保持的空闲。这就像一座四车道的桥,可是只容许三辆车同时经过。

SetConnMaxLifetime 方法

如今让咱们来看一下SetConnMaxLifetime()方法,它设置了链接可重用的最大时间长度。若是您的SQL数据库也实现了最大的链接生存期,或者(例如)您但愿在负载均衡器后面方便地切换数据库,那么这将很是有用。

您能够这样使用它:

// 初始化链接池
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
if err != nil {
    log.Fatal(err)
}
// 设置链接的最大生命周期为一小时。
// 设置为0的话意味着没有最大生命周期,链接老是可重用(默认行为)。
db.SetConnMaxLifetime(time.Hour)

在这个例子中,咱们的全部链接将在第一次建立后1小时“过时”,而且在它们过时后没法重用。可是注意:

这并不能保证链接将在池中存在完整的一小时;极可能因为某种缘由链接将变得不可用,而且在此以前自动关闭。
一个链接在建立后仍可使用一个多小时,只是说一个小时后不能再被重用了。
这不是空闲超时。链接将在第一次建立后1小时后过时,而不是1小时后变成空闲。
每秒自动运行一次清理操做以便从池中删除“过时”链接。
理论上,ConnMaxLifetime越短,从零开始建立链接的频率就越高。

为了说明这一点,我运行了基准测试,将ConnMaxLifetime设置为100ms、200ms、500ms、1000ms和unlimited(永远重复使用),默认设置为unlimited open connections和2个idle connections。这些时间段显然比您在大多数应用程序中使用的要短得多,但它们有助于很好地说明链接库的行为。

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毫秒的内存分配要比unlimited的内存分配多三倍,并且每一个插入的操做的平均运行时间也稍长一些。

超出链接限制
最后,若是不说起超过了数据库链接数的硬限制的话,那么本文就不算一个完整的教程了。

如图所示,我将更改postgresql.conf文件,所以只容许总共5个链接(默认值为100)…

max_connections = 5
使用 unlimited open connections 的配置进行基准测试:

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错误信息,而不是完成插入操做。

为了不这个错误,咱们须要将sql.DB中打开和空闲链接的最大总数设置为5如下。像这样:

// 初始化链接池
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
if err != nil {
    log.Fatal(err)
}
//设置open和idle的总链接数为3
db.SetMaxOpenConns(2)
db.SetMaxIdleConns(1)

如今,由sql.DB建立的链接数最多只能有3个,基准测试运行时应该没有错误。

可是这样也会给咱们带来一个很大的警示:当达到开放链接限制时,应用程序须要执行的任何新数据库任务都将被强制等待,直到链接变为空闲。

对于某些应用程序,该行为可能很好,但对于其余应用程序,则可能很差。例如,在Web应用程序中,最好当即记录错误消息并向用户发送500 Internal Server Error,而不是让他们的HTTP请求挂起,并可能在等待空闲链接时超时。