本文目錄一覽:
Golang需要自己實現數據庫連接池嗎
使用完後必須con.close()掉,
使用連接池的話,執行con.close並不會關閉與數據庫的TCP連接,而是將連接還回到池中去,如果不close掉的話,這個連接將會一直被佔用,直接連接池中的連接耗盡為止。
使用Go實現一個數據庫連接池
開始本文之前,我們看一段Go連接數據庫的代碼:
本文內容我們將解釋連接池背後是如何工作的,並 探索 如何配置數據庫能改變或優化其性能。
轉自:
整理:地鼠文檔:
那麼sql.DB連接池是如何工作的呢?
需要理解的最重要一點是,sql.DB池包含兩種類型的連接——“正在使用”連接和“空閑”連接。當您使用連接執行數據庫任務(例如執行SQL語句或查詢行)時,該連接被標記為正在使用,任務完成後,該連接被標記為空閑。
當您使用Go執行數據庫操作時,它將首先檢查池中是否有可用的空閑連接。如果有可用的連接,那麼Go將重用這個現有連接,並在任務期間將其標記為正在使用。如果在您需要空閑連接時池中沒有空閑連接,那麼Go將創建一個新的連接。
當Go重用池中的空閑連接時,與該連接有關的任何問題都會被優雅地處理。異常連接將在放棄之前自動重試兩次,這時Go將從池中刪除異常連接並創建一個新的連接來執行該任務。
連接池有四個方法,我們可以使用它們來配置連接池的行為。讓我們一個一個地來討論。
SetMaxOpenConns()方法允許您設置池中“打開”連接(使用中+空閑連接)數量的上限。默認情況下,打開的連接數是無限的。
一般來說,MaxOpenConns設置得越大,可以並發執行的數據庫查詢就越多,連接池本身成為應用程序中的瓶頸的風險就越低。
但讓它無限並不是最好的選擇。默認情況下,PostgreSQL最多100個打開連接的硬限制,如果達到這個限制的話,它將導致pq驅動返回”sorry, too many clients already”錯誤。
為了避免這個錯誤,將池中打開的連接數量限制在100以下是有意義的,可以為其他需要使用PostgreSQL的應用程序或會話留下足夠的空間。
設置MaxOpenConns限制的另一個好處是,它充當一個非常基本的限流器,防止數據庫同時被大量任務壓垮。
但設定上限有一個重要的警告。如果達到MaxOpenConns限制,並且所有連接都在使用中,那麼任何新的數據庫任務將被迫等待,直到有連接空閑。在我們的API上下文中,用戶的HTTP請求可能在等待空閑連接時無限期地“掛起”。因此,為了緩解這種情況,使用上下文為數據庫任務設置超時是很重要的。我們將在書的後面解釋如何處理。
SetMaxIdleConns()方法的作用是:設置池中空閑連接數的上限。缺省情況下,最大空閑連接數為2。
理論上,在池中允許更多的空閑連接將增加性能。因為它減少了從頭建立新連接發生概率—,因此有助於節省資源。
但要意識到保持空閑連接是有代價的。它佔用了本來可以用於應用程序和數據庫的內存,而且如果一個連接空閑時間過長,它也可能變得不可用。例如,默認情況下MySQL會自動關閉任何8小時未使用的連接。
因此,與使用更小的空閑連接池相比,將MaxIdleConns設置得過高可能會導致更多的連接變得不可用,浪費資源。因此保持適量的空閑連接是必要的。理想情況下,你只希望保持一個連接空閑,可以快速使用。
另一件要指出的事情是MaxIdleConns值應該總是小於或等於MaxOpenConns。Go會強制保證這點,並在必要時自動減少MaxIdleConns值。
SetConnMaxLifetime()方法用於設置ConnMaxLifetime的極限值,表示一個連接保持可用的最長時間。默認連接的存活時間沒有限制,永久可用。
如果設置ConnMaxLifetime的值為1小時,意味着所有的連接在創建後,經過一個小時就會被標記為失效連接,標誌後就不可復用。但需要注意:
理論上,ConnMaxLifetime為無限大(或設置為很長生命周期)將提升性能,因為這樣可以減少新建連接。但是在某些情況下,設置短期存活時間有用。比如:
如果您決定對連接池設置ConnMaxLifetime,那麼一定要記住連接過期(然後重新創建)的頻率。例如,如果連接池中有100個打開的連接,而ConnMaxLifetime為1分鐘,那麼您的應用程序平均每秒可以殺死並重新創建多達1.67個連接。您不希望頻率太大而最終影響性能吧。
SetConnMaxIdleTime()方法在Go 1.15版本引入對ConnMaxIdleTime進行配置。其效果和ConnMaxLifeTime類似,但這裡設置的是:在被標記為失效之前一個連接最長空閑時間。例如,如果我們將ConnMaxIdleTime設置為1小時,那麼自上次使用以後在池中空閑了1小時的任何連接都將被標記為過期並被後台清理操作刪除。
這個配置非常有用,因為它意味着我們可以對池中空閑連接的數量設置相對較高的限制,但可以通過刪除不再真正使用的空閑連接來周期性地釋放資源。
所以有很多信息要吸收。這在實踐中意味着什麼?我們把以上所有的內容總結成一些可行的要點。
1、根據經驗,您應該顯式地設置MaxOpenConns值。這個值應該低於數據庫和操作系統對連接數量的硬性限制,您還可以考慮將其保持在相當低的水平,以充當基本的限流作用。
對於本書中的項目,我們將MaxOpenConns限制為25個連接。我發現這對於小型到中型的web應用程序和API來說是一個合理的初始值,但理想情況下,您應該根據基準測試和壓測結果調整這個值。
2、通常,更大的MaxOpenConns和MaxIdleConns值會帶來更好的性能。但是,效果是逐漸降低的,而且您應該注意,太多的空閑連接(連接沒有被複用)實際上會導致性能下降和不必要的資源消耗。
因為MaxIdleConns應該總是小於或等於MaxOpenConns,所以對於這個項目,我們還將MaxIdleConns限制為25個連接。
3、為了降低上面第2點的風險,通常應該設置ConnMaxIdleTime值來刪除長時間未使用的空閑連接。在這個項目中,我們將設置ConnMaxIdleTime持續時間為15分鐘。
4、ConnMaxLifetime默認設置為無限大是可以的,除非您的數據庫對連接生命周期施加了硬限制,或者您需要它協助一些操作,比如優雅地交換數據庫。這些都不適用於本項目,所以我們將保留這個默認的無限制配置。
與其硬編碼這些配置,不如更新cmd/api/main.go文件通過命令行參數讀取配置。
ConnMaxIdleTime值比較有意思,因為我們希望它傳遞一段時間,最終需要將其轉換為Go的time.Duration類型。這裡有幾個選擇:
1、我們可以使用一個整數來表示秒(或分鐘)的數量,並將其轉換為time.Duration。
2、我們可以使用一個表示持續時間的字符串——比如“5s”(5秒)或“10m”(10分鐘)——然後使用time.ParseDuration()函數解析它。
3、兩種方法都可以很好地工作,但是在這個項目中我們將使用選項2。繼續並更新cmd/api/main.go文件如下:
File: cmd/api/main.go
如何在 Go 語言中使用 Redis 連接池
一、關於連接池
一個數據庫服務器只擁有有限的資源,並且如果你沒有充分使用這些資源,你可以通過使用更多的連接來提高吞吐量。一旦所有的資源都在使用,那麼你就不 能通過增加更多的連接來提高吞吐量。事實上,吞吐量在連接負載較大時就開始下降了。通常可以通過限制與可用的資源相匹配的數據庫連接的數量來提高延遲和吞 吐量。
如何在Go語言中使用Redis連接池
如果不使用連接池,那麼,每次傳輸數據,我們都需要進行創建連接,收發數據,關閉連接。在並發量不高的場景,基本上不會有什麼問題,一旦並發量上去了,那麼,一般就會遇到下面幾個常見問題:
性能普遍上不去
CPU 大量資源被系統消耗
網絡一旦抖動,會有大量 TIME_WAIT 產生,不得不定期重啟服務或定期重啟機器
服務器工作不穩定,QPS 忽高忽低
要想解決這些問題,我們就要用到連接池了。連接池的思路很簡單,在初始化時,創建一定數量的連接,先把所有長連接存起來,然後,誰需要使用,從這裡取走,幹完活立馬放回來。 如果請求數超出連接池容量,那麼就排隊等待、退化成短連接或者直接丟棄掉。
二、使用連接池遇到的坑
最近在一個項目中,需要實現一個簡單的 Web Server 提供 Redis 的 HTTP interface,提供 JSON 形式的返回結果。考慮用 Go 來實現。
首先,去看一下 Redis 官方推薦的 Go Redis driver。官方 Star 的項目有兩個:Radix.v2 和 Redigo。經過簡單的比較後,選擇了更加輕量級和實現更加優雅的 Radix.v2。
Radix.v2 包是根據功能劃分成一個個的 sub package,每一個 sub package 在一個獨立的子目錄中,結構非常清晰。我的項目中會用到的 sub package 有 redis 和 pool。
由於我想讓這種被 fork 的進程最好簡單點,做的事情單一一些,所以,在沒有深入去看 Radix.v2 的 pool 的實現之前,我選擇了自己實現一個 Redis pool。(這裡,就不貼代碼了。後來發現自己實現的 Redis pool 與 Radix.v2 實現的 Redis pool 的原理是一樣的,都是基於 channel 實現的, 遇到的問題也是一樣的。)
不過在測試過程中,發現了一個詭異的問題。在請求過程中經常會報 EOF 錯誤。而且是概率性出現,一會有問題,一會又好了。通過反覆的測試,發現 bug 是有規律的,當程序空閑一會後,再進行連續請求,會發生3次失敗,然後之後的請求都能成功,而我的連接池大小設置的是3。再進一步分析,程序空閑300秒 後,再請求就會失敗,發現我的 Redis server 配置了 timeout 300,至此,問題就清楚了。是連接超時 Redis server 主動斷開了連接。客戶端這邊從一個超時的連接請求就會得到 EOF 錯誤。
然後我看了一下 Radix.v2 的 pool 包的源碼,發現這個庫本身並沒有檢測壞的連接,並替換為新server{location/pool{content_by_lua_block{localredis=require”resty.redis”localred=redis:new()localok,err=red:connect(“127.0.0.1”,6379)ifnotokthenngx.say(“failedtoconnect:”,err)returnendok,err=red:set(“hello”,”world”)ifnotokthenreturnendred:set_keepalive(10000,100)}}}
發現有個 set_keepalive 的方法,查了一下官方文檔,方法的原型是 syntax: ok, err = red:set_keepalive(max_idle_timeout, pool_size) 貌似 max_idle_timeout 這個參數,就是我們所缺少的東西,然後進一步跟蹤源碼,看看裡面是怎麼保證連接有效的。
function_M.set_keepalive(self,…)localsock=self.sockifnotsockthenreturnnil,”notinitialized”endifself.subscribedthenreturnnil,”subscribedstate”endreturnsock:setkeepalive(…)end
至此,已經清楚了,使用了 tcp 的 keepalive 心跳機制。
於是,通過與 Radix.v2 的作者一些討論,選擇自己在 redis 這層使用心跳機制,來解決這個問題。
四、最後的解決方案
在創建連接池之後,起一個 goroutine,每隔一段 idleTime 發送一個 PING 到 Redis server。其中,idleTime 略小於 Redis server 的 timeout 配置。連接池初始化部分代碼如下:
p,err:=pool.New(“tcp”,u.Host,concurrency)errHndlr(err)gofunc(){for{p.Cmd(“PING”)time.Sleep(idelTime*time.Second)}}()
使用 redis 傳輸數據部分代碼如下:
funcredisDo(p*pool.Pool,cmdstring,args…interface{})(reply*redis.Resp,errerror){reply=p.Cmd(cmd,args…)iferr=reply.Err;err!=nil{iferr!=io.EOF{Fatal.Println(“redis”,cmd,args,”erris”,err)}}return}
其中,Radix.v2 連接池內部進行了連接池內連接的獲取和放回,代碼如下:
//Cmdautomaticallygetsoneclientfromthepool,executesthegivencommand//(returningitsresult),andputstheclientbackinthepoolfunc(p*Pool)Cmd(cmdstring,args…interface{})*redis.Resp{c,err:=p.Get()iferr!=nil{returnredis.NewResp(err)}deferp.Put(c)returnc.Cmd(cmd,args…)}
這樣,我們就有了 keepalive 的機制,不會出現 timeout 的連接了,從 redis 連接池裡面取出的連接都是可用的連接了。看似簡單的代碼,卻完美的解決了連接池裡面超時連接的問題。同時,就算 Redis server 重啟等情況,也能保證連接自動重連。
原創文章,作者:WDBPX,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/316654.html