重慶分公司,新征程啟航
為企業提供網站建設、域名注冊、服務器等服務
為企業提供網站建設、域名注冊、服務器等服務
在go http每一次go serve(l)都會構建Request數據結構。在大量數據請求或高并發的場景中,頻繁創建銷毀對象,會導致GC壓力。解決辦法之一就是使用對象復用技術。在http協議層之下,使用對象復用技術創建Request數據結構。在http協議層之上,可以使用對象復用技術創建(w,*r,ctx)數據結構。這樣即可以回快TCP層讀包之后的解析速度,也可也加快請求處理的速度。
榮昌網站制作公司哪家好,找創新互聯!從網頁設計、網站建設、微信開發、APP開發、成都響應式網站建設公司等網站項目制作,到程序開發,運營維護。創新互聯于2013年創立到現在10年的時間,我們擁有了豐富的建站經驗和運維經驗,來保證我們的工作的順利進行。專注于網站建設就選創新互聯。
先上一個測試:
結論是這樣的:
貌似使用池化,性能弱爆了???這似乎與net/http使用sync.pool池化Request來優化性能的選擇相違背。這同時也說明了一個問題,好的東西,如果濫用反而造成了性能成倍的下降。在看過pool原理之后,結合實例,將給出正確的使用方法,并給出預期的效果。
sync.Pool是一個 協程安全 的 臨時對象池 。數據結構如下:
local 成員的真實類型是一個 poolLocal 數組,localSize 是數組長度。這涉及到Pool實現,pool為每個P分配了一個對象,P數量設置為runtime.GOMAXPROCS(0)。在并發讀寫時,goroutine綁定的P有對象,先用自己的,沒有去偷其它P的。go語言將數據分散在了各個真正運行的P中,降低了鎖競爭,提高了并發能力。
不要習慣性地誤認為New是一個關鍵字,這里的New是Pool的一個字段,也是一個閉包名稱。其API:
如果不指定New字段,對象池為空時會返回nil,而不是一個新構建的對象。Get()到的對象是隨機的。
原生sync.Pool的問題是,Pool中的對象會被GC清理掉,這使得sync.Pool只適合做簡單地對象池,不適合作連接池。
pool創建時不能指定大小,沒有數量限制。pool中對象會被GC清掉,只存在于兩次GC之間。實現是pool的init方法注冊了一個poolCleanup()函數,這個方法在GC之前執行,清空pool中的所有緩存對象。
為使多協程使用同一個POOL。最基本的想法就是每個協程,加鎖去操作共享的POOL,這顯然是低效的。而進一步改進,類似于ConcurrentHashMap(JDK7)的分Segment,提高其并發性可以一定程度性緩解。
注意到pool中的對象是無差異性的,加鎖或者分段加鎖都不是較好的做法。go的做法是為每一個綁定協程的P都分配一個子池。每個子池又分為私有池和共享列表。共享列表是分別存放在各個P之上的共享區域,而不是各個P共享的一塊內存。協程拿自己P里的子池對象不需要加鎖,拿共享列表中的就需要加鎖了。
Get對象過程:
Put過程:
如何解決Get最壞情況遍歷所有P才獲取得對象呢:
方法1止前sync.pool并沒有這樣的設置。方法2由于goroutine被分配到哪個P由調度器調度不可控,無法確保其平衡。
由于不可控的GC導致生命周期過短,且池大小不可控,因而不適合作連接池。僅適用于增加對象重用機率,減少GC負擔。2
執行結果:
單線程情況下,遍歷其它無元素的P,長時間加鎖性能低下。啟用協程改善。
結果:
測試場景在goroutines遠大于GOMAXPROCS情況下,與非池化性能差異巨大。
測試結果
可以看到同樣使用*sync.pool,較大池大小的命中率較高,性能遠高于空池。
結論:pool在一定的使用條件下提高并發性能,條件1是協程數遠大于GOMAXPROCS,條件2是池中對象遠大于GOMAXPROCS。歸結成一個原因就是使對象在各個P中均勻分布。
池pool和緩存cache的區別。池的意思是,池內對象是可以互換的,不關心具體值,甚至不需要區分是新建的還是從池中拿出的。緩存指的是KV映射,緩存里的值互不相同,清除機制更為復雜。緩存清除算法如LRU、LIRS緩存算法。
池空間回收的幾種方式。一些是GC前回收,一些是基于時鐘或弱引用回收。最終確定在GC時回收Pool內對象,即不回避GC。用java的GC解釋弱引用。GC的四種引用:強引用、弱引用、軟引用、虛引用。虛引用即沒有引用,弱引用GC但有空間則保留,軟引用GC即清除。ThreadLocal的值為弱引用的例子。
regexp 包為了保證并發時使用同一個正則,而維護了一組狀態機。
fmt包做字串拼接,從sync.pool拿[]byte對象。避免頻繁構建再GC效率高很多。
安裝:
go get -v -u github.com/rocket049/connpool
go get -v -u gitee.com/rocket049/connpool
rocket049/connpool 包是本人用go語言開發的,提供一個通用的TCP連接池,初始化參數包括最高連接數、超時秒數、連接函數,放回連接池的連接被重新取出時,如果已經超時,將會自動重新連接;如果沒有超時,連接將被復用。
可調用的函數:
調用示例:
1.在創建連接池之后,起一個 go routine,每隔一段 idleTime 發送一個 PING 到 Redis server。其中,idleTime 略小于 Redis server 的 timeout 配置。
2.連接池初始化部分代碼如下:
p, err := pool.New("tcp", u.Host, concurrency) errHndlr(err) go func() { for { p.Cmd("PING") time.Sleep(idelTime * time.Second) } }()
3.使用 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 }
4.其中,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...) }
這樣,就有了系統 keep alive 的機制,不會出現 time out 的連接了,從 redis 連接池里面取出的連接都是可用的連接了。看似簡單的代碼,卻完美的解決了連接池里面超時連接的問題。同時,就算 Redis server 重啟等情況,也能保證連接自動重連。
一、關于連接池
一個數據庫服務器只擁有有限的資源,并且如果你沒有充分使用這些資源,你可以通過使用更多的連接來提高吞吐量。一旦所有的資源都在使用,那么你就不 能通過增加更多的連接來提高吞吐量。事實上,吞吐量在連接負載較大時就開始下降了。通常可以通過限制與可用的資源相匹配的數據庫連接的數量來提高延遲和吞 吐量。
如何在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 重啟等情況,也能保證連接自動重連。
你需要設置 GOPATH 環境變量
你 main.go 中需要 import 相應包
你 調用處需要帶上包前綴,比如 tempconv.FToC