nsq源碼解讀之nsqlookupd

NSQ由3個進程組成:

  • nsqd: 接收,維護隊列和分發(fā)消息給客戶端的daemon進程
  • nsqlookupd: 管理拓撲信息并提供最終一致性的發(fā)現(xiàn)服務
  • nsqadmin: 用于實時監(jiān)控集群運行并提供管理命令的管理網(wǎng)站平臺。
    我們先從nsqlookupd開始。

1. 程序入口

nsqlookup的入口函數(shù)在apps/nsqlookupd/nsqlookupd.go這個文件中。

//apps/nsqlookupd/nsqlookupd.go
func main() {
    prg := &program{}
    if err := svc.Run(prg, syscall.SIGINT, syscall.SIGTERM); err != nil {
        log.Fatal(err)
    }
}

這里用到了github.com/judwhite/go-svc/svc管理進程。實際工作中調用的是Init,Start,Stop三個函數(shù)。

  • Init函數(shù)判斷了當前的操作系統(tǒng)環(huán)境,如果是windwos系統(tǒng)的話,就會將修改工作目錄。可以參考https://github.com/judwhite/go-svc首頁的例子。
  • Start函數(shù)實現(xiàn)了主體功能,接下來會具體分析。
  • Stop函數(shù)接受外界的signal,如果收到syscall.SIGINT和syscall.SIGTERM信號,就會被執(zhí)行。

2. Stop函數(shù)

先易后難,先解讀一下Stop函數(shù)。Stop函數(shù)調用Exit函數(shù),關閉了tcp服務和http服務,然后等兩個服務關閉之后,程序結束。“等兩個服務關閉”這個動作涉及到goroutine同步,nsq通過WaitGroup(參考Goroutine同步)實現(xiàn)。

//nsqlookupd/nsqlookupd.go
func (l *NSQLookupd) Exit() {
    if l.tcpListener != nil {
        l.tcpListener.Close()
    }

    if l.httpListener != nil {
        l.httpListener.Close()
    }
    l.waitGroup.Wait()
}

//internal/util/wait_group_wrapper.go
func (w *WaitGroupWrapper) Wrap(cb func()) {
    w.Add(1)
    go func() {
        cb()
        w.Done()
    }()
}

其中cb函數(shù)以tcp服務為例,當間接檢測到tcp已經(jīng)close時,退出for循環(huán),cb執(zhí)行結束,waitGroup計數(shù)器減一。
這里通過error的值判斷tcpListener是否關閉的方式,值得關注一下。

//internal/protocol/tcp_server.go
func TCPServer(listener net.Listener, handler TCPHandler, l app.Logger) {
    l.Output(2, fmt.Sprintf("TCP: listening on %s", listener.Addr()))

    for {
        clientConn, err := listener.Accept()
        if err != nil {
            if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
                l.Output(2, fmt.Sprintf("NOTICE: temporary Accept() failure - %s", err))
                runtime.Gosched()
                continue
            }
            // theres no direct way to detect this error because it is not exposed
            if !strings.Contains(err.Error(), "use of closed network connection") {
                l.Output(2, fmt.Sprintf("ERROR: listener.Accept() - %s", err))
            }
            break
        }
        go handler.Handle(clientConn)
    }

    l.Output(2, fmt.Sprintf("TCP: closing %s", listener.Addr()))
}

3. Start函數(shù)

Start函數(shù)實現(xiàn)了主要的功能。首先是讀配置,然后初始化nsqlookupd,最后啟動了tcp服務和http服務。
其中NSQLookupd.DB中維護了所有的消息的生產者信息。

3.1 tcp服務

tcp協(xié)議格式: 4字節(jié)的size,4字節(jié)的協(xié)議版本號(V1),之后的都是數(shù)據(jù)。

[x][x][x][x][x][x][x][x][x][x][x][x]...
|  (int32) ||  (int32) || (binary)
|  4-byte  ||  4-byte  || N-byte
------------------------------------...
    size      frame ID     data

tcp解包和處理的部分代碼為nsqlookupd/tcp.go和nsqlookupd/lookup_protocol_v1.go。需要注意的是,producer與nsqlookupd維持了一個長連接。tcp頭域的8個字節(jié)只有第一次連接時才會發(fā)送。
其中IOLoop中這幾行代碼,會持續(xù)的從tcp連接中讀取數(shù)據(jù)包。

//nsqlookupd/lookup_protocol_v1.go
client := NewClientV1(conn)
reader := bufio.NewReader(client)
for {
    line, err = reader.ReadString('\n')
......

tcp服務支持4種操作PING,IDENTIFY,REGISTER,UNREGISTER。
PING用來維持連接,IDENTIFY用來nsqlookupd和producer之間交換身份信息和端口配置信息,REGISTER和UNREGISTER分別是注冊和刪除producer(通過NSQLookupd.DB)

3.2 http服務

http服務支持一系列接口
有兩點比較有趣:

  1. nsq實現(xiàn)了一個裝飾器decorator,是的,效果和python里的裝飾器一樣!使用如下:
//nsqlookupd/http.go
router.Handle("GET", "/ping", http_api.Decorate(s.pingHandler, log, http_api.PlainText))

Decorator實現(xiàn)方式如下:

//internal/http_api/api_response.go
type Decorator func(APIHandler) APIHandler
func Decorate(f APIHandler, ds ...Decorator) httprouter.Handle {
    decorated := f
    for _, decorate := range ds {
        decorated = decorate(decorated)
    }
    return func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
        decorated(w, req, ps)
    }
}
  1. 有個接口叫"/topic/tombstone",tombstone是什么意思呢?字面上是墓碑的意思。在這里的意思,引用官網(wǎng)的一段話:

However, it gets a bit more complicated when a topic is no longer produced on a subset of nodes. Because of the way consumers query nsqlookupd and connect to all producers you enter into race conditions with attempting to remove the information from the cluster and consumers discovering that node and reconnecting (thus pushing updates that the topic is still produced on that node). The solution in these cases is to use “tombstones”. A tombstone in nsqlookupd context is producer specific and lasts for a configurable --tombstone-lifetime time. During that window the producer will not be listed in /lookup queries, allowing the node to delete the topic, propagate that information to nsqlookupd (which then removes the tombstoned producer), and prevent any consumer from re-discovering that node.

如果要下掉某個topic的部分節(jié)點,因為消費者會查詢nsqlookup然后去連所有的生產者,會產生一個問題:一方面,nsqlookupd會去刪除集群中相關的信息,另一方面在下掉這部分生產者之后,消費者不會立刻更新生產者的信息,還是會繼續(xù)重新連接生產者,這會促使生產者繼續(xù)生產。解決的辦法就是使用"tombstones"。生產者會存在tombstone-lifetime的時間。在那個時間窗口里面,消費者去/lookup的時候,看不到這個生產者,允許這個生產者節(jié)點刪除這個topic,同時將這個信息傳給nsqlookupd,然后刪除被tombstoned的節(jié)點,阻止消費者重連這個生產者節(jié)點。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,869評論 18 139
  • 姓名:周小蓬 16019110037 轉載自:http://blog.csdn.net/YChenFeng/art...
    aeytifiw閱讀 34,743評論 13 425
  • 前言 許多家長在小孩還沒有踏進校門前 就已經(jīng)開始為孩子做各種學前教育 教他們認字、背古詩、學兒歌 當然也會教他們抽...
    葡萄科技閱讀 250評論 0 0
  • 最近天氣熱了,阿焱也越來越和我不對胃口了,他老是挑我的刺,還總叫我收拾房間打掃客廳,這么熱的天,打掃完又是滿身臭汗...
    候麥閱讀 9,097評論 0 2
  • 20161121問題解析請點擊今日問題下方的“【Java每日一題】20161122”查看(問題解析在公眾號首發(fā),公...
    weknow閱讀 151評論 0 4