在前一篇文章,我们大概了解了NSQ的集群架构和交互流程,这里我们将结合代码详细了解下nsqd的一些特性是如何实现,主要有,
- nsqd与Producer的交互。nsqd如何处理Producer投递的消息,如果消息是延迟发送怎么处理
- nsqd与Consumer的交互。nsqd如何处理Consumer Client的连接,如何把消息投递给Consumer,如果消息ack超时怎么处理
- nsqd与nsqlookupd的交互。nsqd与nsqlookupd定期ping以及获取Topic和Channel的变更信息
数据结构
先看下数据结构的设计,主要三部分:NSQD、Topic和Channel
NSQD
nsq的数据结构定义如下,
type NSQD struct {
// 64bit atomic vars need to be first for proper alignment on 32bit platforms
clientIDSequence int64
sync.RWMutex
opts atomic.Value
dl *dirlock.DirLock
isLoading int32
errValue atomic.Value
startTime time.Time
topicMap map[string]*Topic
clientLock sync.RWMutex
clients map[int64]Client
lookupPeers atomic.Value
tcpListener net.Listener
httpListener net.Listener
httpsListener net.Listener
tlsConfig *tls.Config
poolSize int
notifyChan chan interface{
}
optsNotificationChan chan struct{
}
exitChan chan int
waitGroup util.WaitGroupWrapper
ci *clusterinfo.ClusterInfo
}
NSQD的一些主要的字段,
topicMap map[string]*Topic
存储所有的Topic信息,key为topic name,
clients map[int64]Client
存储当前所有与nsqd建立连接的Consumer,key为ClientID(nsqd会为每个Consumer Client在建立连接时分配一个ID)。
Topic
type Topic struct {
// 64bit atomic vars need to be first for proper alignment on 32bit platforms
messageCount uint64
messageBytes uint64
sync.RWMutex
name string
// 记录Topic对应的Channel
channelMap map[string]*Channel
// 消息后端存储(目前主要是磁盘存储)
backend BackendQueue
// 消息内存存储
memoryMsgChan chan *Message
startChan chan int
exitChan chan int
// 监听Topic下的Channel信息是否更新
channelUpdateChan chan int
waitGroup util.WaitGroupWrapper
exitFlag int32
idFactory *guidFactory
// 是否是临时Topic
ephemeral bool
deleteCallback func(*Topic)
deleter sync.Once
paused int32
pauseChan chan int
ctx *context
}
Channel
NSQ中,消息从Producer投递到Topic,Topic再投递到Channel,和Topic一样,Channel对消息的存储也分内存和后端,其中,后端存储目前也主要是磁盘存储。
type Channel struct {
// 64bit atomic vars need to be first for proper alignment on 32bit platforms
requeueCount uint64
messageCount uint64
timeoutCount uint64
sync.RWMutex
topicName string
name string
ctx *context
// 后端存储
backend BackendQueue
// 内存存储
memoryMsgChan chan *Message
exitFlag int32
exitMutex sync.RWMutex
// state tracking
clients map[int64]Consumer
paused int32
// 是否是临时Channel
ephemeral bool
deleteCallback func(*Channel)
deleter sync.Once
// Stats tracking
e2eProcessingLatencyStream *quantile.Quantile
// TODO: these can be DRYd up
// NSQ支持消息延迟发送,Channel中会存储消息的延迟发送信息
// map存储所有需要延迟发送的消息
deferredMessages map[MessageID]*pqueue.Item
// 延迟发送队列, 基于延迟时间进行堆排序
deferredPQ pqueue.PriorityQueue
deferredMutex sync.Mutex
// map存储处于发送中的消息
inFlightMessages map[MessageID]*Message
// 同样用队列来记录消息的超时时间
inFlightPQ inFlightPqueue
inFlightMutex sync.Mutex
}
功能实现
nsqd启动
先看下nsqd的启动过程,
func (p *program) Start() error {
// 加载 and 验证 启动配置
opts := nsqd.NewOptions()
...
// 加载元数据
err = p.nsqd.LoadMetadata()
// 持久化元数据
err = p.nsqd.PersistMetadata()
// 异步启动 nsqd 实例
go func() {
err := p.nsqd.Main()
if err != nil {
p.Stop()
os.Exit(1)
}
}()
}
上面代码涉及到了MetaData的加载和持久化,这里MetaData主要存储Topic和Channel信息,这样nsqd在崩溃重启时,可以像崩溃前一样继续服务。
func (n *NSQD) Main() error {
// 监听错误退出
exitCh := make(chan error)
var once sync.Once
exitFunc := func(err error) {
once.Do(func() {
if err != nil {
n.logf(LOG_FATAL, "%s", err)
}
exitCh <- err
})
}
tcpServer := &tcpServer{
ctx: ctx}
n.waitGroup.Wrap(func() {
exitFunc(protocol.TCPServer(n.tcpListener, tcpServer, n.logf))
})
httpServer := newHTTPServer(ctx, false, n.getOpts().TLSRequired == TLSRequired)
n.waitGroup.Wrap(func() {
exitFunc(http_api.Serve(n.httpListener, httpServer, "HTTP", n.logf))
})
if n.tlsConfig != nil && n.getOpts().HTTPSAddress != "" {
httpsServer := newHTTPServer(ctx, true, true)
n.waitGroup.Wrap(func() {
exitFunc(http_api.Serve(n.httpsListener, httpsServer, "HTTPS", n.logf))
})
}
n.waitGroup.Wrap(n.queueScanLoop)
n.waitGroup.Wrap(n.lookupLoop)
if n.getOpts().StatsdAddress != "" {
n.waitGroup.Wrap(n.statsdLoop)
}
}
nsqd的启动,除了启动TCP Server和HTTP Server(nsq支持TCP和HTTP两种方式发布消息,消费信息只支持TCP),还启动了三个Goroutine loop,queueScanLoop、lookupLoop 和 statsdLoop。
提供服务
在启动模块我看看到了nsqd启动了TCP Server和HTTP Server,以及三个goroutine,这里详细介绍下这些服务和loop的作用,
TCP Server
nsq支持基于TCP连接来发送、消费消息,创建、删除Topic和Channel等。TCP Server的Handle方法负责处理每个与nsqd建立的TCP连接。nsq制定了一套简单的通讯协议(详细可以参考https://nsq.io/clients/tcp_protocol_spec.html),代码中的实现就是protocolV2的IOLoop方法,
IOLoop
func (p *protocolV2) IOLoop(conn net.Conn) error {
// 注册Client信息。每个与nsqd建立连接的Client都会被分配一个ID
clientID := atomic.AddInt64(&p.ctx.nsqd.clientIDSequence, 1)
client := newClientV2(clientID, conn, p.ctx)
p.ctx.nsqd.AddClient(client.ID, client)
// 启动messagePump goroutine,主要进行