概覽
NSQ是一個實時分散式訊息平臺, 旨在大規模執行, 每天處理數十億條訊息, 被許多網際網路公司所使用;
其中 nsqd 是一個守護程式, 負責接收, 排隊, 投遞訊息給客戶端;
它可以獨立執行, 不過通常它是由 nsqlookupd 例項所在叢集配置的(它在這能宣告 topics 和 channels, 以便大家能找到);
它在 2 個 TCP 埠監聽, 一個給客戶端, 另一個是 HTTP API; 同時, 它也能在第三個埠監聽 HTTPS
模組
nsq
大概分nsqd
nsqlookupd
nsqadmin
三個部分
nsqlookupd
nsqlookupd
是守護程式負責管理拓撲資訊; 客戶端通過查詢nsqlookupd
來發現指定話題(topic)的生產者, 並且 nsqd 節點廣播話題(topic)和通道(channel)資訊, 具有以下功能
- 唯一性, 在一個
nsq
服務中只有一個nsqlookupd
服務, 當然也可以在叢集中部署多個nsqlookupd
, 但它們之間是沒有關聯的 - 去中心化, 即使
nsqlookupd
崩潰, 也會不影響正在執行的nsqd
服務 - 充當
nsqd
和naqadmin
資訊互動的中介軟體 - 提供一個
http
查詢服務, 給客戶端定時更新nsqd
的地址目錄
nsqd
nsqd
是一個守護程式, 負責接收, 排隊, 投遞訊息給客戶端
- 對訂閱了同一個
topic
, 同一個channel
的消費者使用負載均衡策略(不是輪詢) - 只要
channel
存在, 即使沒有該channel
的消費者, 也會將生產者的message
快取到佇列中(注意訊息的過期處理) - 保證佇列中的
message
至少會被消費一次, 即使nsqd
退出, 也會將佇列中的訊息暫存磁碟上(結束程式等意外情況除外) - 限定記憶體佔用, 能夠配置
nsqd
中每個channel
佇列在記憶體中快取的message
數量, 一旦超出,message
將被快取到磁碟中 topic
,channel
一旦建立, 將會一直存在, 要及時在管理臺或者用程式碼清除無效的topic
和channel
, 避免資源的浪費
nsqadmin
是一套 WEB UI, 用來彙集叢集的實時統計, 並執行不同的管理任務
原始碼分析
本文以及後面的分析都是基於 1.0.0 版本程式碼, 為了增加可讀性, 我把註釋放在了函式外, 基本都覆蓋到, 本文中就不囉嗦講如何使用了, 查閱文件即可
nsqlookupd.go
package nsqlookupd
// 鎖
// 配置選項
// tcpListener 如上文所說 tcp http 埠監聽
// httpListener
// waitGroup 執行緒同步
// 資料庫
type NSQLookupd struct {
sync.RWMutex
opts *Options
tcpListener net.Listener
httpListener net.Listener
waitGroup util.WaitGroupWrapper
DB *RegistrationDB
}
// 如果沒有指定 Logger, 就new一個
// new NSQLookupd, 待會看一下 `NewRegistrationDB` 做了什麼事情
// 解析 log level
func New(opts *Options) *NSQLookupd {
if opts.Logger == nil {
opts.Logger = log.New(os.Stderr, opts.LogPrefix, log.Ldate|log.Ltime|log.Lmicroseconds)
}
n := &NSQLookupd{
opts: opts,
DB: NewRegistrationDB(),
}
var err error
opts.logLevel, err = lg.ParseLogLevel(opts.LogLevel, opts.Verbose)
if err != nil {
n.logf(LOG_FATAL, "%s", err)
os.Exit(1)
}
n.logf(LOG_INFO, version.String("nsqlookupd"))
return n
}
// 建立 context, 其實 ctx 就是 NSQLookupd, 不明白為什麼多此一舉, 想要引入原生的 Context struct?
// 建立 tcpListener, 這裡用到了鎖, 說明該場景有併發
// 根據 ctx 建立 tcpServer
// waitGroup 執行緒同步後, 建立 TCPServer
// 重複以上步驟,建立 HTTPServer
func (l *NSQLookupd) Main() {
ctx := &Context{l}
tcpListener, err := net.Listen("tcp", l.opts.TCPAddress)
if err != nil {
l.logf(LOG_FATAL, "listen (%s) failed - %s", l.opts.TCPAddress, err)
os.Exit(1)
}
l.Lock()
l.tcpListener = tcpListener
l.Unlock()
tcpServer := &tcpServer{ctx: ctx}
l.waitGroup.Wrap(func() {
protocol.TCPServer(tcpListener, tcpServer, l.logf)
})
httpListener, err := net.Listen("tcp", l.opts.HTTPAddress)
if err != nil {
l.logf(LOG_FATAL, "listen (%s) failed - %s", l.opts.HTTPAddress, err)
os.Exit(1)
}
l.Lock()
l.httpListener = httpListener
l.Unlock()
httpServer := newHTTPServer(ctx)
l.waitGroup.Wrap(func() {
http_api.Serve(httpListener, httpServer, "HTTP", l.logf)
})
}
// 獲取 TCP 地址, 繼續鎖, 說明地址可能會修改
func (l *NSQLookupd) RealTCPAddr() *net.TCPAddr {
l.RLock()
defer l.RUnlock()
return l.tcpListener.Addr().(*net.TCPAddr)
}
// 獲取 HTTP 地址
func (l *NSQLookupd) RealHTTPAddr() *net.TCPAddr {
l.RLock()
defer l.RUnlock()
return l.httpListener.Addr().(*net.TCPAddr)
}
// 關閉 tcpListener httpListener, 等待執行緒同步後結束
func (l *NSQLookupd) Exit() {
if l.tcpListener != nil {
l.tcpListener.Close()
}
if l.httpListener != nil {
l.httpListener.Close()
}
l.waitGroup.Wait()複製程式碼
OK, 至此 nsqlookupd.go
已經分析完畢, 如果想知道以上程式碼如何單獨使用, 可以看測試nsqlookupd_test.go
呀 ?, 在以上程式碼中, 我們看到了 db
部分, 接下來看看
registrationdb.go
package nsqlookupd
// 鎖
// 以 Registration 為 key 儲存 Producers, 即生產者
type RegistrationDB struct {
sync.RWMutex
registrationMap map[Registration]Producers
}
type Registration struct {
Category string
Key string
SubKey string
}
type Registrations []Registration
// *節點資訊*
// 上次更新時間
// 識別符號
// 地址
// 主機名
// 廣播地址
// tcp 地址
// http 地址
// 版本號
type PeerInfo struct {
lastUpdate int64
id string
RemoteAddress string `json:"remote_address"`
Hostname string `json:"hostname"`
BroadcastAddress string `json:"broadcast_address"`
TCPPort int `json:"tcp_port"`
HTTPPort int `json:"http_port"`
Version string `json:"version"`
}
// *生產者*
// 節點資訊
// 是否刪除
// 刪除時間
type Producer struct {
peerInfo *PeerInfo
tombstoned bool
tombstonedAt time.Time
}
type Producers []*Producer
// 轉換為字串
func (p *Producer) String() string {
return fmt.Sprintf("%s [%d, %d]", p.peerInfo.BroadcastAddress, p.peerInfo.TCPPort, p.peerInfo.HTTPPort)
}
// 刪除
func (p *Producer) Tombstone() {
p.tombstoned = true
p.tombstonedAt = time.Now()
}
// 是否刪除
func (p *Producer) IsTombstoned(lifetime time.Duration) bool {
return p.tombstoned && time.Now().Sub(p.tombstonedAt) < lifetime
}
// 建立 RegistrationDB
func NewRegistrationDB() *RegistrationDB {
return &RegistrationDB{
registrationMap: make(map[Registration]Producers),
}
}
// 增加一個登錄檔 key
func (r *RegistrationDB) AddRegistration(k Registration) {
r.Lock()
defer r.Unlock()
_, ok := r.registrationMap[k]
if !ok {
r.registrationMap[k] = Producers{}
}
}
// 新增一個 producer 到 registration
// 取出 producers, 並遍歷,
// 如果不存在, 就新增進去
// 如果存在, 返回 false
func (r *RegistrationDB) AddProducer(k Registration, p *Producer) bool {
r.Lock()
defer r.Unlock()
producers := r.registrationMap[k]
found := false
for _, producer := range producers {
if producer.peerInfo.id == p.peerInfo.id {
found = true
}
}
if found == false {
r.registrationMap[k] = append(producers, p)
}
return !found
}
// 根據 id 從 registration 中刪除 producer
// 如果不存在, 返回 false
// 建立一個新的 Producers, 遍歷原來的 Producers,
// 如果 id 不相同就新增進去, 即刪除成功 簡單粗暴 哈哈哈哈哈哈
func (r *RegistrationDB) RemoveProducer(k Registration, id string) (bool, int) {
r.Lock()
defer r.Unlock()
producers, ok := r.registrationMap[k]
if !ok {
return false, 0
}
removed := false
cleaned := Producers{}
for _, producer := range producers {
if producer.peerInfo.id != id {
cleaned = append(cleaned, producer)
} else {
removed = true
}
}
// Note: this leaves keys in the DB even if they have empty lists
r.registrationMap[k] = cleaned
return removed, len(cleaned)
}
// 刪除一個 registration
func (r *RegistrationDB) RemoveRegistration(k Registration) {
r.Lock()
defer r.Unlock()
delete(r.registrationMap, k)
}
// 需要過濾
func (r *RegistrationDB) needFilter(key string, subkey string) bool {
return key == "*" || subkey == "*"
}
// 根據 category, key, subkey 查詢 Registrations
// 如果 key == '*' 或者 subkey == '*', 則只查詢一個
// 否則 遍歷 registrationMap, 返回所有條件符合的 registration
func (r *RegistrationDB) FindRegistrations(category string, key string, subkey string) Registrations {
r.RLock()
defer r.RUnlock()
if !r.needFilter(key, subkey) {
k := Registration{category, key, subkey}
if _, ok := r.registrationMap[k]; ok {
return Registrations{k}
}
return Registrations{}
}
results := Registrations{}
for k := range r.registrationMap {
if !k.IsMatch(category, key, subkey) {
continue
}
results = append(results, k)
}
return results
}
// 根據 category, key, subkey 查詢 Producers
// 同上 沒什麼好說的, 多了個根據 id 去重, 略囉嗦
func (r *RegistrationDB) FindProducers(category string, key string, subkey string) Producers {
r.RLock()
defer r.RUnlock()
if !r.needFilter(key, subkey) {
k := Registration{category, key, subkey}
return r.registrationMap[k]
}
results := Producers{}
for k, producers := range r.registrationMap {
if !k.IsMatch(category, key, subkey) {
continue
}
for _, producer := range producers {
found := false
for _, p := range results {
if producer.peerInfo.id == p.peerInfo.id {
found = true
}
}
if found == false {
results = append(results, producer)
}
}
}
return results
}
// 根據 id 查詢 Registrations
// 依然遍歷 沒什麼好說的
func (r *RegistrationDB) LookupRegistrations(id string) Registrations {
r.RLock()
defer r.RUnlock()
results := Registrations{}
for k, producers := range r.registrationMap {
for _, p := range producers {
if p.peerInfo.id == id {
results = append(results, k)
break
}
}
}
return results
}
// 是否匹配
func (k Registration) IsMatch(category string, key string, subkey string) bool {
if category != k.Category {
return false
}
if key != "*" && k.Key != key {
return false
}
if subkey != "*" && k.SubKey != subkey {
return false
}
return true
}
// 過濾
func (rr Registrations) Filter(category string, key string, subkey string) Registrations {
output := Registrations{}
for _, k := range rr {
if k.IsMatch(category, key, subkey) {
output = append(output, k)
}
}
return output
}
// keys
func (rr Registrations) Keys() []string {
keys := make([]string, len(rr))
for i, k := range rr {
keys[i] = k.Key
}
return keys
}
// subkeys
func (rr Registrations) SubKeys() []string {
subkeys := make([]string, len(rr))
for i, k := range rr {
subkeys[i] = k.SubKey
}
return subkeys
}
// 根據時間過濾
func (pp Producers) FilterByActive(inactivityTimeout time.Duration, tombstoneLifetime time.Duration) Producers {
now := time.Now()
results := Producers{}
for _, p := range pp {
cur := time.Unix(0, atomic.LoadInt64(&p.peerInfo.lastUpdate))
if now.Sub(cur) > inactivityTimeout || p.IsTombstoned(tombstoneLifetime) {
continue
}
results = append(results, p)
}
return results
}
// 節點資訊
func (pp Producers) PeerInfo() []*PeerInfo {
results := []*PeerInfo{}
for _, p := range pp {
results = append(results, p.peerInfo)
}
return results
}複製程式碼
好了, 可以看出 RegistrationDB
以 map
結構包含了所有節點資訊; 名為db
, 實則最多算個cache
罷了 2333333; 印證了上文中的 客戶端通過查詢 nsqlookupd 來發現指定話題(topic)的生產者
;
好了, 第一篇暫時結束, 接下來的敬請期待