Go語言 如何配製 高效能sql.DB

Go語言圈發表於2022-01-23

文章來自微信公眾號: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

對於這個基準測試,我們可以看到,允許開啟的連線越多,在資料庫上執行INSERT操作所花費的時間就越少(開啟的連線數為1時,執行速度3129633ns/op,而無限連線:531030ns/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從頭建立一個新的連線,我們可以從基準測試中看到,平均執行時和記憶體使用量相對較高。

只允許保留和重用一個空閒連線對基準測試影響特別明顯——它將平均執行時間減少了大約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意味著沒有最大生存期,連線將永遠可重用(這是預設行為)
db.SetConnMaxLifetime(time.Hour)

在這個例子中,所有的連線都將在建立後1小時“過期”,並且在過期後無法重用。但注意:

  • 這並不能保證連線將在池中存在整整一個小時;很有可能,由於某些原因,連線變得不可用,並在此之前自動關閉。
  • 一個連線在建立後一個多小時仍然可以被使用——它只是在這個時間之後不能被重用。
  • 這不是空閒超時。連線將在第一次建立後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個連線的硬限制,資料庫驅動程式(pq)立即返回一個太多客戶端連線的錯誤訊息,而無法完成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()),使用固定的、快速的超時上下文物件。


總結
1、根據經驗,應該顯式設定MaxOpenConns值。這應該小於資料庫和基礎設施對連線數量的硬性限制。
2、一般來說,更高的MaxOpenConnsMaxIdleConns值將帶來更好的效能。但你應該注意到效果是遞減的,連線池空閒連線太多(連線沒有被重用,最終會變壞)實際上會導致效能下降。
3、為了降低上面第2點帶來的風險,您可能需要設定一個相對較短的ConnMaxLifetime。但你也不希望它太短,導致連線被殺死或不必要地頻繁重建。
4、MaxIdleConns應該總是小於或等於MaxOpenConns
對於中小型web應用程式,我通常使用以下設定作為起點,然後根據實際吞吐量水平的負載測試結果進行優化。

db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5*time.Minute)

到此這篇關於golang配製高效能sql.DB的使用的文章就介紹到這了

本作品採用《CC 協議》,轉載必須註明作者和本文連結
歡迎關注微信公眾號:Go語言圈   點選加入:Go語言技術微信群

相關文章