如何在 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 包的原始碼,發現這個庫本身並沒有檢測壞的連線,並替換為新的連線的機制。也就是說我每次從連線池裡面 Get 的連線有可能是壞的連線。所以,我當時臨時的解決方案是通過增加失敗後自動重試來解決了。不過,這樣的處理方案,連線池的作用好像就沒有了。技術債能早點還的還是早點還上。
三、使用連線池的正確姿勢
想到我們的 ngx_lua 專案裡面也大量使用 redis 連線池,他們怎麼沒有遇到這個問題呢。只能去看看原始碼了。
經過抽象分離, ngx_lua 裡面使用 redis 連線池部分的程式碼大致是這樣的:
server { location /pool { content_by_lua_block { local redis = require "resty.redis" local red = redis:new() local ok, err = red:connect("127.0.0.1", 6379) if not ok then ngx.say("failed to connect: ", err) return end ok, err = red:set("hello", "world") if not ok then return end red: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, ...) local sock = self.sock if not sock then return nil, "not initialized" end if self.subscribed then return nil, "subscribed state" end return sock: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) go func() { for { p.Cmd("PING") time.Sleep(idelTime * time.Second) } }()
使用 redis 傳輸資料部分程式碼如下:
func redisDo(p *pool.Pool, cmd string, args ...interface{}) (reply *redis.Resp, err error) { reply = p.Cmd(cmd, args...) if err = reply.Err; err != nil { if err != io.EOF { Fatal.Println("redis", cmd, args, "err is", err) } } return }
其中,Radix.v2 連線池內部進行了連線池內連線的獲取和放回,程式碼如下:
// Cmd automatically gets one client from the pool, executes the given command // (returning its result), and puts the client back in the pool func (p *Pool) Cmd(cmd string, args ...interface{}) *redis.Resp { c, err := p.Get() if err != nil { return redis.NewResp(err) } defer p.Put(c) return c.Cmd(cmd, args...) }
這樣,我們就有了 keepalive 的機制,不會出現 timeout 的連線了,從 redis 連線池裡面取出的連線都是可用的連線了。看似簡單的程式碼,卻完美的解決了連線池裡面超時連線的問題。同時,就算 Redis server 重啟等情況,也能保證連線自動重連。
相關文章
- go 語言連線池Go
- Go連線池Go
- Jedis使用連線池操作redis叢集Redis
- Go 語言中的 collect 使用Go
- Go語言之從0到1實現一個簡單的Redis連線池GoRedis
- Go 語言中使用 ETCDGo
- 論go語言中goroutine的使用Go
- go~連線redis的方法GoRedis
- 【Azure Redis 快取 Azure Cache For Redis】Redis連線池Redis快取
- java操作redis叢集連線池JavaRedis
- 實現一個redis連線池Redis
- mysql、redis 客戶端連線池MySqlRedis客戶端
- Redis筆記2:Jedis連線池Redis筆記
- ServiceStack.Redis的原始碼分析(連線與連線池)Redis原始碼
- golang開發:類庫篇(二) Redis連線池的使用GolangRedis
- Go 語言中 defer 使用時有哪些陷阱?Go
- Go 語言中的方法Go
- Go語言中的InterfaceGo
- proxool連線池如何使用SSL方式連線?
- Tomcat連線池使用Tomcat
- 在 Go 語言中,我為什麼使用介面Go
- Go語言中切片slice的宣告與使用Go
- 在Go語言中使用 Protobuf-RPCGoRPC
- Go 語言中的外掛Go
- Go 語言中的 切片 --sliceGo
- Proxool 連線池的配置使用
- 連線池
- 【JDBC】使用OracleDataSource建立連線池用於連線OracleJDBCOracle
- 如何在C語言中使用tensorflow(五)進階C語言
- 使用telnet連線redisRedis
- 在Go語言中,怎樣使用Json的方法?GoJSON
- Go語言中使用正則提取匹配的字串Go字串
- GO 語言中的物件導向Go物件
- Go語言中的併發模式Go模式
- 資料庫連線池的使用資料庫
- PROXOOL資料庫連線池使用資料庫
- 在JBOSS中使用mysql連線池MySql
- windwos 使用telnet 連線 redisRedis