Jaeger tchannel-go原始碼閱讀——Peer

cdh0805010118發表於2018-07-24

peer

peer 中文翻譯為:對等體, 它在 tchannel-go 中表示 rpc 呼叫或者被呼叫的一方。

peer 相關錯誤碼:

ErrInvalidConnectionState // 連線處於無效狀態

ErrNoPeers  //  沒有可用的peer

ErrPeerNoFound // 沒有發現peer

ErrNoNewPeers // 沒有新的可用peer

相關 peer 程式碼:

// peer使用該interface,建立connection。peer所擁有的channel實現了該interface
type Connectable interface {
    Connect(ctx context.Context, hostPort string) (*Connection, error)

    Logger() Logger
}

// channel擁有所有建立連線的Peers
type PeerList struct {
    sync.RWMutex

    // parent:後續補充
    parent *RootPeerList
    // channel所有連線,對於每個遠端hostPort所對應的peer
    peersByHostPort map[string]*peerScore
    // peerHeap: 後續補充, 它基於peers的score,來維護peers得最小heap
    peerHeap *peerHeap
    // channel所有連線的peers score計算
    scoreCalculator ScoreCalculator
    // lastSelected: 後續補充
    lastSelected uint64
}

// 建立PeerList,並初始化。RootPeerList後續補充
func newPeerList(root *RootPeerList) *PeerList {
    reutrn &PeerList{
        parent: root,
        peersByHostPort: make(map[string]*peerScore),
        scoreCalculator: newPreferIncomingCalculator(),
        peerHeap: newPeerHeap(),
    }
}

// 設定channel所有連線peers的score計算方式
// 最後並重新計算當前channel下所有peers的score
func (l *PeerList) SetStrategy(sc ScoreCalculator) {
    l.Lock()
    defer l.Unlock()

    l.scoreCalculator = sc
    for _, ps := range l.peersByHostPort {
        newScore := l.scoreCalculator.GetScore(ps.Peer)
        // 更新peer的score
        l.updatePeer(ps, newScore)
    }
}

// 根據peerList建立一個它的兄弟PeerList。
func (l *PeerList) newSibling() *PeerList {
    sib := newPeerList(l.parent)
    return sib
}

// 在channel的PeerList中增加一個peer, 這個方法引出了後記
func (l *PeerList) Add(hostPort string) *Peer {
    // 如果hostPort已存在,則不需要增加peer
    if ps, ok := l.exists(hostPort); ok {
        return ps.Peer
    }

    l.Lock()
    defer l.Unlock()

    // 再次校驗是否已經存在peer
    if p, ok := l.peersByHostPort[hostPort]; ok {
        return p.Peer
    }

    // 確定不存在peer,建立peer, 增加RootPeerList的peersByHostPort對映關係
    p := l.parent.Add(hostPort)
    // 增加subchannel的引用計數
    p.addSC()
    // 根據peer建立,peerScore物件
    ps := newPeerScore(p, l.scoreCalculator.GetScore(p))

    // 通過channel的peersByHostPort儲存對映關係
    l.peersByHostPort[hostPort] = ps
    // push peer到peer heap中,後面再介紹
    l.peerHeap.addPeer(ps)

    return p
}

後記

第一個討論點

我們在看 tchannel-go 原始碼過程中經常遇到一個這樣的寫法,大家可以注意下:

tips: 下面我們都假設YYY已經初始化過了。

type XXX struct {
    sync.Mutex

    YYY map[string]interface{}
}

func (x *XXX) Add(key string, value interface{}) {
    x.RLock()
    if _, ok := x.YYY[key]; ok {
        return 
    }
    x.RUnlock()

    x.Lock()
    if _, ok := x.YYY[key]; ok {
        return
    }

    x.YYY[key]=value
    x.Unlock()
}

在其他專案中也會存在這種寫法。首先對於存在競態的資料,在讀寫操作時需要加鎖互斥操作。所以,下面這種做法也是經常見到的。

func (x *XXX) Add(key string, value interface{}) {
    x.Lock()
    defer x.Unlock()

    if _, ok := x.YYY[key]; ok {
        return
    }

    x.YYY[key] = value
    return
}

對比這兩者的寫法,沒有太多不同。這裡要考慮的第一點:

讀鎖和讀寫鎖,時效性明顯不同。對於讀鎖,只有在寫時阻塞,可以併發讀;對於讀寫鎖,讀寫都互斥。後者明顯時耗大一些,如果併發量夠大,則效果更明顯

我們考慮第二點,為何第一種寫法,在嘗試讀取後,下一個鎖操作內還要取嘗試取一次資料,失敗然後再進行儲存操作?

因為每一次鎖操作是一個完整操作,如果在臨界區間,goroutine 被排程,則其他想要操作該鎖也得等待該鎖釋放,最後該 goroutine 被排程,完成釋放鎖後;如果該 goroutine 又被排程,則其他 goroutine 可能會操作該互斥 YYY 變數,所以下次進入臨界區時,需要嘗試再取一次資料,如果沒有則進行儲存操作

第二個討論點

對於 PeerList 的 Add 方法,或者說整個 tchannel-go 有一點做得不太好的地方是,方法裡的鎖操作太多,而且沒有標識,比如l.parent.Addp.addSC方法都存在鎖操作,如果外界直接呼叫,鎖相同則會導致死鎖。在 go 有關的很多專案都是這樣寫的:

func (x *XXX) AddLock() {
    x.Lock()
    defer x.Unlock()
    ... // other operations
}

func (x *XXX) AddNoLock() {
    ... // other operations
}

或者

func (x *XXX) Add() {
    ... // other operations
}

這個方法顯示的告訴大家,方法內是否存在鎖操作,這樣儘量減少死鎖反應。

更多原創文章乾貨分享,請關注公眾號
  • Jaeger tchannel-go原始碼閱讀——Peer
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章