redis的订阅,我踩坑了

432 阅读2分钟

就在上周,生产上出了一个问题,问题的大致情况就是连接数达到了最大的数值,我心想,最大值有1万吧,怎么会那么多链接呢 image.png

我火速上到服务器上查看问题的详情

image.png

发现大多数的连接存活时间(age)比较长,(idle)空闲时间到时不长,说明了,链接都在忙,我一看最近的执行命令cmd都是ping之类的。我就想是谁在一直ping服务器呢,并且连接数在重启服务器后,掉了大几千。

我重新检查了项目的代码,在redis连接这块,并没有重复连接的情况,那为什么连接数就是不释放呢,问题陷入了无解的境况,一时没有思路。

于是,我就反思,什么样的场景下,会存在连接服务器呢,如果单纯执行查询缓存的命令,相信是不太会执行重连的逻辑的,此时,映入我眼帘的是,关于redis的发布和订阅功能,我想,从这里获取能找到一些蛛丝马迹,我迅速的看了一下源码,当我们订阅某一个channel时,

pubsub := rdb.Subscribe(context.Background(), "asynq:cancel")

会实例化一个PubSub,这个对象实现了redis中的Pub/Sub命令,该对象在出现网络问题时会自动重连redis服务器和重新订阅指定的channel。

func (c *Client) Subscribe(ctx context.Context, channels ...string) *PubSub {
	pubsub := c.pubSub()
	if len(channels) > 0 {
		_ = pubsub.Subscribe(ctx, channels...)
	}
	return pubsub
}

// Subscribe the client to the specified channels. It returns
// empty subscription if there are no channels.
func (c *PubSub) Subscribe(ctx context.Context, channels ...string) error {
	c.mu.Lock()
	defer c.mu.Unlock()

	err := c.subscribe(ctx, "subscribe", channels...)
	if c.channels == nil {
		c.channels = make(map[string]struct{})
	}
	for _, s := range channels {
		c.channels[s] = struct{}{}
	}
	return err
}

问题似乎有点头绪了,我们继续看,当我们读取订阅的消息时,一般会

ch := pubsub.Channel() 
for msg := range ch { 
    fmt.Println(msg.Channel, msg.Payload) 
}
// Channel returns a Go channel for concurrently receiving messages.
// The channel is closed together with the PubSub. If the Go channel
// is blocked full for 30 seconds the message is dropped.
// Receive* APIs can not be used after channel is created.
//
// go-redis periodically sends ping messages to test connection health
// and re-subscribes if ping can not not received for 30 seconds.
func (c *PubSub) Channel(opts ...ChannelOption) <-chan *Message {
	c.chOnce.Do(func() {
		c.msgCh = newChannel(c, opts...)
		c.msgCh.initMsgChan()
	})
	if c.msgCh == nil {
		err := fmt.Errorf("redis: Channel can't be called after ChannelWithSubscriptions")
		panic(err)
	}
	return c.msgCh.msgCh
}

这也就印证了,为什么我们的连接一直存活,空闲时间较短,那问题该如何解决呢,

pubsub := rdb.Subscribe(context.Background(), "asynq:cancel")
defer pubsub.Close()

在订阅后,业务处理完成后,记得进行关闭,这样,重新在其他地方进行订阅的时候,链接数不至于一直增加,用完所有的链接。重新发布代码后,生产的redis客户端连接数迅速降下来了,保持在正常的数值。