本文目錄一覽:
- 1、golang map源碼淺析
- 2、Golang database/sql源碼分析
- 3、Golang實驗性功能SetMaxHeap 固定值GC
- 4、golang性能測試框架k6源碼分析
- 5、golang 把中文轉換為首字母的方法
- 6、golang unicode/utf8源碼分析
golang map源碼淺析
golang 中 map的實現結構為: 哈希表 + 鏈表。 其中鏈表,作用是當發生hash衝突時,拉鏈法生成的結點。
可以看到, []bmap 是一個hash table, 每一個 bmap是我們常說的「桶」。 經過hash 函數計算出來相同的hash值, 放到相同的桶中。 一個 bmap中可以存放 8個 元素, 如果多出8個,則生成新的結點,尾接到隊尾。
以上是只是靜態文件 src/runtime/map.go 中的定義。 實際上編譯期間會給它加料 ,動態地創建一個新的結構:
上圖就是 bmap的內存模型, HOB Hash 指的就是 top hash。 注意到 key 和 value 是各自放在一起的,並不是 key/value/key/value/… 這樣的形式。源碼里說明這樣的好處是在某些情況下可以省略掉 padding 欄位,節省內存空間。
每個 bmap設計成 最多只能放 8 個 key-value 對 ,如果有第 9 個 key-value 落入當前的 bmap,那就需要再構建一個 bmap,通過 overflow 指針連接起來。
map創建方法:
我們實際上是通過調用的 makemap ,來創建map的。實際工作只是初始化了hmap中的各種欄位,如:設置B的大小, 設置hash 種子 hash 0.
注意 :
makemap 返回是*hmap 指針, 即 map 是引用對象, 對map的操作會影響到結構體內部 。
使用方式
對應的是下面兩種方法
map的key的類型,實現了自己的hash 方式。每種類型實現hash函數方式不一樣。
key 經過哈希計算後得到hash值,共 64 個 bit 位。 其中後B 個bit位置, 用來定位當前元素落在哪一個桶里, 高8個bit 為當前 hash 值的top hash。 實際上定位key的過程是一個雙重循環的過程, 外層循環遍歷 所有的overflow, 內層循環遍歷 當前bmap 中的 8個元素 。
舉例說明: 如果當前 B 的值為 5, 那麼buckets 的長度 為 2^5 = 32。假設有個key 經過hash函數計算後,得到的hash結果為:
外層遍歷bucket 中的鏈表
內層循環遍歷 bmap中的8個 cell
建議先不看此部分內容,看完後續 修改 map中元素 – 擴容 操作後 再回頭看此部分內容。
擴容前的數據:
等量擴容後的數據:
等量擴容後,查找方式和原本相同, 不多做贅述。
兩倍擴容後的數據
兩倍擴容後,oldbuckets 的元素,可能被分配成了兩部分。查找順序如下:
此處只分析 mapaccess1 ,。 mapaccess2 相比 mapaccess1 多添加了是否找到的bool值, 有興趣可自行看一下。
使用方式:
步驟如下:
擴容條件 :
擴容的標識 : h.oldbuckets != nil
假設當前定位到了新的buckets的3號桶中,首先會判斷oldbuckets中的對應的桶有沒有被搬遷過。 如果搬遷過了,不需要看原來的桶了,直接遍歷新的buckets的3號桶。
擴容前:
等量擴容結果
雙倍擴容會將old buckets上的元素分配到x, y兩個部key 1 B == 0 分配到x部分,key 1 B == 1 分配到y部分
注意: 當前只對雙倍擴容描述, 等量擴容只是重新填充了一下元素, 相對位置沒有改變。
假設當前map 的B == 5,原本元素經過hash函數計算的 hash 值為:
因為雙倍擴容之後 B = B + 1,此時B == 6。key 1 B == 1, 即 當前元素rehash到高位,新buckets中 y 部分. 否則 key 1 B == 0 則rehash到低位,即x 部分。
使用方式:
可以看到,每一遍歷生成迭代器的時候,會隨機選取一個bucket 以及 一個cell開始。 從前往後遍歷,再次遍歷到起始位置時,遍歷完成。
Golang database/sql源碼分析
Gorm是Go語言開發用的比較多的一個ORM。它的功能比較全:
但是這篇文章中並不會直接看Gorm的源碼,我們會先從database/sql分析。原因是Gorm也是基於這個包來封裝的一些功能。所以只有先了解了database/sql包才能更加好的理解Gorm源碼。
database/sql 其實也是一個對於mysql驅動的上層封裝。」github.com/go-sql-driver/mysql」就是一個對於mysql的驅動,database/sql 就是在這個基礎上做的基本封裝包含連接池的使用
下面這個是最基本的增刪改查操作
操作分下面幾個步驟:
因為Gorm的連接池就是使用database/sql包中的連接池,所以這裡我們需要學習一下包里的連接池的源碼實現。其實所有連接池最重要的就是連接池對象、獲取函數、釋放函數下面來看一下database/sql中的連接池。
DB對象
獲取方法
釋放連接方法
連接池的實現有很多方法,在database/sql包中使用的是chan阻塞 使用map記錄等待列表,等到有連接釋放的時候再把連接傳入等待列表中的chan 不在阻塞返回連接。
之前我們看到的Redigo是使用一個chan 來阻塞,然後釋放的時候放入空閑列表,在往這一個chan中傳入struct{}{},讓程序繼續 獲取的時候再從空閑列表中獲取。並且使用的是鏈表的結構來存儲空閑列表。
database/sql 是對於mysql驅動的封裝,然而Gorm則是對於database/sql的再次封裝。讓我們可以更加簡單的實現對於mysql資料庫的操作。
Golang實驗性功能SetMaxHeap 固定值GC
簡單來說, SetMaxHeap 提供了一種可以設置固定觸發閾值的 GC (Garbage Collection垃圾回收)方式
官方源碼鏈接
大量臨時對象分配導致的 GC 觸發頻率過高, GC 後實際存活的對象較少,
或者機器內存較充足,希望使用剩餘內存,降低 GC 頻率的場景
GC 會 STW ( Stop The World ),對於時延敏感場景,在一個周期內連續觸發兩輪 GC ,那麼 STW 和 GC 佔用的 CPU 資源都會造成很大的影響, SetMaxHeap 並不一定是完美的,在某些場景下做了些權衡,官方也在進行相關的實驗,當前方案仍沒有合入主版本。
先看下如果沒有 SetMaxHeap ,對於如上所述的場景的解決方案
這裡簡單說下 GC 的幾個值的含義,可通過 GODEBUG=gctrace=1 獲得如下數據
這裡只關注 128-132-67 MB 135 MB goal ,
分別為 GC開始時內存使用量 – GC標記完成時內存使用量 – GC標記完成時的存活內存量 本輪GC標記完成時的 預期 內存使用量(上一輪 GC 完成時確定)
引用 GC peace設計文檔 中的一張圖來說明
對應關係如下:
簡單說下 GC pacing (信用機制)
GC pacing 有兩個目標,
那麼當一輪 GC 完成時,如何只根據本輪 GC 存活量去實現這兩個小目標呢?
這裡實際是根據當前的一些數據或狀態去 預估 「未來」,所有會存在些誤差
首先確定 gc Goal goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100
heap_marked 為本輪 GC 存活量, gcpercent 默認為 100 ,可以通過環境變數 GOGC=100 或者 debug.SetGCPercent(100) 來設置
那麼默認情況下 goal = 2 * heap_marked
gc_trigger 是與 goal 相關的一個值( gc_trigger 大約為 goal 的 90% 左右),每輪 GC 標記完成時,會根據 |Ha-Hg| 和實際使用的 cpu 資源 動態調整 gc_trigger 與 goal 的差值
goal 與 gc_trigger 的差值即為,為 GC 期間分配的對象所預留的空間
GC pacing 還會預估下一輪 GC 發生時,需要掃描對象對象的總量,進而換算為下一輪 GC 所需的工作量,進而計算出 mark assist 的值
本輪 GC 觸發( gc_trigger ),到本輪的 goal 期間,需要儘力完成 GC mark 標記操作,所以當 GC 期間,某個 goroutine 分配大量內存時,就會被拉去做 mark assist 工作,先進行 GC mark 標記賺取足夠的信用值後,才能分配對應大小的對象
根據本輪 GC 存活的內存量( heap_marked )和下一輪 GC 觸發的閾值( gc_trigger )計算 sweep assist 的值,本輪 GC 完成,到下一輪 GC 觸發( gc_trigger )時,需要儘力完成 sweep 清掃操作
預估下一輪 GC 所需的工作量的方式如下:
繼續分析文章開頭的問題,如何充分利用剩餘內存,降低 GC 頻率和 GC 對 CPU 的資源消耗
如上圖可以看出, GC 後,存活的對象為 2GB 左右,如果將 gcpercent 設置為 400 ,那麼就可以將下一輪 GC 觸發閾值提升到 10GB 左右
前面一輪看起來很好,提升了 GC 觸發的閾值到 10GB ,但是如果某一輪 GC 後的存活對象到達 2.5GB 的時候,那麼下一輪 GC 觸發的閾值,將會超過內存閾值,造成 OOM ( Out of Memory ),進而導致程序崩潰。
可以通過 GOGC=off 或者 debug.SetGCPercent(-1) 來關閉 GC
可以通過進程外監控內存使用狀態,使用信號觸發的方式通知程序,或 ReadMemStats 、或 linkname runtime.heapRetained 等方式進行堆內存使用的監測
可以通過調用 runtime.GC() 或者 debug.FreeOSMemory() 來手動進行 GC 。
這裡還需要說幾個事情來解釋這個方案所存在的問題
通過 GOGC=off 或者 debug.SetGCPercent(-1) 是如何關閉 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
實際上關閉 GC 後, Go 會將 goal 設置為一個極大值 ^uint64(0) ,那麼對應的 GC 觸發閾值也被調成了一個極大值,這種處理方式看起來也沒什麼問題,將閾值調大,預期永遠不會再觸發 GC
那麼如果在關閉 GC 的情況下,手動調用 runtime.GC() 會導致什麼呢?
由於 goal 和 gc_trigger 被設置成了極大值, mark assist 和 sweep assist 也會按照這個錯誤的值去計算,導致工作量預估錯誤,這一點可以從 trace 中進行證明
可以看到很詭異的 trace 圖,這裡不做深究,該方案與 GC pacing 信用機制不兼容
記住,不要在關閉 GC 的情況下手動觸發 GC ,至少在當前 Go1.14 版本中仍存在這個問題
SetMaxHeap 的實現原理,簡單來說是強行控制了 goal 的值
註: SetMaxHeap ,本質上是一個軟限制,並不能解決 極端場景 下的 OOM ,可以配合內存監控和 debug.FreeOSMemory() 使用
SetMaxHeap 控制的是堆內存大小, Go 中除了堆內存還分配了如下內存,所以實際使用過程中,與實際硬體內存閾值之間需要留有一部分餘量。
對於文章開始所述問題,使用 SetMaxHeap 後,預期的 GC 過程大概是這個樣子
簡單用法1
該方法簡單粗暴,直接將 goal 設置為了固定值
註:通過上文所講,觸發 GC 實際上是 gc_trigger ,所以當閾值設置為 12GB 時,會提前一點觸發 GC ,這裡為了描述方便,近似認為 gc_trigger=goal
簡單用法2
當不關閉 GC 時, SetMaxHeap 的邏輯是, goal 仍按照 gcpercent 進行計算,當 goal 小於 SetMaxHeap 閾值時不進行處理;當 goal 大於 SetMaxHeap 閾值時,將 goal 限制為 SetMaxHeap 閾值
註:通過上文所講,觸發 GC 實際上是 gc_trigger ,所以當閾值設置為 12GB 時,會提前一點觸發 GC ,這裡為了描述方便,近似認為 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源碼
注意點:
下面源碼中的官方注釋說的比較清楚,在一些關鍵位置加入了中文注釋
入參bytes為要設置的閾值
notify 簡單理解為 GC 的策略 發生變化時會向 channel 發送通知,後續源碼可以看出「策略」具體指哪些內容
返回值為本次設置之前的 MaxHeap 值
$GOROOT/src/runtime/debug/garbage.go
$GOROOT/src/runtime/mgc.go
註:作者盡量用通俗易懂的語言去解釋 Go 的一些機制和 SetMaxHeap 功能,可能有些描述與實現細節不完全一致,如有錯誤還請指出
golang性能測試框架k6源碼分析
k6是新興的性能測試框架,比肩jmeter,另外測試腳本使用js,更加適合自動化的架構。
k6啟動的框架是使用golang的cli標準框架cobra,入口函數
進入cobra框架後,我們直接查看getRunCmd,這個是命令run的入口,主要工作都是從這裡開始。
重點關注初始化Runner,這個是通過js腳本,使用goja庫解析後,生成的實際執行單元。
進入js目錄,查看Runner的結構,runner.go
Runner有一些配置屬性,另外還有方法,方法用lib.Runner的介面進行規範。
Runner有一個NewVU方法,裡面定義了連接參數,實現api測試
返回主函數,在初始化完成Runner後,啟動調度器,以及做結果收集
最終封裝成一個engine
啟動測試
golang 把中文轉換為首字母的方法
Go語言的string模塊包含了ToLower和ToUpper函數,用於將字元串轉換成小寫和大寫
代碼如下:
package main
import (
“fmt”
“strings”
)
func main() {
fmt.Println(strings.ToUpper(“hello world”))
}
golang unicode/utf8源碼分析
包 utf-8 實現的功能和常量用於文章utf8編碼,包含runes和utf8位元組序列的轉換功能.在unicode中,一個中文佔兩個位元組,utf-8中一個中文佔三個位元組,golang默認的編碼是utf-8編碼,因此默認一個中文佔三個位元組,但是golang中的字元串底層實際上是一個byte數組.
Output:
RuneSelf該值的位元組碼值為128,在判斷是否是常規的ascii碼是使用。hicb位元組碼值為191. FF 的對應的位元組碼為255。
計算字元串中的rune數量,原理:首先取出字元串的碼值,然後判斷是不是個小於128的,如果是小於則直接continue.rune個數++.
如果是個十六進位f1.的則是無效字元,直接continue.rune個數++,也就是說一個無效的字元也當成一個字長為1的rune.如果字元的碼值在first列表中的值和7按位的結果為其字長,比如上面示例中的 鋼 。其字長為三位,第一位的值為 233 .二進位形式為 11101001 ;與7按位與後的值為0.從acceptRanges中取出的結果為{locb, hicb}。也就是標識 ox80 到 0xbf 之間的值。而結果n也就是直接size+3跳過3個位元組後,rune個數++。其他函數的處理流程差不多,不再過多敘述。
示例:
ValidString返回值表明參數字元串是否是一個合法的可utf8編碼的字元串。
RuneCount返回參數中包含的rune數量,第一個例子中將 utf8.RuneCountInString ,改成該方法調用,返回的結果相同。錯誤的和短的被當成一個長一位元組的rune.單個字元 H 就表示一個長度為1位元組的rune.
該函數標識參數是否以一個可編碼的rune開頭,上面的例子中,因為字元串是以一個ascii碼值在0-127內的字元開頭,所以在執行
first[p[0]] 時,取到的是 p[0] 是72,在first列表中,127之前的值都相同都為 0xF0 ,十進位標識為240,與7按位與後值為0,所以,直接返回 true .
和FullRune類似,只是參數為字元串形式
原創文章,作者:ZEUY,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/139235.html