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服務支持一系列接口。
有兩點比較有趣:
- 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)
}
}
- 有個接口叫"/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é)點。