go笔记 NSQ (6) ( nsqd如何创建消费者以及消费消息)
前面几章中可以看到,nsq进行消息消费的时候主要使用tcpServer去处理,也就是如下的方法
func (p *tcpServer) Handle(clientConn net.Conn) { p.ctx.nsqd.logf(LOG_INFO, "TCP: new client(%s)", clientConn.RemoteAddr()) // The client should initialize itself by sending a 4 byte sequence indicating // the version of the protocol that it intends to communicate, this will allow us // to gracefully upgrade the protocol away from text/line oriented to whatever... buf := make([]byte, 4) _, err := io.ReadFull(clientConn, buf) if err != nil { p.ctx.nsqd.logf(LOG_ERROR, "failed to read protocol version - %s", err) return } protocolMagic := string(buf) p.ctx.nsqd.logf(LOG_INFO, "CLIENT(%s): desired protocol magic '%s'", clientConn.RemoteAddr(), protocolMagic) var prot protocol.Protocol switch protocolMagic { case " V2": prot = &protocolV2{ctx: p.ctx} default: protocol.SendFramedResponse(clientConn, frameTypeError, []byte("E_BAD_PROTOCOL")) clientConn.Close() p.ctx.nsqd.logf(LOG_ERROR, "client(%s) bad protocol magic '%s'", clientConn.RemoteAddr(), protocolMagic) return } err = prot.IOLoop(clientConn) if err != nil { p.ctx.nsqd.logf(LOG_ERROR, "client(%s) - %s", clientConn.RemoteAddr(), err) return } }
在验证了协议魔数后,开启一个新的goroute去处理这个连接,也就是protocolV2.IOLoop来处理这个conn,一旦有消费者连接这个nsqd便会执行这个方法。具体的处理逻辑都在这个方法中。
1.protocolV2.IOLoop
func (p *protocolV2) IOLoop(conn net.Conn) error { var err error var line []byte var zeroTime time.Time //获取一个当前client的id 一般是在已有的client数量上原子性的+1 clientID := atomic.AddInt64(&p.ctx.nsqd.clientIDSequence, 1) //创建一个client client := newClientV2(clientID, conn, p.ctx) //当前client添加到上下文nsqd中 p.ctx.nsqd.AddClient(client.ID, client) //client已经准备好接收消息时才接收消息 messagePumpStartedChan := make(chan bool) //启动一个goroute来处理该client的接收的信号 例如订阅的topic有消息出现等 go p.messagePump(client, messagePumpStartedChan) <-messagePumpStartedChan //接收并处理client端发送过来的各种消息,例如身份验证,作为生产者生产消息,作为消费者消费消息 for { //根据心跳间隔,设置连接超时时间 如果为0说明一直不会超时 if client.HeartbeatInterval > 0 { client.SetReadDeadline(time.Now().Add(client.HeartbeatInterval * 2)) } else { client.SetReadDeadline(zeroTime) } //根据 间隔符来读取数据 这儿用的是换行符 line, err = client.Reader.ReadSlice('\n') if err != nil { if err == io.EOF { err = nil } else { err = fmt.Errorf("failed to read command - %s", err) } break } // 去掉最后一个换行符 line = line[:len(line)-1] //如果用的是回车 \r\n 那要把这个\r也去掉 if len(line) > 0 && line[len(line)-1] == '\r' { line = line[:len(line)-1] } //将信息根据空格进行分隔 params := bytes.Split(line, separatorBytes) p.ctx.nsqd.logf(LOG_DEBUG, "PROTOCOL(V2): [%s] %s", client, params) var response []byte //根据获取到的信息 分别进行不同的处理 //处理结果通常会返回给上面client创建的goroute中处理 response, err = p.Exec(client, params) .................//错误处理 } //如果出错 或者断开连接 就会跳出上面的循环 p.ctx.nsqd.logf(LOG_INFO, "PROTOCOL(V2): [%s] exiting ioloop", client) //关闭连接 conn.Close() close(client.ExitChan) //移除当前client if client.Channel != nil { client.Channel.RemoveClient(client.ID) } p.ctx.nsqd.RemoveClient(client.ID) return err }
首先根据我们的提供的信息创建了client也就是连接客户端。然后下面两个核心操作
go p.messagePump(client, messagePumpStartedChan) 和 response, err = p.Exec(client, params)
前者主要是该client对接收到的信号进行处理,例如有需要消费的消息等,信号是nsqd端发出。后者是对该client接收到的信息处理,例如client身份验证,功能声明等,消息是client端发送过来的。
关于这个client我们可以看下其结构体,并做一些说明
c := &clientV2{ ID: id, //id ctx: ctx, //上下文 里面主要包含了nsqd Conn: conn, //该client的tcp连接 Reader: bufio.NewReaderSize(conn, defaultBufferSize), //该tcp连接的reader Writer: bufio.NewWriterSize(conn, defaultBufferSize), //该tcp连接的writer OutputBufferSize: defaultBufferSize,//写数据缓冲区大小 OutputBufferTimeout: ctx.nsqd.getOpts().OutputBufferTimeout,//写数据缓冲超时时间 MsgTimeout: ctx.nsqd.getOpts().MsgTimeout,//消息超时时间 // ReadyStateChan has a buffer of 1 to guarantee that in the event // there is a race the state update is not lost ReadyStateChan: make(chan int, 1), //该client是否准备好 ExitChan: make(chan int),//client断开信号 ConnectTime: time.Now(),//第一次连接的时间 State: stateInit, ClientID: identifier, //client的验证信息 Hostname: identifier,//client的hostName SubEventChan: make(chan *Channel, 1), //subchan信号,如果该client为消费客户端且发送了sub信息,会接收到信号,接收到的是该client所订阅的topic下的某个channel IdentifyEventChan: make(chan identifyEvent, 1),//验证chan客户端发送验证信息好会收到信号 // heartbeats are client configurable but default to 30s HeartbeatInterval: ctx.nsqd.getOpts().ClientTimeout / 2, //心跳间隔 pubCounts: make(map[string]uint64),//如果为消息生产者则用来统计已生产消息数量 } c.lenSlice = c.lenBuf[:] return c }
在本文中我们需要关注的就是这个SubEventChan,在客户端发送sub指令后该chan会收到信号,内容一般是其所订阅的channel
2. response, err = p.Exec(client, params)
我们先看nsqd是如何处理client端发送过来的信息的。
func (p *protocolV2) Exec(client *clientV2, params [][]byte) ([]byte, error) { if bytes.Equal(params[0], []byte("IDENTIFY")) { return p.IDENTIFY(client, params) } err := enforceTLSPolicy(client, p, params[0]) if err != nil { return nil, err } switch { case bytes.Equal(params[0], []byte("FIN")): return p.FIN(client, params) case bytes.Equal(params[0], []byte("RDY")): return p.RDY(client, params) case bytes.Equal(params[0], []byte("REQ")): return p.REQ(client, params) case bytes.Equal(params[0], []byte("PUB")): return p.PUB(client, params) case bytes.Equal(params[0], []byte("MPUB")): return p.MPUB(client, params) case bytes.Equal(params[0], []byte("DPUB")): return p.DPUB(client, params) case bytes.Equal(params[0], []byte("NOP")): return p.NOP(client, params) case bytes.Equal(params[0], []byte("TOUCH")): return p.TOUCH(client, params) case bytes.Equal(params[0], []byte("SUB")): return p.SUB(client, params) case bytes.Equal(params[0], []byte("CLS")): return p.CLS(client, params) case bytes.Equal(params[0], []byte("AUTH")): return p.AUTH(client, params) } return nil, protocol.NewFatalClientErr(nil, "E_INVALID", fmt.Sprintf("invalid command %s", params[0])) }
可以看到都是根据信息通过空格分隔后的第一段消息来进行判断,选择指令对应的处理方式。这儿可以对部分指令做一个说明
- IDENTIFY client端的身份验证,一般是client端刚连上nsqd后,在协议魔数之后发送的东西,主要提供一些该client的信息,具体的信息可以查看https://nsq.io/clients/tcp_protocol_spec.html
- FIN 丢弃某个消息
- PUB 以生产者的身份生产消息 上节末尾有讲到
- MPUB 以生产者的身份生产多条信息
- SUB 以消费者的身份消费消息
本文主要讲下以消费者的身份消费消息。
2.1 protocolV2.SUB
该方法主要是client作为消费者的时候来消费消息。
func (p *protocolV2) SUB(client *clientV2, params [][]byte) ([]byte, error) { //一些检查 if atomic.LoadInt32(&client.State) != stateInit { return nil, protocol.NewFatalClientErr(nil, "E_INVALID", "cannot SUB in current state") } if client.HeartbeatInterval <= 0 { return nil, protocol.NewFatalClientErr(nil, "E_INVALID", "cannot SUB with heartbeats disabled") } if len(params) < 3 { return nil, protocol.NewFatalClientErr(nil, "E_INVALID", "SUB insufficient number of parameters") } //获取到topicName topicName := string(params[1]) if !protocol.IsValidTopicName(topicName) { return nil, protocol.NewFatalClientErr(nil, "E_BAD_TOPIC", fmt.Sprintf("SUB topic name %q is not valid", topicName)) } //获取到channeName channelName := string(params[2]) if !protocol.IsValidChannelName(channelName) { return nil, protocol.NewFatalClientErr(nil, "E_BAD_CHANNEL", fmt.Sprintf("SUB channel name %q is not valid", channelName)) } //确认该client'是否有权限 if err := p.CheckAuth(client, "SUB", topicName, channelName); err != nil { return nil, err } //获取到指定topic下的指定channel //之所以轮询获取是防止在获取后topic或者channel就已经关闭了 var channel *Channel for { topic := p.ctx.nsqd.GetTopic(topicName) channel = topic.GetChannel(channelName) channel.AddClient(client.ID, client) if (channel.ephemeral && channel.Exiting()) || (topic.ephemeral && topic.Exiting()) { channel.RemoveClient(client.ID) time.Sleep(1 * time.Millisecond) continue } break } atomic.StoreInt32(&client.State, stateSubscribed) //设置该client指定的获取洗脑的channel client.Channel = channel // 将该channel放入SubEventChan 中 client.SubEventChan <- channel return okBytes, nil }
可以看到最后根据sub信息获取到指定的channel,并将这个channel发送到了SubEventChan。这就和上一节讲到的东西串起来了,在上一节生产者的消息成功的传递到了topic,最后成功的分发到了topic下的每个channel的memoryMsgChan中。而此处channel会被放到所有订阅了该channel的client中,后面的处理其实就很容易想到,只要每个client都启动一个goroute,在for中获取其所属channel中memoryMsgChan的值,那每当有消息到达channel时,其下的client就总有一个能获取到该消息。 其实这正是我们1中所看到的另一个重要的方法 go p.messagePump(client, messagePumpStartedChan)中的操作。
3.处理client信号
我们直接看下go p.messagePump(client, messagePumpStartedChan)。方法的具体内容。
func (p *protocolV2) messagePump(client *clientV2, startedChan chan bool) { //声明错误 var err error //内存消息chan 为topic下 var memoryMsgChan chan *Message var backendMsgChan chan []byte var subChannel *Channel // NOTE: `flusherChan` is used to bound message latency for // the pathological case of a channel on a low volume topic // with >1 clients having >1 RDY counts var flusherChan <-chan time.Time var sampleRate int32 //获取client下的subEventChan subEventChan := client.SubEventChan //获取识别 identifyEventChan identifyEventChan := client.IdentifyEventChan //输出buffer缓冲超时 outputBufferTicker := time.NewTicker(client.OutputBufferTimeout) //心跳超时 heartbeatTicker := time.NewTicker(client.HeartbeatInterval) heartbeatChan := heartbeatTicker.C //消息超时 msgTimeout := client.MsgTimeout flushed := true //可以接收client端发送的信息了 close(startedChan) for { //还没准备好的操作 if subChannel == nil || !client.IsReadyForMessages() { // the client is not ready to receive messages... memoryMsgChan = nil backendMsgChan = nil flusherChan = nil // force flush client.writeLock.Lock() err = client.Flush() client.writeLock.Unlock() if err != nil { goto exit } flushed = true //如果需要刷新的话 会进行操作 } else if flushed { // last iteration we flushed... // do not select on the flusher ticker channel memoryMsgChan = subChannel.memoryMsgChan backendMsgChan = subChannel.backend.ReadChan() flusherChan = nil } else { // we're buffered (if there isn't any more data we should flush)... // select on the flusher ticker channel, too memoryMsgChan = subChannel.memoryMsgChan backendMsgChan = subChannel.backend.ReadChan() flusherChan = outputBufferTicker.C } select { //如果接收到刷新信号 则会将flushed置为true //下次循环的时候就会重置 memoryMsgChan backendMsgChan 等 case <-flusherChan: client.writeLock.Lock() err = client.Flush() client.writeLock.Unlock() if err != nil { goto exit } flushed = true //如果接收到了ReadyStateChan 这儿没有操作 case <-client.ReadyStateChan: //如果接收到了subEventChan //*********************************************** //** 注意这儿就是当client端发送sub指令时最后会 * //** 将其所属的channel发送到subEventChan中. * //*********************************************** case subChannel = <-subEventChan: // you can't SUB anymore subEventChan = nil //当client刚连接时会发送IDENTIFY指令 最后会将验证信息发送到该chan case identifyData := <-identifyEventChan: // you can't IDENTIFY anymore identifyEventChan = nil outputBufferTicker.Stop() if identifyData.OutputBufferTimeout > 0 { outputBufferTicker = time.NewTicker(identifyData.OutputBufferTimeout) } heartbeatTicker.Stop() heartbeatChan = nil if identifyData.HeartbeatInterval > 0 { heartbeatTicker = time.NewTicker(identifyData.HeartbeatInterval) heartbeatChan = heartbeatTicker.C } if identifyData.SampleRate > 0 { sampleRate = identifyData.SampleRate } msgTimeout = identifyData.MsgTimeout //如果到了心跳验证的话 发送心跳验证信息 case <-heartbeatChan: err = p.Send(client, frameTypeResponse, heartbeatBytes) if err != nil { goto exit } //接收备份信息并发送给client case b := <-backendMsgChan: if sampleRate > 0 && rand.Int31n(100) > sampleRate { continue } //解码消息 msg, err := decodeMessage(b) if err != nil { p.ctx.nsqd.logf(LOG_ERROR, "failed to decode message - %s", err) continue } //添加消息尝试次数 msg.Attempts++ subChannel.StartInFlightTimeout(msg, client.ID, msgTimeout) client.SendingMessage() //发送消息 err = p.SendMessage(client, msg) if err != nil { goto exit } //下次循环时刷新 flushed = false //接收实时消息并发送给client case msg := <-memoryMsgChan: if sampleRate > 0 && rand.Int31n(100) > sampleRate { continue } //添加消息尝试次数 msg.Attempts++ subChannel.StartInFlightTimeout(msg, client.ID, msgTimeout) client.SendingMessage() //发送消息 err = p.SendMessage(client, msg) if err != nil { goto exit } //下次循环时刷新 flushed = false //如果接收到该client关闭信息 执行关闭代码块 case <-client.ExitChan: goto exit } } //关闭代码块 exit: p.ctx.nsqd.logf(LOG_INFO, "PROTOCOL(V2): [%s] exiting messagePump", client) heartbeatTicker.Stop() outputBufferTicker.Stop() if err != nil { p.ctx.nsqd.logf(LOG_ERROR, "PROTOCOL(V2): [%s] messagePump error - %s", client, err) } }
可以看到其实处理过程也是比较简单的。就是监听多个信号,然后根据不同的信号,做不同的事儿。可以发现goroute+chan的方式可以说将生产者/消费者模型用到了极致,而且不通方法之间极大的松耦合,并且提高了并发性,而不用担心某一环耗时导致整个操作都僵住,这应该也就是go语言的核心魅力了,对于想开发高并发应用来说也主要就是玩转这两了。 这儿需要特殊说明下的是subEventChan,这个chan获取到的值也就是第2步最后我们塞到subEventChan的值,也就是该client所订阅的channel。
到此,其实整个消息的生产以及消费就算走完了,当然,目前还没涉及到nsqlookupd,这个后面会再讲到。
可以看到nsq的生产消费相比与大众化的消息中间件,多了一个channel组件。channel组件的存在支撑了点对点消息传递和广播式的消息传递。nsq在获取到消息后会将消息发送到topic,由topic遍历发送到其所属的每个channel,而channl的消息将被其下的随机一个client获取。
如果需要多个消费端都接收到某个topic下的消息,则可以在该topic下创建多个channel,多个消费者都订阅自己的channel,这样topic有消息则都可以接收到。
如果需要多个消费端随机一个接收某个topic下的消息(比如需要负载均衡的场景),则可以多个消费端都只订阅topic下的唯一一个channel,nsq会将消息只发送到某个client
可以再回顾下nsq官网的那种消息流转图,用来加深理解