以太坊事件框架

過去在學Actor模型的時候,就認爲異步消息是相當的重要,在華爲的時候,也深扒了一下當時產品用的消息模型,簡單實用,支撐起了很多模塊和業務,但也有一個缺點是和其他的框架有耦合,最近看到以太坊的事件框架,同樣簡單簡潔,理念很適合初步接觸事件框架的同學,寫文介紹一下。

以太坊的事件框架是一個單獨的基礎模塊,存在於目錄go-ethereum/event中,它有2中獨立的事件框架實現,老點的叫TypeMux,已經基本棄用,新的叫Feed,當前正在廣泛使用。

TypeMuxFeed還只是簡單的事件框架,與Kafka、RocketMQ等消息系統相比,是非常的傳統和簡單,但是TypeMuxFeed的簡單簡潔,已經很好的支撐以太坊的上層模塊,這是當下最好的選擇。

TypeMuxFeed各有優劣,最優秀的共同特點是,他們只依賴於Golang原始的包,完全與以太坊的其他模塊隔離開來,也就是說,你完全可以把這兩個事件框架用在自己的項目中。

TypeMux的特點是,你把所有的訂閱塞給它就好,事件來了它自會通知你,但有可能會阻塞,通知你不是那麼及時,甚至過了一段挺長的時間。

Feed的特點是,它通常不存在阻塞的情況,會及時的把事件通知給你,但需要你爲每類事件都建立一個Feed,然後不同的事件去不同的Feed上訂閱和發送,這其實挺煩人的,如果你用錯了Feed,會導致panic。

接下來,介紹下這種簡單事件框架的抽象模型,然後再回歸到以太坊,介紹下TypeMuxFeed

!<--more-->

事件框架的抽象結構

原始事件框架抽象結構

如上圖,輕量級的事件框架會把所有的被訂閱的事件收集起來,然後把每個訂閱者組合成一個列表,當事件框架收到某個事件的時候,就把訂閱該事件的所有訂閱者找出來,然後把這個事件發給他們。

它需要具有2個功能:

  1. 讓訂閱者訂閱、取消訂閱某類事件。
  2. 讓發佈者能夠發佈某個事件,並且把事件送到每個訂閱者。

如果做成完善的消息系統,就還得考慮這些特性:可用性、吞吐量、傳輸延遲、有序消息、消息存儲、過濾、重發,這和事件框架相比就複雜上去了,我們專注的介紹下以太坊的事件模型怎麼完成上述3個功能的。

以太坊的事件模型

TypeMux是一個以太坊不太滿意的事件框架,所以以太坊就搞了Feed出來,它解決了TypeMux效率低下,延遲交付的問題。接下來就先看下這個TypeMux

TypeMux:同步事件框架

TypeMux是一個同步事件框架。它的實現和上面講的事件框架的抽象結構是完全一樣的,它維護了一個訂閱表,表裏維護了每個事件的訂閱者列表。它的特點:

  1. 採用多對多結構:多個事件對多個訂閱者。
  2. 採用推模式,把事件/消息推送給訂閱者,就像信件一樣,會被送到你的信箱,你在信箱裏取信就行了。
  3. 是一個同步事件框架。這也是它的缺點所在,舉個例子就是:郵遞員要給小紅、小明送信,只有信箱裏的信被小紅取走後,郵遞員纔去給小明送信,如果小紅旅遊去了無法取信,郵遞員就一直等在小紅家,而小明一直收不到信,小明很無辜無辜啊!

看下它2個功能的實現:

  1. 訂閱和取消訂閱。訂閱通過函數TypeMux.Subscribe(),入參爲要訂閱的事件類型,會返回TypeMuxSubscription給訂閱者,訂閱者可通過此控制訂閱,通過TypeMuxSubscription.Unsubscribe() 可以取消訂閱。
  2. 發佈事件和傳遞事件。TypeMux.Post(),入參爲事件類型,根據訂閱表找出該事件的訂閱者列表,遍歷列表,依次向每個訂閱者傳遞事件,如果前一個沒有傳遞完成進入阻塞,會導致後邊的訂閱者不能及時收到事件。

TypeMux抽象結構

TypeMux源碼速遞

TypeMux的精簡組成:

// A TypeMux dispatches events to registered receivers. Receivers can be
// registered to handle events of certain type. Any operation
// called after mux is stopped will return ErrMuxClosed.
//
// The zero value is ready to use.
//
// Deprecated: use Feed
// 本質:哈希列表,每個事件的訂閱者都存到對於的列表裏
type TypeMux struct {
    mutex   sync.RWMutex // 鎖
    subm    map[reflect.Type][]*TypeMuxSubscription // 訂閱表:所有事件類型的所有訂閱者
    stopped bool
}

訂閱:

// Subscribe creates a subscription for events of the given types. The
// subscription's channel is closed when it is unsubscribed
// or the mux is closed.
// 訂閱者只傳入訂閱的事件類型,然後TypeMux會返回給它一個訂閱對象
func (mux *TypeMux) Subscribe(types ...interface{}) *TypeMuxSubscription {
    sub := newsub(mux)
    mux.mutex.Lock()
    defer mux.mutex.Unlock()
    if mux.stopped {
        // set the status to closed so that calling Unsubscribe after this
        // call will short circuit.
        sub.closed = true
        close(sub.postC)
    } else {
        if mux.subm == nil {
            mux.subm = make(map[reflect.Type][]*TypeMuxSubscription)
        }
        for _, t := range types {
            rtyp := reflect.TypeOf(t)
            // 在同一次訂閱中,不要重複訂閱同一個類型的事件
            oldsubs := mux.subm[rtyp]
            if find(oldsubs, sub) != -1 {
                panic(fmt.Sprintf("event: duplicate type %s in Subscribe", rtyp))
            }
            subs := make([]*TypeMuxSubscription, len(oldsubs)+1)
            copy(subs, oldsubs)
            subs[len(oldsubs)] = sub
            mux.subm[rtyp] = subs
        }
    }
    return sub
}

取消訂閱:

func (s *TypeMuxSubscription) Unsubscribe() {
    s.mux.del(s)
    s.closewait()
}

發佈事件和傳遞事件:

// Post sends an event to all receivers registered for the given type.
// It returns ErrMuxClosed if the mux has been stopped.
// 遍歷map,找到所有訂閱的人,向它們傳遞event,同一個event對象,非拷貝,運行在調用者goroutine
func (mux *TypeMux) Post(ev interface{}) error {
    event := &TypeMuxEvent{
        Time: time.Now(),
        Data: ev,
    }
    rtyp := reflect.TypeOf(ev)
    mux.mutex.RLock()
    if mux.stopped {
        mux.mutex.RUnlock()
        return ErrMuxClosed
    }
    subs := mux.subm[rtyp]
    mux.mutex.RUnlock()
    for _, sub := range subs {
        sub.deliver(event)
    }
    return nil
}

func (s *TypeMuxSubscription) deliver(event *TypeMuxEvent) {
    // Short circuit delivery if stale event
    // 不發送過早(老)的消息
    if s.created.After(event.Time) {
        return
    }
    // Otherwise deliver the event
    s.postMu.RLock()
    defer s.postMu.RUnlock()

    select {
    case s.postC <- event:
    case <-s.closing:
    }
}

我上面指出了發送事件可能阻塞,阻塞在哪?關鍵就在下面這裏:創建TypeMuxSubscription時,通道使用的是無緩存通道,讀寫是同步的,這裏註定了TypeMux是一個同步事件框架,這是以太坊改用Feed的最大原因

func newsub(mux *TypeMux) *TypeMuxSubscription {
    c := make(chan *TypeMuxEvent) // 無緩衝通道,同步讀寫
    return &TypeMuxSubscription{
        mux:     mux,
        created: time.Now(),
        readC:   c,
        postC:   c,
        closing: make(chan struct{}),
    }
}

Feed:流式框架

Feed是一個流式事件框架。上文強調了TypeMux是一個同步框架,也正是因爲此以太坊丟棄了它,難道Feed就是一個異步框架?不一定是的,這取決於訂閱者是否採用有緩存的通道,採用有緩存的通道,則Feed就是異步的,採用無緩存的通道,Feed就是同步的,把同步還是異步的選擇交給使用者

本節強調Feed的流式特點。事件本質是一個數據,連續不斷的事件就組成了一個數據流,這些數據流不停的流向它的訂閱者那裏,並且不會阻塞在任何一個訂閱者那裏。

舉幾個不是十分恰當的例子。

  1. 公司要放中秋節,HR給所有同事都發了一封郵件,有些同事讀了,有些同事沒讀,要到國慶節了HR又給所有同事發了一封郵件,這些郵件又進入到每個人的郵箱,不會因爲任何一個人沒有讀郵件,導致剩下的同事收不到郵件。
  2. 你在朋友圈給朋友旅行的照片點了個贊,每當你們共同朋友點贊或者評論的時候,你都會收到提醒,無論你看沒看這些提醒,這些提醒都會不斷的發過來。
  3. 你微博關注了謝娜,謝娜發了個搞笑的視頻,你刷微博的時候就收到了,但也有很多人根本沒刷微博,你不會因爲別人沒有刷,你就收不到謝娜的動態。

Feed和TypeMux相同的是,它們都是推模式,不同的是Feed是異步的,如果有些訂閱者阻塞了,沒關係,它會繼續向後面的訂閱者發送事件/消息。

Feed是一個一對多的事件流框架。每個類型的事件都需要一個與之對應的Feed,訂閱者通過這個Feed進行訂閱事件,發佈者通過這個Feed發佈事件。

Feed抽象結構

看下Feed是如何實現2個功能的:

  1. 訂閱和取消訂閱:Feed.Subscribe(),入參是一個通道,通常是有緩衝的,就算是無緩存也不會造成Feed阻塞,Feed會校驗這個通道的類型和本Feed管理的事件類型是否一致,然後把通道保存下來,返回給訂閱者一個Subscription,可以通過它取消訂閱和讀取通道錯誤。
  2. 發佈事件和傳遞事件。Feed.Send()入參是一個事件,加鎖確保本類型事件只有一個發送協程正在進行,然後校驗事件類型是否匹配,Feed會嘗試給每個訂閱者發送事件,如果訂閱者阻塞,Feed就繼續嘗試給下一個訂閱者發送,直到給每個訂閱者發送事件,返回發送該事件的數量。

Feed源碼速遞

Feed定義:

// Feed implements one-to-many subscriptions where the carrier of events is a channel.
// Values sent to a Feed are delivered to all subscribed channels simultaneously.
//
// Feeds can only be used with a single type. The type is determined by the first Send or
// Subscribe operation. Subsequent calls to these methods panic if the type does not
// match.
//
// The zero value is ready to use.
// 一對多的事件訂閱管理:每個feed對象,當別人調用send的時候,會發送給所有訂閱者
// 每種事件類型都有一個自己的feed,一個feed內訂閱的是同一種類型的事件,得用某個事件的feed才能訂閱該事件
type Feed struct {
    once      sync.Once        // ensures that init only runs once
    sendLock  chan struct{}    // sendLock has a one-element buffer and is empty when held.It protects sendCases. 這個鎖確保了只有一個協程在使用go routine
    removeSub chan interface{} // interrupts Send
    sendCases caseList         // the active set of select cases used by Send,訂閱的channel列表,這些channel是活躍的

    // The inbox holds newly subscribed channels until they are added to sendCases.
    mu     sync.Mutex
    inbox  caseList // 不活躍的在這裏
    etype  reflect.Type
    closed bool
}

訂閱事件:

// Subscribe adds a channel to the feed. Future sends will be delivered on the channel
// until the subscription is canceled. All channels added must have the same element type.
//
// The channel should have ample buffer space to avoid blocking other subscribers.
// Slow subscribers are not dropped.
// 訂閱者傳入接收事件的通道,feed將通道保存爲case,然後返回給訂閱者訂閱對象
func (f *Feed) Subscribe(channel interface{}) Subscription {
    f.once.Do(f.init)

    // 通道和通道類型檢查
    chanval := reflect.ValueOf(channel)
    chantyp := chanval.Type()
    if chantyp.Kind() != reflect.Chan || chantyp.ChanDir()&reflect.SendDir == 0 {
        panic(errBadChannel)
    }
    sub := &feedSub{feed: f, channel: chanval, err: make(chan error, 1)}

    f.mu.Lock()
    defer f.mu.Unlock()
    if !f.typecheck(chantyp.Elem()) {
        panic(feedTypeError{op: "Subscribe", got: chantyp, want: reflect.ChanOf(reflect.SendDir, f.etype)})
    }
    
    // 把通道保存到case
    // Add the select case to the inbox.
    // The next Send will add it to f.sendCases.
    cas := reflect.SelectCase{Dir: reflect.SelectSend, Chan: chanval}
    f.inbox = append(f.inbox, cas)
    return sub
}

發送和傳遞事件:這個發送是比較繞一點的,要想真正掌握其中的運行,最好寫個小程序練習下。

// Send delivers to all subscribed channels simultaneously.
// It returns the number of subscribers that the value was sent to.
// 同時向所有的訂閱者發送事件,返回訂閱者的數量
func (f *Feed) Send(value interface{}) (nsent int) {
    rvalue := reflect.ValueOf(value)

    f.once.Do(f.init)
    <-f.sendLock // 獲取發送鎖

    // Add new cases from the inbox after taking the send lock.
    // 從inbox加入到sendCases,不能訂閱的時候直接加入到sendCases,因爲可能其他協程在調用發送
    f.mu.Lock()
    f.sendCases = append(f.sendCases, f.inbox...)
    f.inbox = nil

    // 類型檢查:如果該feed不是要發送的值的類型,釋放鎖,並且執行panic
    if !f.typecheck(rvalue.Type()) {
        f.sendLock <- struct{}{}
        panic(feedTypeError{op: "Send", got: rvalue.Type(), want: f.etype})
    }
    f.mu.Unlock()

    // Set the sent value on all channels.
    // 把發送的值關聯到每個case/channel,每一個事件都有一個feed,所以這裏全是同一個事件的
    for i := firstSubSendCase; i < len(f.sendCases); i++ {
        f.sendCases[i].Send = rvalue
    }

    // Send until all channels except removeSub have been chosen. 'cases' tracks a prefix
    // of sendCases. When a send succeeds, the corresponding case moves to the end of
    // 'cases' and it shrinks by one element.
    // 所有case仍然保留在sendCases,只是用過的會移動到最後面
    cases := f.sendCases
    for {
        // Fast path: try sending without blocking before adding to the select set.
        // This should usually succeed if subscribers are fast enough and have free
        // buffer space.
        // 使用非阻塞式發送,如果不能發送就及時返回
        for i := firstSubSendCase; i < len(cases); i++ {
            // 如果發送成功,把這個case移動到末尾,所以i這個位置就是沒處理過的,然後大小減1
            if cases[i].Chan.TrySend(rvalue) {
                nsent++
                cases = cases.deactivate(i)
                i--
            }
        }

        // 如果這個地方成立,代表所有訂閱者都不阻塞,都發送完了
        if len(cases) == firstSubSendCase {
            break
        }

        // Select on all the receivers, waiting for them to unblock.
        // 返回一個可用的,直到不阻塞。
        chosen, recv, _ := reflect.Select(cases)
        if chosen == 0 /* <-f.removeSub */ {
            // 這個接收方要刪除了,刪除並縮小sendCases
            index := f.sendCases.find(recv.Interface())
            f.sendCases = f.sendCases.delete(index)
            if index >= 0 && index < len(cases) {
                // Shrink 'cases' too because the removed case was still active.
                cases = f.sendCases[:len(cases)-1]
            }
        } else {
            // reflect已經確保數據已經發送,無需再嘗試發送
            cases = cases.deactivate(chosen)
            nsent++
        }
    }

    // 把sendCases中的send都標記爲空
    // Forget about the sent value and hand off the send lock.
    for i := firstSubSendCase; i < len(f.sendCases); i++ {
        f.sendCases[i].Send = reflect.Value{}
    }
    f.sendLock <- struct{}{}
    return nsent
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章