NSQLookup & NSQAdmin设计原理
nsqlookup是管理中心,负责管理拓扑信息。nsqd节点在启动的时候会向nsqlookup进行注册,并且nsqd节点通过nsqlookup广播话题(topic)和通道(channel)信息。消费者通过查询nsqlookupd来发现指定话题(topic)的nsqd,并与nsqd建立连接进行通信和订阅。
NSQLookup源码实现
启动流程
main 函数:apps/nsqlookup/main.go
启动流程基本上和nsqd查不多,主要是Main 是主要的入口逻辑
type NSQLookupd struct {
sync.RWMutex // 读写锁
opts *Options // nsqlookup配置
tcpListener net.Listener // 监听的tcp服务
httpListener net.Listener // 监听的http服务
waitGroup util.WaitGroupWrapper // 用于等待goroutine结束
DB *RegistrationDB // 注册数据库,存放着topic和producer的映射关系注册表,存储在内存的Map中
}
type RegistrationDB struct {
sync.RWMutex
registrationMap map[Registration]ProducerMap
}
func (l *NSQLookupd) Main() error {
// ... 省略代码
// tcp 服务启动
l.waitGroup.Wrap(func() {
exitFunc(protocol.TCPServer(l.tcpListener, l.tcpServer, l.logf))
})
// http 服务启动
httpServer := newHTTPServer(l)
l.waitGroup.Wrap(func() {
exitFunc(http_api.Serve(l.httpListener, httpServer, "HTTP", l.logf))
})
err := <-exitCh
return err
}
总结
- nsqlookupd提供了两种请求方式:
- 基于http的方式
- 基于tcp的方式
与NSQD和消费者交互流程
与NSQD交互流程
- NSQLookup 与 NSQD的交互主要是用于NSQD信息的上报和Topic管理
- NSQD在启动时会将自己的一些基本信息上报给NSQLookup
- 有消费者在订阅了Topic和Channel时会将Topic和Channe上报给NSQLookup,有NSQDLookup进行存储
NSQD启动上报基础信息
- 会上报基础的信息,比如端口,主机名称
- 上报Topic信息
func connectCallback(n *NSQD, hostname string) func(*lookupPeer) {
return func(lp *lookupPeer) {
// 基础信息的上报
ci := make(map[string]interface{})
ci["version"] = version.Binary
ci["tcp_port"] = n.getOpts().BroadcastTCPPort
ci["http_port"] = n.getOpts().BroadcastHTTPPort
ci["hostname"] = hostname
ci["broadcast_address"] = n.getOpts().BroadcastAddress
cmd, err := nsq.Identify(ci)
if err != nil {
lp.Close()
return
}
resp, err := lp.Command(cmd)
// ... 省略代码
var commands []*nsq.Command
n.RLock()
// 获取 topic 信息
for _, topic := range n.topicMap {
topic.RLock()
if len(topic.channelMap) == 0 {
commands = append(commands, nsq.Register(topic.name, ""))
} else {
for _, channel := range topic.channelMap {
commands = append(commands, nsq.Register(channel.topicName, channel.name))
}
}
topic.RUnlock()
}
n.RUnlock()
// 上报 topic 信息
for _, cmd := range commands {
n.logf(LOG_INFO, "LOOKUPD(%s): %s", lp, cmd)
_, err := lp.Command(cmd)
if err != nil {
n.logf(LOG_ERROR, "LOOKUPD(%s): %s - %s", lp, cmd, err)
return
}
}
}
}
与消费者交互流程
消费者获取到Topic和Channel的NSQD
// nsqlookup 查询 topic 下的
func (s *httpServer) doLookup(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
reqParams, err := http_api.NewReqParams(req)
if err != nil {
return nil, http_api.Err{400, "INVALID_REQUEST"}
}
topicName, err := reqParams.Get("topic")
if err != nil {
return nil, http_api.Err{400, "MISSING_ARG_TOPIC"}
}
registration := s.nsqlookupd.DB.FindRegistrations("topic", topicName, "")
if len(registration) == 0 {
return nil, http_api.Err{404, "TOPIC_NOT_FOUND"}
}
channels := s.nsqlookupd.DB.FindRegistrations("channel", topicName, "*").SubKeys()
producers := s.nsqlookupd.DB.FindProducers("topic", topicName, "")
// 如果长时间没有更新,就认为这个节点有问题,不会把这个节点的信息加入到可用列表。
producers = producers.FilterByActive(s.nsqlookupd.opts.InactiveProducerTimeout,
s.nsqlookupd.opts.TombstoneLifetime)
return map[string]interface{}{
"channels": channels,
"producers": producers.PeerInfo(),
}, nil
}
客户端和nsqlookupd、nsqd的通信实现
连接NSQLookup
func InitConsumer(topic string, channel string, address string) {
cfg := nsq.NewConfig()
cfg.LookupdPollInterval = time.Second //设置重连时间
c, err := nsq.NewConsumer(topic, channel, cfg) // 新建一个消费者, 消费 topic&channel 数据
if err != nil {
panic(err)
}
c.SetLogger(nil, 0) //屏蔽系统日志
c.AddHandler(&Consumer{}) // 添加消费者接口
// 建立NSQLookup连接
if err := c.ConnectToNSQLookupd(address); err != nil {
panic(err)
}
// c.ConnectToNSQD() 也可以使用 ConnectToNSQD 直接链接 nsqd
}
链接 NSQD
- queryLookupd 会查询到可用的 NSQD列表有哪一些,会跟这些NSQD建立链接
- lookupdLoop 会定时更新可用的 NSQD列表
func (r *Consumer) ConnectToNSQLookupd(addr string) error {
// ... 省略代码
if numLookupd == 1 {
r.queryLookupd()
r.wg.Add(1)
go r.lookupdLoop() // 其实就是在不定的定时调用 queryLookupd
}
return nil
}
// poll all known lookup servers every LookupdPollInterval
func (r *Consumer) lookupdLoop() {
// ... 省略代码
for {
select {
case <-ticker.C:
r.queryLookupd()
case <-r.lookupdRecheckChan:
r.queryLookupd()
case <-r.exitChan:
goto exit
}
}
// ... 省略代码
}
Topic 管理
Topic 创建
topic的创建可以从HTTP 接口和 TCP 接口创建,topic 数据存储在 RegistrationDB 中, Registration 是 topic ,channel,client 的信息,通过Category字段来区别,ProducerMap 是nsqd 的信息
type RegistrationDB struct {
sync.RWMutex
registrationMap map[Registration]ProducerMap
}
// HTTP 接口创建topic
func (s *httpServer) doCreateTopic(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
// ... 省略代码
key := Registration{"topic", topicName, ""}
s.nsqlookupd.DB.AddRegistration(key) // 注册到 RegistrationDB 中
return nil, nil
}
// TCP 接口创建topic
func (p *LookupProtocolV1) REGISTER(client *ClientV1, reader *bufio.Reader, params []string) ([]byte, error) {
if client.peerInfo == nil {
return nil, protocol.NewFatalClientErr(nil, "E_INVALID", "client must IDENTIFY")
}
topic, channel, err := getTopicChan("REGISTER", params)
if err != nil {
return nil, err
}
if channel != "" {
// Category 是 channel
key := Registration{"channel", topic, channel}
if p.nsqlookupd.DB.AddProducer(key, &Producer{peerInfo: client.peerInfo}) {
p.nsqlookupd.logf(LOG_INFO, "DB: client(%s) REGISTER category:%s key:%s subkey:%s",
client, "channel", topic, channel)
}
}
// Category 是 topic
key := Registration{"topic", topic, ""}
if p.nsqlookupd.DB.AddProducer(key, &Producer{peerInfo: client.peerInfo}) {
p.nsqlookupd.logf(LOG_INFO, "DB: client(%s) REGISTER category:%s key:%s subkey:%s",
client, "topic", topic, "")
}
return []byte("OK"), nil
}
NSQAdmin设计原理
NSQAdmin 是一套 WEB UI,用来汇集集群的实时统计,并执行不同的管理任务。
- NSQAdmin 提供页面给用户查询信息,页面地址在:http://127.0.0.1:4171/
- 当获取信息时,NSQAdmin会向NSQLookup 或者 NSQD 请求相关信息
- NSQAdmin 就像是一个Proxy,替用户请求相关的数据,自己本身并不存储信息