NSQ源碼-NSQD

看完了nsqlookupd我們繼續往下看, nsqd纔是他的核心. 裏面大量的使用到了go channel, 相信看完之後對你學習go有很大的幫助.相較於lookupd部分無論在代碼邏輯和實現上都要複雜很多.
不過基本的代碼結構基本上都是一樣的, 進程使用go-srv來管理, Main裏啓動一個http sever和一個tcp server, 這裏可以參考下之前文章的進程模型小節, 不過在nsqd中會啓動另外的兩個goroutine queueScanLoop和lookupLoop。下面是一個
具體的進程模型。
圖片描述
後面的分析都是基於這個進程模型。

NSQD的啓動

啓動時序這塊兒大體上和lookupd中的一致, 我們下面來看看lookupLoop和queueScanLoop.
lookupLoop代碼見nsqd/lookup.go中 主要做以下幾件事情:

  • 和lookupd建立連接(這裏是一個長連接)
  • 每隔15s ping一下lookupd
  • 新增或者刪除topic的時候通知到lookupd
  • 新增或者刪除channel的時候通知到lookupd
  • 動態的更新options

由於設計到了nsq裏的in-flight/deferred message, 我們把queueScanLoop放到最後來看.

一條message的LifeLine

下面我們就通過一條message的生命週期來看下nsqd的工作原理. 根據官方的QuickStart, 我們可以通過curl來pub一條消息.

curl -d 'hello world 1' 'http://127.0.0.1:4151/pub?topic=test'

http handler

我們就跟着代碼看一下, 首先是http對此的處理:

// nsq/nsqd/http.go
func (s *httpServer) doPUB(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
    ...
    reqParams, topic, err := s.getTopicFromQuery(req) // 從http query中拿到topic信息
    ...
}
// nsq/nsqd/http.go
func (s *httpServer) getTopicFromQuery(req *http.Request) (url.Values, *Topic, error) {
    reqParams, err := url.ParseQuery(req.URL.RawQuery)
    topicNames, ok := reqParams["topic"]
    return reqParams, s.ctx.nsqd.GetTopic(topicName), nil
}
// nsq/nsqd/nsqd.go
// GetTopic performs a thread safe operation
// to return a pointer to a Topic object (potentially new)
func (n *NSQD) GetTopic(topicName string) *Topic {
    // 1. 首先查看n.topicMap,確認該topic是否已經存在(存在直接返回)
    t, ok := n.topicMap[topicName]
    // 2. 否則將新建一個topic
    t = NewTopic(topicName, &context{n}, deleteCallback)
    n.topicMap[topicName] = t

    // 3. 查看該nsqd是否設置了lookupd, 從lookupd獲取該tpoic的channel信息
    // 這個topic/channel已經通過nsqlookupd的api添加上去的, 但是nsqd的本地
    // 還沒有, 針對這種情況我們需要創建該channel對應的deffer queue和inFlight
    // queue.
    lookupdHTTPAddrs := n.lookupdHTTPAddrs()
    if len(lookupdHTTPAddrs) > 0 {
        channelNames, err := n.ci.GetLookupdTopicChannels(t.name, lookupdHTTPAddrs)
    }
    // now that all channels are added, start topic messagePump
    // 對該topic的初始化已經完成下面就是message
    t.Start()
    return t
}

topic messagePump

在上面消息初始化完成之後就啓動了tpoic對應的messagePump

// nsq/nsqd/topic.go
// messagePump selects over the in-memory and backend queue and
// writes messages to every channel for this topic
func (t *Topic) messagePump() {

    // 1. do not pass messages before Start(), but avoid blocking Pause() 
    // or GetChannel() 
    // 等待channel相關的初始化完成,GetTopic中最後的t.Start()才正式啓動該Pump
    

    // 2. main message loop
    // 開始從Memory chan或者disk讀取消息
    // 如果topic對應的channel發生了變化,則更新channel信息
    
    // 3. 往該tpoic對應的每個channel寫入message(如果是deffermessage
    // 的話放到對應的deffer queue中
    // 否則放到該channel對應的memoryMsgChan中)。
}

至此也就完成了從tpoic memoryMsgChan收到消息投遞到channel memoryMsgChan的投遞, 我們先看下http
收到消息到通知pump處理的過程。

// nsq/nsqd/http.go
func (s *httpServer) doPUB(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
    ...
    msg := NewMessage(topic.GenerateID(), body)
    msg.deferred = deferred
    err = topic.PutMessage(msg)
    if err != nil {
        return nil, http_api.Err{503, "EXITING"}
    }

    return "OK", nil
}
// nsq/nsqd/topic.go
// PutMessage writes a Message to the queue
func (t *Topic) PutMessage(m *Message) error {
    t.RLock()
    defer t.RUnlock()
    if atomic.LoadInt32(&t.exitFlag) == 1 {
        return errors.New("exiting")
    }
    err := t.put(m)
    if err != nil {
        return err
    }
    atomic.AddUint64(&t.messageCount, 1)
    return nil
}
func (t *Topic) put(m *Message) error {
    select {
    case t.memoryMsgChan <- m:
    default:
        b := bufferPoolGet()
        err := writeMessageToBackend(b, m, t.backend)
        bufferPoolPut(b)
        t.ctx.nsqd.SetHealth(err)
        if err != nil {
            t.ctx.nsqd.logf(LOG_ERROR,
                "TOPIC(%s) ERROR: failed to write message to backend - %s",
                t.name, err)
            return err
        }
    }
    return nil
}

這裏memoryMsgChan的大小我們可以通過--mem-queue-size參數來設置,上面這段代碼的流程是如果memoryMsgChan還沒有滿的話
就把消息放到memoryMsgChan中,否則就放到backend(disk)中。topic的mesasgePump檢測到有新的消息寫入的時候就開始工作了,
從memoryMsgChan/backend(disk)讀取消息投遞到channel對應的chan中。 還有一點請注意就是messagePump中

    if len(chans) > 0 && !t.IsPaused() {
        memoryMsgChan = t.memoryMsgChan
        backendChan = t.backend.ReadChan()
    }

這段代碼只有channel(此channel非golang裏的channel而是nsq的channel類似nsq_to_file)存在的時候纔會去投遞。上面部分就是
msg從producer生產消息到吧消息寫到memoryChan/Disk的過程,下面我們來看下consumer消費消息的過程。

首先是consumer從nsqlookupd查詢到自己所感興趣的topic/channel的nsqd信息, 然後就是來連接了。

tcp handler

對新的client的處理

//nsq/internal/protocol/tcp_server.go
func TCPServer(listener net.Listener, handler TCPHandler, logf lg.AppLogFunc) {
    go handler.Handle(clientConn)
}
//nsq/nsqd/tcp.go
func (p *tcpServer) Handle(clientConn net.Conn) {
    prot.IOLoop(clientConn)
}

針對每個client起一個messagePump吧msg從上面channel對應的chan 寫入到consumer側

//nsq/nsqd/protocol_v2.go
func (p *protocolV2) IOLoop(conn net.Conn) error {
    client := newClientV2(clientID, conn, p.ctx)
    p.ctx.nsqd.AddClient(client.ID, client)

    messagePumpStartedChan := make(chan bool)
    go p.messagePump(client, messagePumpStartedChan)

    // read the request
    line, err = client.Reader.ReadSlice('\n')
    response, err = p.Exec(client, params)
    p.Send(client, frameTypeResponse, response)

}
//nsq/nsqd/protocol_v2.go
func (p *protocolV2) Exec(client *clientV2, params [][]byte) ([]byte, error) {
    switch {
    case bytes.Equal(params[0], []byte("FIN")):
        return p.FIN(client, params)
    case bytes.Equal(params[0], []byte("RDY")):
        return p.RDY(client, params)
    case bytes.Equal(params[0], []byte("REQ")):
        return p.REQ(client, params)
    case bytes.Equal(params[0], []byte("PUB")):
        return p.PUB(client, params)
    case bytes.Equal(params[0], []byte("MPUB")):
        return p.MPUB(client, params)
    case bytes.Equal(params[0], []byte("DPUB")):
        return p.DPUB(client, params)
    case bytes.Equal(params[0], []byte("NOP")):
        return p.NOP(client, params)
    case bytes.Equal(params[0], []byte("TOUCH")):
        return p.TOUCH(client, params)
    case bytes.Equal(params[0], []byte("SUB")):
        return p.SUB(client, params)
    case bytes.Equal(params[0], []byte("CLS")):
        return p.CLS(client, params)
    case bytes.Equal(params[0], []byte("AUTH")):
        return p.AUTH(client, params)
    }
}
//nsq/nsqd/protocol_v2.go
func (p *protocolV2) SUB(client *clientV2, params [][]byte) ([]byte, error) {
    var channel *Channel
    topic := p.ctx.nsqd.GetTopic(topicName)
    channel = topic.GetChannel(channelName)
    channel.AddClient(client.ID, client)

    // 通知messagePump開始工作
    client.SubEventChan <- channel

通知topic的messagePump開始工作

func (t *Topic) GetChannel(channelName string) *Channel {
    t.Lock()
    channel, isNew := t.getOrCreateChannel(channelName)
    t.Unlock()

    if isNew {
        // update messagePump state
        select {
        case t.channelUpdateChan <- 1:
        case <-t.exitChan:
        }
    }

    return channel
}

message 對應的Pump

func (p *protocolV2) messagePump(client *clientV2, startedChan chan bool) {
    for {
        if subChannel == nil || !client.IsReadyForMessages() {
            // the client is not ready to receive messages...
            // 等待client ready,並且channel的初始化完成
            flushed = true
        } else if flushed {
            // last iteration we flushed...
            // do not select on the flusher ticker channel
            memoryMsgChan = subChannel.memoryMsgChan
            backendMsgChan = subChannel.backend.ReadChan()
            flusherChan = nil
        } else {
            // we're buffered (if there isn't any more data we should flush)...
            // select on the flusher ticker channel, too
            memoryMsgChan = subChannel.memoryMsgChan
            backendMsgChan = subChannel.backend.ReadChan()
            flusherChan = outputBufferTicker.C
        }

        select {
        case <-flusherChan:
            // if this case wins, we're either starved
            // or we won the race between other channels...
            // in either case, force flush
        case <-client.ReadyStateChan:
        case subChannel = <-subEventChan:
            // you can't SUB anymore
            // channel初始化完成,pump開始工作
            subEventChan = nil
        case identifyData := <-identifyEventChan:
            // you can't IDENTIFY anymore
        case <-heartbeatChan:
            // heartbeat的處理
        case b := <-backendMsgChan:
            // 1. decode msg
            // 2. 把msg push到Flight Queue裏
            // 3. send msg to client
        case msg := <-memoryMsgChan:
            // 1. 把msg push到Flight Queue裏
            // 2. send msg to client
        case <-client.ExitChan:
            // exit the routine
        }
    }

至此我們看的代碼就是一條消息從pub到nsqd中到被消費者處理的過程。不過得注意一點,我們在上面的代碼分析中,創建
topic/channel的部分放到了message Pub的鏈上, 如果是沒有lookupd的模式的話這部分是在client SUB鏈上的。

topic/hannel的管理

在NSQ內部通過

type NSQD struct {
    topicMap map[string]*Topic
}
和
type Topic struct {
    channelMap        map[string]*Channel
}

來維護一個內部的topic/channel狀態,然後在提供瞭如下的接口來管理topic和channel

/topic/create - create a new topic
/topic/delete - delete a topic
/topic/empty - empty a topic
/topic/pause - pause message flow for a topic
/topic/unpause - unpause message flow for a topic
/channel/create - create a new channel
/channel/delete - delete a channel
/channel/empty - empty a channel
/channel/pause - pause message flow for a channel
/channel/unpause - unpause message flow for a channel

create topic/channel的話我們在之前的代碼看過了,這裏可以重點看下topic/channel delete的時候怎樣保證數據優雅的刪除的,以及
messagePump的退出機制。

queueScanLoop的工作

// queueScanLoop runs in a single goroutine to process in-flight and deferred
// priority queues. It manages a pool of queueScanWorker (configurable max of
// QueueScanWorkerPoolMax (default: 4)) that process channels concurrently.
//
// It copies Redis's probabilistic expiration algorithm: it wakes up every
// QueueScanInterval (default: 100ms) to select a random QueueScanSelectionCount
// (default: 20) channels from a locally cached list (refreshed every
// QueueScanRefreshInterval (default: 5s)).
//
// If either of the queues had work to do the channel is considered "dirty".
//
// If QueueScanDirtyPercent (default: 25%) of the selected channels were dirty,
// the loop continues without sleep.

這裏的註釋已經說的很明白了,queueScanLoop就是通過動態的調整queueScanWorker的數目來處理
in-flight和deffered queue的。在具體的算法上的話參考了redis的隨機過期算法。

總結

閱讀源碼就是走走停停的過程,從一開始的無從下手到後面的一點點的把它啃透。一開始都覺得很困難,無從下手。以前也是嘗試着去看一些
經典的開源代碼,但都沒能堅持下來,有時候人大概是會高估自己的能力的,好多東西自以爲看個一兩遍就能看懂,其實不然,
好多知識只有不斷的去研究你才能參透其中的原理。

      一定要持續的讀,不然過幾天之後就忘了前面讀的內容
      一定要多總結, 總結就是在不斷的讀的過程,從第一遍讀通到你把它表述出來至少需要再讀5-10次
      多思考,這段時間在地鐵上/跑步的時候我會迴向一下其中的流程
      分享(讀懂是一個層面,寫出來是一個層面,講給別人聽是另外一個層面)

後面我會先看下go-nsqd部分的代碼,之後會研究下gnatsd, 兩個都是cloud native的消息系統,看下有啥區別。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章