go語言math包里面定義了min/max函數(shù),但是是float64類型的,而并沒有整數(shù)類型的min/max。
創(chuàng)新互聯(lián)專注于企業(yè)全網(wǎng)營銷推廣、網(wǎng)站重做改版、花溪網(wǎng)站定制設(shè)計、自適應(yīng)品牌網(wǎng)站建設(shè)、html5、商城網(wǎng)站開發(fā)、集團(tuán)公司官網(wǎng)建設(shè)、外貿(mào)網(wǎng)站制作、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁設(shè)計等建站業(yè)務(wù),價格優(yōu)惠性價比高,為花溪等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。
因?yàn)間o沒有重載,這是個大坑。所以math庫里min/max函數(shù)都只能定義一個,所以官方選擇了比較難實(shí)現(xiàn)的float64類型。而簡單的整形就需要讓程序員自己實(shí)現(xiàn)了
const修飾的數(shù)據(jù)類型是指常類型,常類型的變量或?qū)ο蟮闹凳遣荒鼙桓碌?。const關(guān)鍵字的作用主要有以下幾點(diǎn):(1)可以定義const常量,具有不可變性。例如:constintMax=100;intArray[Max];(2)便于進(jìn)行類型檢查,使編譯器對處理內(nèi)容有了解,消除了一些隱患。例如:voidf(constinti){}編譯器就會知道i是一個常量,不允許修改;(3)可以避免意義模糊的數(shù)字出現(xiàn),同樣可以很方便地進(jìn)行參數(shù)的調(diào)整和修改。(4)可以保護(hù)被修飾的東西,防止意外的修改,增強(qiáng)程序的健壯性。還是上面的例子,如果在函數(shù)體內(nèi)修改了i,編譯器就會報錯;例如:voidf(constinti){i=10;//error!}(5)為函數(shù)重載提供了一個參考。classA{voidf(inti){}//一個函數(shù)voidf(inti)const{}//上一個函數(shù)的重載};(6)可以節(jié)省空間,避免不必要的內(nèi)存分配。例如:#definePI3.14159//常量宏constdoulbePi=3.14159;//此時并未將Pi放入ROM中doublei=Pi;//此時為Pi分配內(nèi)存,以后不再分配!doubleI=PI;//編譯期間進(jìn)行宏替換,分配內(nèi)存doublej=Pi;//沒有內(nèi)存分配doubleJ=PI;//再進(jìn)行宏替換,又一次分配內(nèi)存!const定義常量從匯編的角度來看,只是給出了對應(yīng)的內(nèi)存地址,而不是象#define一樣給出的是立即數(shù),所以,const定義的常量在程序運(yùn)行過程中只有一份拷貝,而#define定義的常量在內(nèi)存中有若干個拷貝。(7)提高了效率。編譯器通常不為普通const常量分配存儲空間,而是將它們保存在符號表中,這使得它成為一個編譯期間的常量,沒有了存儲與讀內(nèi)存的操作,使得它的效率也很高。
本文介紹一些Go語言的基礎(chǔ)語法。
先來看一個簡單的go語言代碼:
go語言的注釋方法:
代碼執(zhí)行結(jié)果:
下面來進(jìn)一步介紹go的基礎(chǔ)語法。
go語言中格式化輸出可以使用 fmt 和 log 這兩個標(biāo)準(zhǔn)庫,
常用方法:
示例代碼:
執(zhí)行結(jié)果:
更多格式化方法可以訪問中的fmt包。
log包實(shí)現(xiàn)了簡單的日志服務(wù),也提供了一些格式化輸出的方法。
執(zhí)行結(jié)果:
下面來介紹一下go的數(shù)據(jù)類型
下表列出了go語言的數(shù)據(jù)類型:
int、float、bool、string、數(shù)組和struct屬于值類型,這些類型的變量直接指向存在內(nèi)存中的值;slice、map、chan、pointer等是引用類型,存儲的是一個地址,這個地址存儲最終的值。
常量是在程序編譯時就確定下來的值,程序運(yùn)行時無法改變。
執(zhí)行結(jié)果:
執(zhí)行結(jié)果:
Go 語言的運(yùn)算符主要包括算術(shù)運(yùn)算符、關(guān)系運(yùn)算符、邏輯運(yùn)算符、位運(yùn)算符、賦值運(yùn)算符以及指針相關(guān)運(yùn)算符。
算術(shù)運(yùn)算符:
關(guān)系運(yùn)算符:
邏輯運(yùn)算符:
位運(yùn)算符:
賦值運(yùn)算符:
指針相關(guān)運(yùn)算符:
下面介紹一下go語言中的if語句和switch語句。另外還有一種控制語句叫select語句,通常與通道聯(lián)用,這里不做介紹。
if語法格式如下:
if ... else :
else if:
示例代碼:
語法格式:
另外,添加 fallthrough 會強(qiáng)制執(zhí)行后面的 case 語句,不管下一條case語句是否為true。
示例代碼:
執(zhí)行結(jié)果:
下面介紹幾種循環(huán)語句:
執(zhí)行結(jié)果:
執(zhí)行結(jié)果:
也可以通過標(biāo)記退出循環(huán):
--THE END--
一、關(guān)于連接池
一個數(shù)據(jù)庫服務(wù)器只擁有有限的資源,并且如果你沒有充分使用這些資源,你可以通過使用更多的連接來提高吞吐量。一旦所有的資源都在使用,那么你就不 能通過增加更多的連接來提高吞吐量。事實(shí)上,吞吐量在連接負(fù)載較大時就開始下降了。通常可以通過限制與可用的資源相匹配的數(shù)據(jù)庫連接的數(shù)量來提高延遲和吞 吐量。
如何在Go語言中使用Redis連接池
如果不使用連接池,那么,每次傳輸數(shù)據(jù),我們都需要進(jìn)行創(chuàng)建連接,收發(fā)數(shù)據(jù),關(guān)閉連接。在并發(fā)量不高的場景,基本上不會有什么問題,一旦并發(fā)量上去了,那么,一般就會遇到下面幾個常見問題:
性能普遍上不去
CPU 大量資源被系統(tǒng)消耗
網(wǎng)絡(luò)一旦抖動,會有大量 TIME_WAIT 產(chǎn)生,不得不定期重啟服務(wù)或定期重啟機(jī)器
服務(wù)器工作不穩(wěn)定,QPS 忽高忽低
要想解決這些問題,我們就要用到連接池了。連接池的思路很簡單,在初始化時,創(chuàng)建一定數(shù)量的連接,先把所有長連接存起來,然后,誰需要使用,從這里取走,干完活立馬放回來。 如果請求數(shù)超出連接池容量,那么就排隊等待、退化成短連接或者直接丟棄掉。
二、使用連接池遇到的坑
最近在一個項(xiàng)目中,需要實(shí)現(xiàn)一個簡單的 Web Server 提供 Redis 的 HTTP interface,提供 JSON 形式的返回結(jié)果??紤]用 Go 來實(shí)現(xiàn)。
首先,去看一下 Redis 官方推薦的 Go Redis driver。官方 Star 的項(xiàng)目有兩個:Radix.v2 和 Redigo。經(jīng)過簡單的比較后,選擇了更加輕量級和實(shí)現(xiàn)更加優(yōu)雅的 Radix.v2。
Radix.v2 包是根據(jù)功能劃分成一個個的 sub package,每一個 sub package 在一個獨(dú)立的子目錄中,結(jié)構(gòu)非常清晰。我的項(xiàng)目中會用到的 sub package 有 redis 和 pool。
由于我想讓這種被 fork 的進(jìn)程最好簡單點(diǎn),做的事情單一一些,所以,在沒有深入去看 Radix.v2 的 pool 的實(shí)現(xiàn)之前,我選擇了自己實(shí)現(xiàn)一個 Redis pool。(這里,就不貼代碼了。后來發(fā)現(xiàn)自己實(shí)現(xiàn)的 Redis pool 與 Radix.v2 實(shí)現(xiàn)的 Redis pool 的原理是一樣的,都是基于 channel 實(shí)現(xiàn)的, 遇到的問題也是一樣的。)
不過在測試過程中,發(fā)現(xiàn)了一個詭異的問題。在請求過程中經(jīng)常會報 EOF 錯誤。而且是概率性出現(xiàn),一會有問題,一會又好了。通過反復(fù)的測試,發(fā)現(xiàn) bug 是有規(guī)律的,當(dāng)程序空閑一會后,再進(jìn)行連續(xù)請求,會發(fā)生3次失敗,然后之后的請求都能成功,而我的連接池大小設(shè)置的是3。再進(jìn)一步分析,程序空閑300秒 后,再請求就會失敗,發(fā)現(xiàn)我的 Redis server 配置了 timeout 300,至此,問題就清楚了。是連接超時 Redis server 主動斷開了連接。客戶端這邊從一個超時的連接請求就會得到 EOF 錯誤。
然后我看了一下 Radix.v2 的 pool 包的源碼,發(fā)現(xiàn)這個庫本身并沒有檢測壞的連接,并替換為新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)}}}
發(fā)現(xiàn)有個 set_keepalive 的方法,查了一下官方文檔,方法的原型是 syntax: ok, err = red:set_keepalive(max_idle_timeout, pool_size) 貌似 max_idle_timeout 這個參數(shù),就是我們所缺少的東西,然后進(jìn)一步跟蹤源碼,看看里面是怎么保證連接有效的。
function_M.set_keepalive(self,...)localsock=self.sockifnotsockthenreturnnil,"notinitialized"endifself.subscribedthenreturnnil,"subscribedstate"endreturnsock:setkeepalive(...)end
至此,已經(jīng)清楚了,使用了 tcp 的 keepalive 心跳機(jī)制。
于是,通過與 Radix.v2 的作者一些討論,選擇自己在 redis 這層使用心跳機(jī)制,來解決這個問題。
四、最后的解決方案
在創(chuàng)建連接池之后,起一個 goroutine,每隔一段 idleTime 發(fā)送一個 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 傳輸數(shù)據(jù)部分代碼如下:
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 連接池內(nèi)部進(jìn)行了連接池內(nèi)連接的獲取和放回,代碼如下:
//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 的機(jī)制,不會出現(xiàn) timeout 的連接了,從 redis 連接池里面取出的連接都是可用的連接了??此坪唵蔚拇a,卻完美的解決了連接池里面超時連接的問題。同時,就算 Redis server 重啟等情況,也能保證連接自動重連。
簡單來說, SetMaxHeap 提供了一種可以設(shè)置固定觸發(fā)閾值的 GC (Garbage Collection垃圾回收)方式
官方源碼鏈接
大量臨時對象分配導(dǎo)致的 GC 觸發(fā)頻率過高, GC 后實(shí)際存活的對象較少,
或者機(jī)器內(nèi)存較充足,希望使用剩余內(nèi)存,降低 GC 頻率的場景
GC 會 STW ( Stop The World ),對于時延敏感場景,在一個周期內(nèi)連續(xù)觸發(fā)兩輪 GC ,那么 STW 和 GC 占用的 CPU 資源都會造成很大的影響, SetMaxHeap 并不一定是完美的,在某些場景下做了些權(quán)衡,官方也在進(jìn)行相關(guān)的實(shí)驗(yàn),當(dāng)前方案仍沒有合入主版本。
先看下如果沒有 SetMaxHeap ,對于如上所述的場景的解決方案
這里簡單說下 GC 的幾個值的含義,可通過 GODEBUG=gctrace=1 獲得如下數(shù)據(jù)
這里只關(guān)注 128-132-67 MB 135 MB goal ,
分別為 GC開始時內(nèi)存使用量 - GC標(biāo)記完成時內(nèi)存使用量 - GC標(biāo)記完成時的存活內(nèi)存量 本輪GC標(biāo)記完成時的 預(yù)期 內(nèi)存使用量(上一輪 GC 完成時確定)
引用 GC peace設(shè)計文檔 中的一張圖來說明
對應(yīng)關(guān)系如下:
簡單說下 GC pacing (信用機(jī)制)
GC pacing 有兩個目標(biāo),
那么當(dāng)一輪 GC 完成時,如何只根據(jù)本輪 GC 存活量去實(shí)現(xiàn)這兩個小目標(biāo)呢?
這里實(shí)際是根據(jù)當(dāng)前的一些數(shù)據(jù)或狀態(tài)去 預(yù)估 “未來”,所有會存在些誤差
首先確定 gc Goal goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100
heap_marked 為本輪 GC 存活量, gcpercent 默認(rèn)為 100 ,可以通過環(huán)境變量 GOGC=100 或者 debug.SetGCPercent(100) 來設(shè)置
那么默認(rèn)情況下 goal = 2 * heap_marked
gc_trigger 是與 goal 相關(guān)的一個值( gc_trigger 大約為 goal 的 90% 左右),每輪 GC 標(biāo)記完成時,會根據(jù) |Ha-Hg| 和實(shí)際使用的 cpu 資源 動態(tài)調(diào)整 gc_trigger 與 goal 的差值
goal 與 gc_trigger 的差值即為,為 GC 期間分配的對象所預(yù)留的空間
GC pacing 還會預(yù)估下一輪 GC 發(fā)生時,需要掃描對象對象的總量,進(jìn)而換算為下一輪 GC 所需的工作量,進(jìn)而計算出 mark assist 的值
本輪 GC 觸發(fā)( gc_trigger ),到本輪的 goal 期間,需要盡力完成 GC mark 標(biāo)記操作,所以當(dāng) GC 期間,某個 goroutine 分配大量內(nèi)存時,就會被拉去做 mark assist 工作,先進(jìn)行 GC mark 標(biāo)記賺取足夠的信用值后,才能分配對應(yīng)大小的對象
根據(jù)本輪 GC 存活的內(nèi)存量( heap_marked )和下一輪 GC 觸發(fā)的閾值( gc_trigger )計算 sweep assist 的值,本輪 GC 完成,到下一輪 GC 觸發(fā)( gc_trigger )時,需要盡力完成 sweep 清掃操作
預(yù)估下一輪 GC 所需的工作量的方式如下:
繼續(xù)分析文章開頭的問題,如何充分利用剩余內(nèi)存,降低 GC 頻率和 GC 對 CPU 的資源消耗
如上圖可以看出, GC 后,存活的對象為 2GB 左右,如果將 gcpercent 設(shè)置為 400 ,那么就可以將下一輪 GC 觸發(fā)閾值提升到 10GB 左右
前面一輪看起來很好,提升了 GC 觸發(fā)的閾值到 10GB ,但是如果某一輪 GC 后的存活對象到達(dá) 2.5GB 的時候,那么下一輪 GC 觸發(fā)的閾值,將會超過內(nèi)存閾值,造成 OOM ( Out of Memory ),進(jìn)而導(dǎo)致程序崩潰。
可以通過 GOGC=off 或者 debug.SetGCPercent(-1) 來關(guān)閉 GC
可以通過進(jìn)程外監(jiān)控內(nèi)存使用狀態(tài),使用信號觸發(fā)的方式通知程序,或 ReadMemStats 、或 linkname runtime.heapRetained 等方式進(jìn)行堆內(nèi)存使用的監(jiān)測
可以通過調(diào)用 runtime.GC() 或者 debug.FreeOSMemory() 來手動進(jìn)行 GC 。
這里還需要說幾個事情來解釋這個方案所存在的問題
通過 GOGC=off 或者 debug.SetGCPercent(-1) 是如何關(guān)閉 GC 的?
gc 4 @1.006s 0%: 0.033+5.6+0.024 ms clock, 0.27+4.4/11/25+0.19 ms cpu, 428-428-16 MB, 17592186044415 MB goal, 8 P (forced)
通過 GC trace 可以看出,上面所說的 goal 變成了一個很詭異的值 17592186044415
實(shí)際上關(guān)閉 GC 后, Go 會將 goal 設(shè)置為一個極大值 ^uint64(0) ,那么對應(yīng)的 GC 觸發(fā)閾值也被調(diào)成了一個極大值,這種處理方式看起來也沒什么問題,將閾值調(diào)大,預(yù)期永遠(yuǎn)不會再觸發(fā) GC
那么如果在關(guān)閉 GC 的情況下,手動調(diào)用 runtime.GC() 會導(dǎo)致什么呢?
由于 goal 和 gc_trigger 被設(shè)置成了極大值, mark assist 和 sweep assist 也會按照這個錯誤的值去計算,導(dǎo)致工作量預(yù)估錯誤,這一點(diǎn)可以從 trace 中進(jìn)行證明
可以看到很詭異的 trace 圖,這里不做深究,該方案與 GC pacing 信用機(jī)制不兼容
記住,不要在關(guān)閉 GC 的情況下手動觸發(fā) GC ,至少在當(dāng)前 Go1.14 版本中仍存在這個問題
SetMaxHeap 的實(shí)現(xiàn)原理,簡單來說是強(qiáng)行控制了 goal 的值
注: SetMaxHeap ,本質(zhì)上是一個軟限制,并不能解決 極端場景 下的 OOM ,可以配合內(nèi)存監(jiān)控和 debug.FreeOSMemory() 使用
SetMaxHeap 控制的是堆內(nèi)存大小, Go 中除了堆內(nèi)存還分配了如下內(nèi)存,所以實(shí)際使用過程中,與實(shí)際硬件內(nèi)存閾值之間需要留有一部分余量。
對于文章開始所述問題,使用 SetMaxHeap 后,預(yù)期的 GC 過程大概是這個樣子
簡單用法1
該方法簡單粗暴,直接將 goal 設(shè)置為了固定值
注:通過上文所講,觸發(fā) GC 實(shí)際上是 gc_trigger ,所以當(dāng)閾值設(shè)置為 12GB 時,會提前一點(diǎn)觸發(fā) GC ,這里為了描述方便,近似認(rèn)為 gc_trigger=goal
簡單用法2
當(dāng)不關(guān)閉 GC 時, SetMaxHeap 的邏輯是, goal 仍按照 gcpercent 進(jìn)行計算,當(dāng) goal 小于 SetMaxHeap 閾值時不進(jìn)行處理;當(dāng) goal 大于 SetMaxHeap 閾值時,將 goal 限制為 SetMaxHeap 閾值
注:通過上文所講,觸發(fā) GC 實(shí)際上是 gc_trigger ,所以當(dāng)閾值設(shè)置為 12GB 時,會提前一點(diǎn)觸發(fā) GC ,這里為了描述方便,近似認(rèn)為 gc_trigger=goal
切換到 go1.14 分支,作者選擇了 git checkout go1.14.5
選擇官方提供的 cherry-pick 方式(可能需要梯子,文件改動不多,我后面會列出具體改動)
git fetch "" refs/changes/67/227767/3 git cherry-pick FETCH_HEAD
需要重新編譯Go源碼
注意點(diǎn):
下面源碼中的官方注釋說的比較清楚,在一些關(guān)鍵位置加入了中文注釋
入?yún)ytes為要設(shè)置的閾值
notify 簡單理解為 GC 的策略 發(fā)生變化時會向 channel 發(fā)送通知,后續(xù)源碼可以看出“策略”具體指哪些內(nèi)容
返回值為本次設(shè)置之前的 MaxHeap 值
$GOROOT/src/runtime/debug/garbage.go
$GOROOT/src/runtime/mgc.go
注:作者盡量用通俗易懂的語言去解釋 Go 的一些機(jī)制和 SetMaxHeap 功能,可能有些描述與實(shí)現(xiàn)細(xì)節(jié)不完全一致,如有錯誤還請指出