本篇文章的主要目的是记录一下最近在使用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
}