redis: connection pool timeout 错误分析

1,049 阅读1分钟

太长不看版

go redis客户端维护了一个连接池,新请求会尝试从连接池获取一个连接,超过一定时间取不到就会报这个错。

源码分析

go redis客户端维护了一个连接池,这样可以复用连接,减少系统开销 (池化技术介绍)

  • 先看连接池有哪些接口,关键的是Get和Put
    • 当有新请求进来时,调用Get接口获取*Conn
    • 当完成一个请求时,调用Put把连接放回池子
type Pooler interface {
	NewConn(context.Context) (*Conn, error)
	CloseConn(*Conn) error

	Get(context.Context) (*Conn, error)
	Put(context.Context, *Conn)
	Remove(context.Context, *Conn, error)

	Len() int
	IdleLen() int
	Stats() *Stats

	Close() error
}
  • 来看连接池的具体实现
    • queue这个channel用来控制连接数,被设置成连接池大小
    • 当尝试获取一个连接,就往里面塞入一个元素
    • 如果queue满了,就会造成无法写入,一定时间后产生超时
type ConnPool struct {
	cfg *Options

	dialErrorsNum uint32 // atomic
	lastDialError atomic.Value

	queue chan struct{}

	connsMu   sync.Mutex
	conns     []*Conn
	idleConns []*Conn

	poolSize     int
	idleConnsLen int

	stats Stats

	_closed uint32 // atomic
}
  • 新建连接池时会把queue这个channel初始化成连接池大小
func NewConnPool(opt *Options) *ConnPool {
	p := &ConnPool{
		cfg: opt,

		queue:     make(chan struct{}, opt.PoolSize),
		conns:     make([]*Conn, 0, opt.PoolSize),
		idleConns: make([]*Conn, 0, opt.PoolSize),
	}

	p.connsMu.Lock()
	p.checkMinIdleConns()
	p.connsMu.Unlock()

	return p
}
  • 获取连接时会先尝试p.queue <- struct{}{}占位
  • 超时则报ErrPoolTimeout,也就是【redis: connection pool timeout】`
func (p *ConnPool) waitTurn(ctx context.Context) error {
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}

	select {
	case p.queue <- struct{}{}:
		return nil
	default:
	}

	timer := timers.Get().(*time.Timer)
	timer.Reset(p.cfg.PoolTimeout)

	select {
	case <-ctx.Done():
		if !timer.Stop() {
			<-timer.C
		}
		timers.Put(timer)
		return ctx.Err()
	case p.queue <- struct{}{}:
		if !timer.Stop() {
			<-timer.C
		}
		timers.Put(timer)
		return nil
	case <-timer.C:
		timers.Put(timer)
		atomic.AddUint32(&p.stats.Timeouts, 1)
		return ErrPoolTimeout
	}
}

  • 释放连接时会从queue中释放一个空位
func (p *ConnPool) freeTurn() {
    <-p.queue
}