本篇文章的主要目的是记录一下最近在使用RMQ的过程中,碰到的一些坑以及需要注意的地方,表示在使用一个没有用过的工具的时候,还是需要优先简要的读一下相关文档,虽然我也读了!!!

引子

其实,主要就是在用一个可靠的消息队列能够进行消息的订阅推送,并且由于公司所有服务的都具备多个运行实例,希望在推送某些消息的时候,这些集群中的服务器只有一个服务会消费这个消息,刚好发现这个有这个特性,当然MQTT也可以支持共享通道,之用在topic上加上$share/就能让多个订阅了这个共享主题的消费者中的一个消费者消费,另外,NATS也支持这种特性,个人实际上倾向于NATS,无奈公司之前就是使用的RMQ,所以。

效率性能

我们的技术栈是使用的go,关于如何在go中使用RMQ,不多做记录了,只记录一下碰到的坑,从各种文档以及网上各种资料的反馈记录来看,RMQ的性能虽然不是特别好,但是绝对不可能1000条消息推送接收完毕需要一点多秒,完全不明所以,最后仔细翻阅了资料,发现一个可疑的位置点,就是RMQ中有一个channel的概念,而之前呢,我是直接根据Demo中,创建了一个channel,然后一个connection<->channel,这样对应起来了,就相当于我一个连接就搞了一个channel,这个channel,然后使用这个channel来绑定了生产者,无论有多少个队列,多少个消费者,也是使用这个channel来处理,由于最初没看代码,也没看channel相关的文档,所以天然的以为channel是connection的一个直接代理,以为用这个就行了,随后,去仔细查看了文档,原来channel是RMQ的一个信道的概念,一个连接上可疑组织N个信道,而一个信道用来执行一个队列进行消费或者生产,这个在RMQ服务内部,会根据这个来做处理,各个channel之间互不干扰,没看过RMQ服务的源码,只能个人臆测一下,实际上这个channel就相当于在服务端开了一个线程或者一个协程,所有的匹配这个channel的都会在这个里面处理,那么可优化点就来了,我们对于消费者而言,一个队列,咱们给他建立一个channel,有几个消费者就建立几个channel,和其他的区分开,就可疑避免服务端的锁开销处理,然后对于生产者,可疑定义一个发送channel池,发送直接从这个里面取,发完再放回去,然后对于那种消息特别多的,再搞多个connection建立一个连接池,连接池中的每个连接下在按照上面的思路,统一处理,就可以提升不小。由于公司本身的并发并不是特别大,所以,暂时没整连接池,就用了一个连接,然后采用channel池,最后优化之后的结果为,发送1000条消息在50ms左右,从一点多秒优化到50ms,这个提升还是很可观的。

代码中的坑

1.莫名其妙的堵塞

使用的RMQ的库为 github.com/streadway/amqp

代码如下:

channel,err := rmqCon.Channel()
channelErr := make(chan *amqp.Error)
channel.NotifyClose(channelErr)
if err = channel.ExchangeDeclare(c.Name, c.Model.String(), true, false, false, false, nil); err != nil {
	return err
}

上面的代码,在ExchangeDeclare的时候有可能会出现堵死,至于原因,这个需要去看库的源码

func (ch *Channel) call(req message, res ...message) error {
	if err := ch.send(req); err != nil {
		return err
	}

	if req.wait() {
		select {
		case e, ok := <-ch.errors:
			if ok {
				return e
			}
			return ErrClosed

		case msg := <-ch.rpc:
			if msg != nil {
				for _, try := range res {
					if reflect.TypeOf(msg) == reflect.TypeOf(try) {
						// *res = *msg
						vres := reflect.ValueOf(try).Elem()
						vmsg := reflect.ValueOf(msg).Elem()
						vres.Set(vmsg)
						return nil
					}
				}
				return ErrCommandInvalid
			}
			// RPC channel has been closed without an error, likely due to a hard
			// error on the Connection.  This indicates we have already been
			// shutdown and if were waiting, will have returned from the errors chan.
			return ErrClosed
		}
	}

	return nil
}

最终会调用channel的call,然后由于咱们设置了需要RMQ返回数据,也就是参数NoWait=false,

func (msg *exchangeDeclare) wait() bool {
	return true && !msg.NoWait
}

所以,此时req.wait()为真,会进入到下面的等待返回的select语句块中去,正常情况下,这个不会出问题,但是只要发生错误,此时就会堵塞在这里,这个原因就在之前调用了

channel.NotifyClose(channelErr)

给channel绑定了一个关闭通知的通道,而这个通道,没有做消费接收,从而导致了出错的时候卡死,具体分析如下,当RMQ建立连接成功之后,会去执行不断的从连接中接收数据,方法如下:

func (c *Connection) reader(r io.Reader) {
	buf := bufio.NewReader(r)
	frames := &reader{buf}
	conn, haveDeadliner := r.(readDeadliner)

	for {
		frame, err := frames.ReadFrame()

		if err != nil {
			c.shutdown(&Error{Code: FrameError, Reason: err.Error()})
			return
		}

		c.demux(frame)

		if haveDeadliner {
			select {
			case c.deadlines <- conn:
			default:
				// On c.Close() c.heartbeater() might exit just before c.deadlines <- conn is called.
				// Which results in this goroutine being stuck forever.
			}
		}
	}
}

主要的函数就是里面的c.demux(frame),最终会执行到

func (ch *Channel) dispatch(msg message) {
	switch m := msg.(type) {
	case *channelClose:
		// lock before sending connection.close-ok
		// to avoid unexpected interleaving with basic.publish frames if
		// publishing is happening concurrently
		ch.m.Lock()
		ch.send(&channelCloseOk{})
		ch.m.Unlock()
		ch.connection.closeChannel(ch, newError(m.ReplyCode, m.ReplyText))

	case *channelFlow:
		ch.notifyM.RLock()
		for _, c := range ch.flows {
			c <- m.Active
		}
		ch.notifyM.RUnlock()
		ch.send(&channelFlowOk{Active: m.Active})

	case *basicCancel:
		ch.notifyM.RLock()
		for _, c := range ch.cancels {
			c <- m.ConsumerTag
		}
		ch.notifyM.RUnlock()
		ch.consumers.cancel(m.ConsumerTag)

	case *basicReturn:
		ret := newReturn(*m)
		ch.notifyM.RLock()
		for _, c := range ch.returns {
			c <- *ret
		}
		ch.notifyM.RUnlock()

	case *basicAck:
		if ch.confirming {
			if m.Multiple {
				ch.confirms.Multiple(Confirmation{m.DeliveryTag, true})
			} else {
				ch.confirms.One(Confirmation{m.DeliveryTag, true})
			}
		}

	case *basicNack:
		if ch.confirming {
			if m.Multiple {
				ch.confirms.Multiple(Confirmation{m.DeliveryTag, false})
			} else {
				ch.confirms.One(Confirmation{m.DeliveryTag, false})
			}
		}

	case *basicDeliver:
		ch.consumers.send(m.ConsumerTag, newDelivery(ch, m))
		// TODO log failed consumer and close channel, this can happen when
		// deliveries are in flight and a no-wait cancel has happened

	default:
		ch.rpc <- msg
	}
}

当定义出现错误的时候,就会触发*channelClose,从而最终触发

func (c *Connection) closeChannel(ch *Channel, e *Error) {
	ch.shutdown(e)
	c.releaseChannel(ch.id)
}

然后就会堵塞在ch.shudown(e)上面了,因为咱们NotifyClose给通道的close列表中添加了一个通知管道,而这个管道,并没有使用一个goroutine去消费,于是就会一直卡住,等待一个goroutine去消费这个通知 说实话,这个是比较坑的,所以,如果要想这个能正常往下走,请在将各种exchange,还有queueue都处理好了之后,再做NotifyClose,要么就是调用了NotifyClose之后,在调用其他相关方法之前,立即开启一个goroutine去消费这个关闭通知。

2. RMQ的内部队列名称

其实这个问题,是和问题1一起暴露出来的,一般情况下,这问题不会存在,主要就是没有指定队列名称,然后RMQ内部建立了队列名称,队列的名称为amq.*****这种,然后呢,中间网络出了问题,实现断线重连,然后断线重连呢,我就直接传递了第一次RMQ内部创建的队列名称进去(实际上第一次是没指定名称的),然后同事说有些服务收不到RMQ的订阅消息了重启了服务,于是开始分析,最终确认是堵塞在了QueueDeclare那里了,究其原因是因为QueueDeclare不能使用amq.开头作为消息队列的名称,然而,特么的由于之前那个NotifyClose导致这里卡死了,所以实际上根本没清楚到底是啥问题导致的,最后还是在将之前的一个Exchange从Direct模式改到Fanout模式的时候,ExchangeDeclare的时候,也卡死了,然后去看这个库的源码,才特么的发现问题1,于是看到了问题2。

func (c *RbConsume) reconnect(con *amqp.Connection)error {
	if c.autoDelete && strings.HasPrefix(c.QueueName,"amq."){
		c.QueueName = ""
	}
	c.Lock()
	err := c.createSubscribeChannel(con,c.QueueName,c.bindings...)
	c.Unlock()
	return err
}