MOSN 源碼分析 - 共享內(nèi)存模型

原文鏈接: https://trainyao.github.io/post/mosn/source_shm/

本文記錄了對 MOSN 的源碼研究 - MOSN 的共享內(nèi)存模型蛾方。

本文的內(nèi)容基于 MOSN v0.9.0单默,commit id b2a239f5温峭。

機(jī)制

MOSN 用共享內(nèi)存來存儲 metrics 信息锰霜。MOSN 用 mmap 將文件映射到內(nèi)存,在內(nèi)存數(shù)組之上封裝了一層關(guān)于 metrics 的存取邏輯区岗,實現(xiàn)了 go-metrics
包的關(guān)于 metrics 的接口矛洞,通過這種方式組裝出了一種基于共享內(nèi)存的 metrics 實現(xiàn)供 MOSN 使用。

創(chuàng)建共享內(nèi)存:Mmap

操作共享內(nèi)存的方法主要在 pkg/shm/shm.go 文件下:

func Alloc(name string, size int) (*ShmSpan, error) {
    ...
    return NewShmSpan(name, data), nil
}

func Free(span *ShmSpan) error {
    Clear(span.name)
    return syscall.Munmap(span.origin)
}

func Clear(name string) error {
    return os.Remove(path(name))
}

都是圍繞著 ShmSpan 結(jié)構(gòu)體的幾個操作方法呕屎。再來看 ShmSpan 結(jié)構(gòu)體:

type ShmSpan struct {
    origin []byte // mmap 返回的數(shù)組
    name   string // span 名, 創(chuàng)建時指定

    data   uintptr // 保存 mmap 內(nèi)存段的首指針
    offset int // span 已經(jīng)使用的字節(jié)長度
    size   int // span 大小
}

Alloc 方法按照給定的 name 參數(shù),在配置文件的目錄下創(chuàng)建文件敬察,并執(zhí)行 sync.Mmap秀睛,其文件尺寸即 size 參數(shù)大小。Mmap 過后莲祸,將信息保存在 ShmSpan結(jié)構(gòu)內(nèi)返回蹂安。

代碼邏輯比較簡單椭迎,大家可以自行閱讀:https://github.com/mosn/mosn/blob/b2a239f5/pkg/shm/shm.go#L28

由此看出,一個 ShmSpan 可以看做是一個共享內(nèi)存塊田盈。

下面我們將會分析共享內(nèi)存塊在 MOSN 里的使用場景:metrics侠碧。

操作共享內(nèi)存:配置

在分析如何通過共享內(nèi)存存取 metrics 之前,首先看這相關(guān)的功能是如何配置的缠黍。

https://github.com/mosn/mosn/blob/b2a239f5/pkg/mosn/starter.go#L318

func initializeMetrics(config v2.MetricsConfig) {
    // init shm zone
    if config.ShmZone != "" && config.ShmSize > 0 {
        shm.InitDefaultMetricsZone(config.ShmZone, int(config.ShmSize), store.GetMosnState() != store.Active_Reconfiguring)
        ...

從這里看出,通過讀取配置文件的 ShmZoneShmSize 來初始化共享內(nèi)存药蜻,即配置文件的以下兩個字段是控制著共享內(nèi)存的文件名和大小的:

{
  ...
  "metrics": {
    ...
    "shm_zone": "文件名",
    "shm_size": "共享內(nèi)存文件大小"
  },
  ...
}

操作共享內(nèi)存:metrics

metrics 相關(guān)的邏輯在 pkg/metrics 包下瓷式。

上文說的 ShmSpan 是保存共享內(nèi)存信息的結(jié)構(gòu)體,而要理解 MOSN metrics 對共享內(nèi)存的使用语泽,還要先理解 MOSN 封裝的幾個結(jié)構(gòu)體:zone贸典、hashSethashEntry

這幾個結(jié)構(gòu)體與 ShmSpan 的關(guān)系大致是這樣的:

shm.png

ShmSpan 是共享內(nèi)存塊踱卵,而 zone廊驼、hashSethashEntryShmSpan 進(jìn)行了劃分:

  • hashSet 封裝出了 metrics name 映射到 metrics value 的哈希表
  • hashEntry 是哈希表的值,也是 metrics 值的保存的共享內(nèi)存空間
  • zoneShmSpan 進(jìn)行了劃分惋砂,劃分出了一個 int32 值作為互斥鎖妒挎;一個 int32 值作為 zone 的引用計數(shù);也劃分出了一片空間保存 hashSet

以上步驟做好后西饵,創(chuàng)建一個 metrics 就可以通過創(chuàng)建對應(yīng)的哈希 key value酝掩,拿到對應(yīng)的共享內(nèi)存地址,存取 metrics 信息眷柔。

下面是源碼步驟期虾,大家可以自行跟蹤調(diào)試:

1) 創(chuàng)建 zone:

https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/zone.go#L81

func newSharedMetrics(name string, size int) (*zone, error) {
    alignedSize := align(size, pageSize)

    // 申請 ShmSpan
    span, err := shm.Alloc(name, alignedSize)
    if err != nil {
        return nil, err
    }
    // 1. mutex and ref
    // 從 span 里取 4 個字節(jié)做互斥鎖
    mutex, err := span.Alloc(4)
    if err != nil {
        return nil, err
    }

    // 從 span 里取 4 個字節(jié)做引用計數(shù)
    ref, err := span.Alloc(4)
    if err != nil {
        return nil, err
    }

    zone := &zone{
        span:  span,
        mutex: (*uint32)(unsafe.Pointer(mutex)),
        ref:   (*uint32)(unsafe.Pointer(ref)),
    }

    // 2. hashSet
    // 劃分哈希表過程

    // assuming that 100 entries with 50 slots, so the ratio of occupied memory is
    // entries:slots  = 100 x 128 : 50 x 4 = 64 : 1
    // so assuming slots memory size is N, total allocated memory size is M, then we have:
    // M - 1024 < 65N + 28 <= M

    // 計算 slot 的數(shù)量和內(nèi)存占用大小
    slotsNum := (alignedSize - 28) / (65 * 4)
    slotsSize := slotsNum * 4
    // 計算 entry 數(shù)量和內(nèi)存占用大小
    entryNum := slotsNum * 2
    entrySize := slotsSize * 64
    
    // 哈希表內(nèi)存大小 = entry 內(nèi)存占用 + 20 字節(jié) + slot 內(nèi)存占用大小
    hashSegSize := entrySize + 20 + slotsSize
    hashSegment, err := span.Alloc(hashSegSize)
    if err != nil {
        return nil, err
    }

    // if zones's ref > 0, no need to initialize hashset's value
    // 初始化哈希表結(jié)構(gòu)
    set, err := newHashSet(hashSegment, hashSegSize, entryNum, slotsNum, atomic.LoadUint32(zone.ref) == 0)
    if err != nil {
        return nil, err
    }
    zone.set = set

    // add ref
    atomic.AddUint32(zone.ref, 1)

    return zone, nil
}

這里可以大致說一下哈希表初始化的算法:首先 alignedSize 表示 4k 對齊后的 ShmSpan 大小,前 8 個字節(jié)被分配為互斥鎖和引用計數(shù)驯嘱,
另外 20 個字節(jié)被分配為哈希表的 meta 結(jié)構(gòu)體镶苞,

https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/hashset.go#L54

type hashSet struct {
    entry []hashEntry
    meta  *meta
    slots []uint32
}

...


type meta struct {
    cap       uint32
    size      uint32
    freeIndex uint32

    slotsNum uint32
    bytesNum uint32
}

所以真正能被分配為哈希表信息儲存的空間 = 總空間 - 8 字節(jié) - 20 字節(jié)。那能分配多少個哈希表信息呢鞠评?要看看 MOSN 的哈希表組織形式:entry 最開始首尾相連茂蚓,后面會被組織成一個一個的 slot 鏈表供哈希碰撞時遍歷查詢。
所以 slot 和 entry 的比例控制著哈希表查找的性能:entry 比 slot 作為比例的話谢澈,比例越高意味著更容易碰撞煌贴,鏈表越長,查找性能下降锥忿;相反比例越低鏈表越短牛郑,查找性能越高,但是有越多 slot 閑置敬鬓,空間會浪費淹朋。

從注釋看笙各,MOSN 將比例寫死為 2:1。假設(shè) 100 個 entry + 50 個 slot础芍,其內(nèi)存比等于 100 * 128(entry 內(nèi)存占用):50 * 4 = 64:1杈抢,即一份 2:1 的 entry+slot 需要用到(64 + 1)* 4 個字節(jié)。

所以仑性,如果按照 2:1 來分配的話惶楼,一共可以分配的份數(shù) = 哈希表信息儲存空間 / 每一份空間占用 = (總空間 - 8 - 20) / (64 + 1) * 4 份。由此就可以算出 entry 和 slot 可以分配多少份了诊杆。

2) 創(chuàng)建指標(biāo):

https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/counter.go#L56

func NewShmCounterFunc(name string) func() gometrics.Counter {
    return func() gometrics.Counter {
        if defaultZone != nil {
            if entry, err := defaultZone.alloc(name); err == nil {
            ...

https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/zone.go#L166

func (z *zone) alloc(name string) (*hashEntry, error) {
    z.lock()
    defer z.unlock()

    entry, create := z.set.Alloc(name)
    ...

https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/hashset.go#L135

func (s *hashSet) Alloc(name string) (*hashEntry, bool) {
    // 1. search existed slots and entries
    // 計算 hash 值作為 slot index
    h := hash(name)
    slot := h % s.meta.slotsNum

    // name convert if length exceeded
    if len(name) > maxNameLength {
        // if name is longer than max length, use hash_string as leading character
        // and the remaining maxNameLength - len(hash_string) bytes follows
        hStr := strconv.Itoa(int(h))
        name = hStr + name[len(hStr)+len(name)-maxNameLength:]
    }

    nameBytes := []byte(name)

    // 查找鏈表找到對應(yīng)的 entry
    var entry *hashEntry
    for index := s.slots[slot]; index != sentinel; {
        entry = &s.entry[index]

        if entry.equalName(nameBytes) {
            return entry, false
        }

        index = entry.next
    }

    // 2. create new entry
    // 如果找不到, 創(chuàng)建新的 entry
    if s.meta.size >= s.meta.cap {
        return nil, false
    }

    // 創(chuàng)建新的 entry 從 hashset 的 meta 信息里拿 next free index
    newIndex := s.meta.freeIndex
    newEntry := &s.entry[newIndex]
    newEntry.assignName(nameBytes)
    newEntry.ref = 1

    if entry == nil {
        // 所以是鏈表頭,保存 index 到 slot
        s.slots[slot] = newIndex
    } else {
        // 否則保存在上一個 entry 的 next 字段內(nèi)
        entry.next = newIndex
    }

    s.meta.size++
    // 設(shè)置 next free index
    s.meta.freeIndex = newEntry.next
    // 設(shè)置隊尾
    newEntry.next = sentinel

    return newEntry, true
}

3) 用 Entry 保存 metrics 值

https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/counter.go#L56

func NewShmCounterFunc(name string) func() gometrics.Counter {
    return func() gometrics.Counter {
        if defaultZone != nil {
            if entry, err := defaultZone.alloc(name); err == nil {
                return ShmCounter(unsafe.Pointer(&entry.value))
            }
            ...

可以看出歼捐,entry 的 value 是真正被用作記錄 metrics 值的地方,它是一個 64 位的空間晨汹。

為什么使用共享內(nèi)存保存 metrics

看到這里你可能會問豹储,為什么要這么辛苦封裝共享內(nèi)存來保存 metrics 值?為什么不直接使用堆空間來做呢淘这?

其實在源碼里也有答案:

https://github.com/mosn/mosn/blob/b2a239f5/pkg/mosn/starter.go#L318

func initializeMetrics(config v2.MetricsConfig) {
    // init shm zone
    if config.ShmZone != "" && config.ShmSize > 0 {
        shm.InitDefaultMetricsZone(config.ShmZone, int(config.ShmSize), store.GetMosnState() != store.Active_Reconfiguring)
        ...

https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/zone.go#L58

func createMetricsZone(name string, size int, clear bool) *zone {
    if clear {
        shm.Clear(name)
    }
    ...

如果 clear 為真剥扣,共享內(nèi)存就會被清除,那么什么時候為假呢铝穷?當(dāng) store.GetMosnState() 方法返回 store.Active_Reconfigureing 的時候钠怯。即,當(dāng) MOSN reconfig 重啟的時候氧骤,
已經(jīng)保存的 metrics 是會被保留的呻疹。

而且從這里可以看出:

https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/zone.go#L124

func newSharedMetrics(name string, size int) (*zone, error) {
    ...
    set, err := newHashSet(hashSegment, hashSegSize, entryNum, slotsNum, atomic.LoadUint32(zone.ref) == 0)
    ...

https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/hashset.go#L78

func newHashSet(segment uintptr, bytesNum, cap, slotsNum int, init bool) (*hashSet, error) {
    set := &hashSet{}
    ...

當(dāng) zone.ref 引用計數(shù)不為 0 的時候,哈希表里面的信息也是會被保留的筹陵。

再來看 zone.mutex 互斥鎖的使用:

https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/zone.go#L136

func (z *zone) lock() {
    times := 0

    // 5ms spin interval, 5 times burst
    for {
        if atomic.CompareAndSwapUint32(z.mutex, 0, pid) {
            return
        }
    ...

是通過設(shè)置進(jìn)程 ID 來獲取鎖的刽锤,由此能看出 MOSN 的用意:這個以文件作為 mmap 的共享內(nèi)存是可以被多個 MOSN 進(jìn)程共用的

例如像同一臺機(jī)器多個 MOSN 作為 sidecar 的場景朦佩,我們完全可以掛載宿主機(jī)同一個文件作為不同 sidecar 的共享內(nèi)存文件映射并思,
除了能達(dá)到 metrics 信息共享的效果外,也避免了 metrics 重復(fù)的內(nèi)存占用语稠,這里應(yīng)該是有優(yōu)化考慮在的宋彼。而且這個文件可以看作是一種文件格式,
在任何時候都可以被持久化保存和提取分析使用的仙畦。

總結(jié)

本文通過分析 MOSN 源碼输涕,簡述了 MOSN 的共享內(nèi)存模型,分析了 MOSN 創(chuàng)建共享內(nèi)存慨畸、配置 metrics 和 metrics 對共享內(nèi)存塊的使用莱坎。


參考資料:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市寸士,隨后出現(xiàn)的幾起案子檐什,更是在濱河造成了極大的恐慌碴卧,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乃正,死亡現(xiàn)場離奇詭異住册,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)瓮具,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門荧飞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人名党,你說我怎么就攤上這事垢箕。” “怎么了兑巾?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長忠荞。 經(jīng)常有香客問我蒋歌,道長,這世上最難降的妖魔是什么委煤? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任堂油,我火速辦了婚禮,結(jié)果婚禮上碧绞,老公的妹妹穿的比我還像新娘府框。我一直安慰自己,他們只是感情好讥邻,可當(dāng)我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布迫靖。 她就那樣靜靜地躺著,像睡著了一般兴使。 火紅的嫁衣襯著肌膚如雪系宜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天发魄,我揣著相機(jī)與錄音盹牧,去河邊找鬼。 笑死励幼,一個胖子當(dāng)著我的面吹牛汰寓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播苹粟,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼有滑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了六水?” 一聲冷哼從身側(cè)響起俺孙,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤辣卒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后睛榄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荣茫,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年场靴,在試婚紗的時候發(fā)現(xiàn)自己被綠了啡莉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡旨剥,死狀恐怖咧欣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情轨帜,我是刑警寧澤魄咕,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站蚌父,受9級特大地震影響哮兰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜苟弛,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一喝滞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧膏秫,春花似錦右遭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至亭敢,卻和暖如春宵距,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背吨拗。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工痕支, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留白筹,地道東北人竖配。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓通今,卻偏偏與公主長得像,于是被迫代替她去往敵國和親娇妓。 傳聞我的和親對象是個殘疾皇子像鸡,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,490評論 2 348